2017-02-02 04:47:22 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2018-09-14 18:03:23 +00:00
|
|
|
import time
|
2017-10-28 20:58:47 +00:00
|
|
|
from datetime import datetime
|
|
|
|
from functools import partial
|
2018-09-14 18:03:23 +00:00
|
|
|
from os import environ
|
2020-04-15 18:41:15 +00:00
|
|
|
|
|
|
|
import plexapi
|
|
|
|
import pytest
|
|
|
|
import requests
|
|
|
|
from plexapi.client import PlexClient
|
2018-09-14 18:03:23 +00:00
|
|
|
from plexapi.myplex import MyPlexAccount
|
2020-04-15 18:41:15 +00:00
|
|
|
from plexapi.server import PlexServer
|
2018-09-14 18:03:23 +00:00
|
|
|
|
2020-05-06 14:15:03 +00:00
|
|
|
from .payloads import ACCOUNT_XML
|
|
|
|
|
2017-10-28 20:58:47 +00:00
|
|
|
try:
|
2019-09-24 18:35:29 +00:00
|
|
|
from unittest.mock import patch, MagicMock, mock_open
|
2017-10-28 20:58:47 +00:00
|
|
|
except ImportError:
|
2019-09-24 18:35:29 +00:00
|
|
|
from mock import patch, MagicMock, mock_open
|
2017-10-28 20:58:47 +00:00
|
|
|
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
SERVER_BASEURL = plexapi.CONFIG.get("auth.server_baseurl")
|
|
|
|
MYPLEX_USERNAME = plexapi.CONFIG.get("auth.myplex_username")
|
|
|
|
MYPLEX_PASSWORD = plexapi.CONFIG.get("auth.myplex_password")
|
|
|
|
CLIENT_BASEURL = plexapi.CONFIG.get("auth.client_baseurl")
|
|
|
|
CLIENT_TOKEN = plexapi.CONFIG.get("auth.client_token")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-05-13 03:25:57 +00:00
|
|
|
MIN_DATETIME = datetime(1999, 1, 1)
|
2020-04-16 21:51:18 +00:00
|
|
|
REGEX_EMAIL = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
|
|
|
|
REGEX_IPADDR = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"
|
2017-04-23 05:18:53 +00:00
|
|
|
|
2017-04-29 05:47:21 +00:00
|
|
|
AUDIOCHANNELS = {2, 6}
|
2020-04-16 21:51:18 +00:00
|
|
|
AUDIOLAYOUTS = {"5.1", "5.1(side)", "stereo"}
|
|
|
|
CODECS = {"aac", "ac3", "dca", "h264", "mp3", "mpeg4"}
|
|
|
|
CONTAINERS = {"avi", "mp4", "mkv"}
|
|
|
|
CONTENTRATINGS = {"TV-14", "TV-MA", "G", "NR", "Not Rated"}
|
|
|
|
FRAMERATES = {"24p", "PAL", "NTSC"}
|
|
|
|
PROFILES = {"advanced simple", "main", "constrained baseline"}
|
|
|
|
RESOLUTIONS = {"sd", "480", "576", "720", "1080"}
|
|
|
|
ENTITLEMENTS = {
|
|
|
|
"ios",
|
|
|
|
"roku",
|
|
|
|
"android",
|
|
|
|
"xbox_one",
|
|
|
|
"xbox_360",
|
|
|
|
"windows",
|
|
|
|
"windows_phone",
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_AUTHENTICATED = "authenticated"
|
|
|
|
TEST_ANONYMOUSLY = "anonymously"
|
2018-09-15 08:42:42 +00:00
|
|
|
ANON_PARAM = pytest.param(TEST_ANONYMOUSLY, marks=pytest.mark.anonymous)
|
|
|
|
AUTH_PARAM = pytest.param(TEST_AUTHENTICATED, marks=pytest.mark.authenticated)
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
def pytest_addoption(parser):
|
2020-04-16 21:51:18 +00:00
|
|
|
parser.addoption(
|
|
|
|
"--client", action="store_true", default=False, help="Run client tests."
|
|
|
|
)
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2018-09-15 08:42:42 +00:00
|
|
|
def pytest_generate_tests(metafunc):
|
2020-04-16 21:51:18 +00:00
|
|
|
if "plex" in metafunc.fixturenames:
|
|
|
|
if (
|
|
|
|
"account" in metafunc.fixturenames
|
|
|
|
or TEST_AUTHENTICATED in metafunc.definition.keywords
|
|
|
|
):
|
|
|
|
metafunc.parametrize("plex", [AUTH_PARAM], indirect=True)
|
2018-09-15 08:42:42 +00:00
|
|
|
else:
|
2020-04-16 21:51:18 +00:00
|
|
|
metafunc.parametrize("plex", [ANON_PARAM, AUTH_PARAM], indirect=True)
|
|
|
|
elif "account" in metafunc.fixturenames:
|
|
|
|
metafunc.parametrize("account", [AUTH_PARAM], indirect=True)
|
2018-09-15 08:42:42 +00:00
|
|
|
|
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
def pytest_runtest_setup(item):
|
2020-04-16 21:51:18 +00:00
|
|
|
if "client" in item.keywords and not item.config.getvalue("client"):
|
|
|
|
return pytest.skip("Need --client option to run.")
|
|
|
|
if TEST_AUTHENTICATED in item.keywords and not (
|
|
|
|
MYPLEX_USERNAME and MYPLEX_PASSWORD
|
|
|
|
):
|
|
|
|
return pytest.skip(
|
|
|
|
"You have to specify MYPLEX_USERNAME and MYPLEX_PASSWORD to run authenticated tests"
|
|
|
|
)
|
2018-09-15 08:42:42 +00:00
|
|
|
if TEST_ANONYMOUSLY in item.keywords and MYPLEX_USERNAME and MYPLEX_PASSWORD:
|
2020-04-16 21:51:18 +00:00
|
|
|
return pytest.skip(
|
|
|
|
"Anonymous tests should be ran on unclaimed server, without providing MYPLEX_USERNAME and "
|
|
|
|
"MYPLEX_PASSWORD"
|
|
|
|
)
|
2017-01-31 00:02:22 +00:00
|
|
|
|
|
|
|
|
2017-06-06 01:40:52 +00:00
|
|
|
# ---------------------------------
|
|
|
|
# Fixtures
|
|
|
|
# ---------------------------------
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2018-09-15 08:42:42 +00:00
|
|
|
|
|
|
|
def get_account():
|
|
|
|
return MyPlexAccount()
|
|
|
|
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
@pytest.fixture(scope="session")
|
2017-04-15 00:47:59 +00:00
|
|
|
def account():
|
2020-04-16 21:51:18 +00:00
|
|
|
assert MYPLEX_USERNAME, "Required MYPLEX_USERNAME not specified."
|
|
|
|
assert MYPLEX_PASSWORD, "Required MYPLEX_PASSWORD not specified."
|
2018-09-15 08:42:42 +00:00
|
|
|
return get_account()
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
@pytest.fixture(scope="session")
|
2018-09-14 18:03:23 +00:00
|
|
|
def account_once(account):
|
2020-04-16 21:51:18 +00:00
|
|
|
if environ.get("TEST_ACCOUNT_ONCE") != "1" and environ.get("CI") == "true":
|
|
|
|
pytest.skip("Do not forget to test this by providing TEST_ACCOUNT_ONCE=1")
|
2018-09-14 18:03:23 +00:00
|
|
|
return account
|
|
|
|
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
@pytest.fixture(scope="session")
|
2018-09-14 18:03:23 +00:00
|
|
|
def account_plexpass(account):
|
|
|
|
if not account.subscriptionActive:
|
2020-04-16 21:51:18 +00:00
|
|
|
pytest.skip(
|
|
|
|
"PlexPass subscription is not active, unable to test sync-stuff, be careful!"
|
|
|
|
)
|
2018-09-14 18:03:23 +00:00
|
|
|
return account
|
|
|
|
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
@pytest.fixture(scope="session")
|
2018-09-14 18:03:23 +00:00
|
|
|
def account_synctarget(account_plexpass):
|
2020-04-16 21:51:18 +00:00
|
|
|
assert "sync-target" in plexapi.X_PLEX_PROVIDES, (
|
|
|
|
"You have to set env var " "PLEXAPI_HEADER_PROVIDES=sync-target,controller"
|
|
|
|
)
|
|
|
|
assert "sync-target" in plexapi.BASE_HEADERS["X-Plex-Provides"]
|
|
|
|
assert (
|
|
|
|
"iOS" == plexapi.X_PLEX_PLATFORM
|
|
|
|
), "You have to set env var PLEXAPI_HEADER_PLATFORM=iOS"
|
|
|
|
assert (
|
|
|
|
"11.4.1" == plexapi.X_PLEX_PLATFORM_VERSION
|
|
|
|
), "You have to set env var PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1"
|
|
|
|
assert (
|
|
|
|
"iPhone" == plexapi.X_PLEX_DEVICE
|
|
|
|
), "You have to set env var PLEXAPI_HEADER_DEVICE=iPhone"
|
2018-09-14 18:03:23 +00:00
|
|
|
return account_plexpass
|
2018-09-08 15:25:16 +00:00
|
|
|
|
|
|
|
|
2020-05-06 14:15:03 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def mocked_account(requests_mock):
|
|
|
|
requests_mock.get("https://plex.tv/users/account", text=ACCOUNT_XML)
|
|
|
|
return MyPlexAccount(token="faketoken")
|
|
|
|
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
@pytest.fixture(scope="session")
|
2018-09-15 08:42:42 +00:00
|
|
|
def plex(request):
|
2020-04-16 21:51:18 +00:00
|
|
|
assert SERVER_BASEURL, "Required SERVER_BASEURL not specified."
|
2017-04-15 00:47:59 +00:00
|
|
|
session = requests.Session()
|
2018-09-15 08:42:42 +00:00
|
|
|
if request.param == TEST_AUTHENTICATED:
|
|
|
|
token = get_account().authenticationToken
|
|
|
|
else:
|
|
|
|
token = None
|
|
|
|
return PlexServer(SERVER_BASEURL, token, session=session)
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-02-27 05:36:20 +00:00
|
|
|
|
2018-09-08 15:25:16 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def device(account):
|
|
|
|
d = None
|
|
|
|
for device in account.devices():
|
|
|
|
if device.clientIdentifier == plexapi.X_PLEX_IDENTIFIER:
|
|
|
|
d = device
|
|
|
|
break
|
|
|
|
|
|
|
|
assert d
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def clear_sync_device(device, account_synctarget, plex):
|
|
|
|
sync_items = account_synctarget.syncItems(clientId=device.clientIdentifier)
|
|
|
|
for item in sync_items.items:
|
|
|
|
item.delete()
|
|
|
|
plex.refreshSync()
|
|
|
|
return device
|
|
|
|
|
|
|
|
|
2017-07-17 14:11:03 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def fresh_plex():
|
|
|
|
return PlexServer
|
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
2020-04-15 20:53:17 +00:00
|
|
|
def plex2(plex):
|
2017-04-15 00:47:59 +00:00
|
|
|
return plex()
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2017-04-26 03:09:37 +00:00
|
|
|
@pytest.fixture()
|
2020-04-15 20:53:17 +00:00
|
|
|
def client(request, plex):
|
|
|
|
return PlexClient(plex, baseurl=CLIENT_BASEURL, token=CLIENT_TOKEN)
|
2017-04-26 03:09:37 +00:00
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def tvshows(plex):
|
2020-04-16 21:51:18 +00:00
|
|
|
return plex.library.section("TV Shows")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def movies(plex):
|
2020-04-16 21:51:18 +00:00
|
|
|
return plex.library.section("Movies")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def music(plex):
|
2020-04-16 21:51:18 +00:00
|
|
|
return plex.library.section("Music")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def photos(plex):
|
2020-04-16 21:51:18 +00:00
|
|
|
return plex.library.section("Photos")
|
2017-01-31 00:02:22 +00:00
|
|
|
|
2017-02-27 05:36:20 +00:00
|
|
|
|
2017-01-31 00:02:22 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def movie(movies):
|
2020-04-16 21:51:18 +00:00
|
|
|
return movies.get("Elephants Dream")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2019-07-31 18:53:06 +00:00
|
|
|
@pytest.fixture()
|
2020-04-16 21:51:18 +00:00
|
|
|
def collection(plex):
|
2019-09-21 20:54:50 +00:00
|
|
|
try:
|
2020-04-16 21:51:18 +00:00
|
|
|
return plex.library.section("Movies").collection()[0]
|
2019-09-21 20:54:50 +00:00
|
|
|
except IndexError:
|
2020-04-16 21:51:18 +00:00
|
|
|
movie = plex.library.section("Movies").get("Elephants Dream")
|
2019-09-21 20:54:50 +00:00
|
|
|
movie.addCollection(["marvel"])
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
n = plex.library.section("Movies").reload()
|
2020-04-15 18:41:15 +00:00
|
|
|
return n.collection()[0]
|
2019-09-21 21:17:35 +00:00
|
|
|
|
2019-07-31 18:53:06 +00:00
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def artist(music):
|
2020-04-29 21:23:22 +00:00
|
|
|
return music.get("Broke For Free")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def album(artist):
|
2020-04-29 21:23:22 +00:00
|
|
|
return artist.album("Layers")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def track(album):
|
2020-04-29 21:23:22 +00:00
|
|
|
return album.track("As Colourful as Ever")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def show(tvshows):
|
2020-04-16 21:51:18 +00:00
|
|
|
return tvshows.get("Game of Thrones")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def episode(show):
|
2020-04-16 21:51:18 +00:00
|
|
|
return show.get("Winter Is Coming")
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def photoalbum(photos):
|
2017-04-23 05:54:53 +00:00
|
|
|
try:
|
2020-04-16 21:51:18 +00:00
|
|
|
return photos.get("Cats")
|
2019-10-03 00:58:00 +00:00
|
|
|
except Exception:
|
2020-04-16 21:51:18 +00:00
|
|
|
return photos.get("photo_album1")
|
2020-04-15 18:41:15 +00:00
|
|
|
|
|
|
|
|
2019-09-24 03:13:17 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def subtitle():
|
|
|
|
mopen = mock_open()
|
2020-04-16 21:51:18 +00:00
|
|
|
with patch("__main__.open", mopen):
|
|
|
|
with open("subtitle.srt", "w") as handler:
|
|
|
|
handler.write("test")
|
2019-09-24 03:13:17 +00:00
|
|
|
return handler
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2018-09-14 18:03:23 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def shared_username(account):
|
2020-04-16 21:51:18 +00:00
|
|
|
username = environ.get("SHARED_USERNAME", "PKKid")
|
2018-09-14 18:03:23 +00:00
|
|
|
for user in account.users():
|
|
|
|
if user.title.lower() == username.lower():
|
|
|
|
return username
|
2020-04-16 21:51:18 +00:00
|
|
|
elif (
|
|
|
|
user.username
|
|
|
|
and user.email
|
|
|
|
and user.id
|
|
|
|
and username.lower()
|
|
|
|
in (user.username.lower(), user.email.lower(), str(user.id))
|
|
|
|
):
|
2018-09-14 18:03:23 +00:00
|
|
|
return username
|
2020-04-16 21:51:18 +00:00
|
|
|
pytest.skip("Shared user %s wasn`t found in your MyPlex account" % username)
|
2018-09-14 18:03:23 +00:00
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def monkeydownload(request, monkeypatch):
|
2020-04-16 21:51:18 +00:00
|
|
|
monkeypatch.setattr(
|
|
|
|
"plexapi.utils.download", partial(plexapi.utils.download, mocked=True)
|
|
|
|
)
|
2017-01-09 14:21:54 +00:00
|
|
|
yield
|
|
|
|
monkeypatch.undo()
|
2017-04-15 00:47:59 +00:00
|
|
|
|
|
|
|
|
2017-10-28 20:58:47 +00:00
|
|
|
def callable_http_patch():
|
2017-11-01 22:18:18 +00:00
|
|
|
"""This intented to stop some http requests inside some tests."""
|
2020-04-16 21:51:18 +00:00
|
|
|
return patch(
|
|
|
|
"plexapi.server.requests.sessions.Session.send",
|
|
|
|
return_value=MagicMock(status_code=200, text="<xml><child></child></xml>"),
|
|
|
|
)
|
2017-10-28 20:58:47 +00:00
|
|
|
|
2017-10-26 21:55:59 +00:00
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def empty_response(mocker):
|
2020-04-16 21:51:18 +00:00
|
|
|
response = mocker.MagicMock(status_code=200, text="<xml><child></child></xml>")
|
2017-10-26 21:55:59 +00:00
|
|
|
return response
|
|
|
|
|
2017-10-26 23:09:17 +00:00
|
|
|
|
2017-10-26 21:55:59 +00:00
|
|
|
@pytest.fixture()
|
2017-10-28 22:07:06 +00:00
|
|
|
def patched_http_call(mocker):
|
2017-11-01 22:18:18 +00:00
|
|
|
"""This will stop any http calls inside any test."""
|
2020-04-16 21:51:18 +00:00
|
|
|
return mocker.patch(
|
|
|
|
"plexapi.server.requests.sessions.Session.send",
|
|
|
|
return_value=MagicMock(status_code=200, text="<xml><child></child></xml>"),
|
|
|
|
)
|
2017-10-26 21:55:59 +00:00
|
|
|
|
|
|
|
|
2017-06-06 01:40:52 +00:00
|
|
|
# ---------------------------------
|
|
|
|
# Utility Functions
|
|
|
|
# ---------------------------------
|
2017-04-15 00:47:59 +00:00
|
|
|
def is_datetime(value):
|
2020-10-08 20:15:43 +00:00
|
|
|
if value is None:
|
|
|
|
return True
|
2017-04-15 00:47:59 +00:00
|
|
|
return value > MIN_DATETIME
|
|
|
|
|
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
def is_int(value, gte=1):
|
|
|
|
return int(value) >= gte
|
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
def is_float(value, gte=1.0):
|
|
|
|
return float(value) >= gte
|
2017-04-15 00:47:59 +00:00
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
def is_metadata(key, prefix="/library/metadata/", contains="", suffix=""):
|
2017-04-23 05:18:53 +00:00
|
|
|
try:
|
|
|
|
assert key.startswith(prefix)
|
|
|
|
assert contains in key
|
|
|
|
assert key.endswith(suffix)
|
|
|
|
return True
|
|
|
|
except AssertionError:
|
|
|
|
return False
|
2017-04-15 00:47:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_part(key):
|
2020-04-16 21:51:18 +00:00
|
|
|
return is_metadata(key, prefix="/library/parts/")
|
2017-04-23 05:18:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_section(key):
|
2020-04-16 21:51:18 +00:00
|
|
|
return is_metadata(key, prefix="/library/sections/")
|
2017-04-23 05:18:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_string(value, gte=1):
|
2020-05-12 21:26:29 +00:00
|
|
|
return isinstance(value, str) and len(value) >= gte
|
2017-04-15 00:47:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_thumb(key):
|
2020-04-16 21:51:18 +00:00
|
|
|
return is_metadata(key, contains="/thumb/")
|
2018-09-14 18:03:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def wait_until(condition_function, delay=0.25, timeout=1, *args, **kwargs):
|
|
|
|
start = time.time()
|
|
|
|
ready = condition_function(*args, **kwargs)
|
|
|
|
retries = 1
|
|
|
|
while not ready and time.time() - start < timeout:
|
|
|
|
retries += 1
|
|
|
|
time.sleep(delay)
|
|
|
|
ready = condition_function(*args, **kwargs)
|
|
|
|
|
2020-04-16 21:51:18 +00:00
|
|
|
assert ready, "Wait timeout after %d retries, %.2f seconds" % (
|
|
|
|
retries,
|
|
|
|
time.time() - start,
|
|
|
|
)
|
2018-09-14 18:03:23 +00:00
|
|
|
|
|
|
|
return ready
|