Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
263572eb66 | ||
|
|
3a97a928de | ||
|
|
0322414de1 | ||
|
|
1c75e86863 | ||
|
|
0ae71f6367 | ||
|
|
af7c38695a | ||
|
|
c741c22876 | ||
|
|
06b9b12eeb | ||
|
|
daf7078f98 | ||
|
|
c2db57ecb3 | ||
|
|
0336c6a8f8 | ||
|
|
22d3fa9565 | ||
|
|
9d81973b3a | ||
|
|
fe8eac3dc6 | ||
|
|
5fc315201a | ||
|
|
a9d3d5568b | ||
|
|
f88dbdaeba | ||
|
|
48028914de | ||
|
|
fcdb70d246 | ||
|
|
e8589bfa05 | ||
|
|
854ffbf26a | ||
|
|
ca109f703c | ||
|
|
82d5a7ea51 | ||
|
|
79ead7b826 | ||
|
|
6e360b2bb1 | ||
|
|
1aaa5b649f | ||
|
|
435f3b9831 | ||
|
|
bcf0bb5525 | ||
|
|
322aae37dc | ||
|
|
45cb4028b8 | ||
|
|
151d9b7375 | ||
|
|
d393c861d7 | ||
|
|
02c4b2e7e1 | ||
|
|
77959d6d94 | ||
|
|
ad832c4d2d | ||
|
|
0ce2aec139 | ||
|
|
d3761b23ec | ||
|
|
cdbd1a4c27 | ||
|
|
2d2e569595 | ||
|
|
391373b304 | ||
|
|
a030f46b4d | ||
|
|
f6dc22f241 | ||
|
|
220638e525 | ||
|
|
24b1aac1be | ||
|
|
c4608f4258 | ||
|
|
ca19ea62a4 | ||
|
|
2d8f4d8289 | ||
|
|
24ca9d9617 | ||
|
|
41aa1839b7 | ||
|
|
40354e87a0 |
+37
@@ -4,6 +4,43 @@ Changelog
|
|||||||
|
|
||||||
A list of changes between each release
|
A list of changes between each release
|
||||||
|
|
||||||
|
0.16.1 (2020-07-29)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Unpin requeirements, set minimum version instead
|
||||||
|
- Bump coverage to 5.2.1
|
||||||
|
- Bump pytest to 6.0.0
|
||||||
|
|
||||||
|
|
||||||
|
0.16.0 (2020-07-20)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Breaking Changes:**
|
||||||
|
|
||||||
|
- Add arm property to camera, deprecate motion enable method (`#273 <https://github.com/fronzbot/blinkpy/pull/273>`__)
|
||||||
|
- Complete refactoring of auth logic (breaks all pre-0.16.0 setups!) (`#261 <https://github.com/fronzbot/blinkpy/pull/261>`__)
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
|
||||||
|
- Add is_errored property to Auth class (`#275 <https://github.com/fronzbot/blinkpy/pull/275>`__)
|
||||||
|
- Add new endpoint to get user infor (`#280 <https://github.com/fronzbot/blinkpy/pull/280>`__)
|
||||||
|
- Add get_liveview command to camera module (`#289 <https://github.com/fronzbot/blinkpy/pull/289>`__)
|
||||||
|
- Add blink Mini Camera support (`#290 <https://github.com/fronzbot/blinkpy/pull/290>`__)
|
||||||
|
- Add option to skip homescreen check (`#305 <https://github.com/fronzbot/blinkpy/pull/305>`__)
|
||||||
|
- Add different timeout for video and image retrieval (`#323 <https://github.com/fronzbot/blinkpy/pull/323>`__)
|
||||||
|
- Modifiy session to use HTTPAdapter and handle retries (`#324 <https://github.com/fronzbot/blinkpy/pull/324>`__)
|
||||||
|
- Add retry option overrides (`#339 <https://github.com/fronzbot/blinkpy/pull/339>`__)
|
||||||
|
|
||||||
|
**All changes:**
|
||||||
|
|
||||||
|
Please see the change list in the (`Release Notes <https://github.com/fronzbot/releases/tag/v0.16.0>`__)
|
||||||
|
|
||||||
|
|
||||||
|
0.15.1 (2020-07-11)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
- Bugfix: remove "Host" from auth header (`#330 <https://github.com/fronzbot/blinkpy/pull/330>`__)
|
||||||
|
|
||||||
|
|
||||||
0.15.0 (2020-05-08)
|
0.15.0 (2020-05-08)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
**Breaking Changes:**
|
**Breaking Changes:**
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from blinkpy.helpers.util import get_time, Throttle
|
from blinkpy.helpers.util import get_time, Throttle
|
||||||
from blinkpy.helpers.constants import DEFAULT_URL
|
from blinkpy.helpers.constants import DEFAULT_URL, TIMEOUT
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ def request_motion_detection_disable(blink, network, camera_id):
|
|||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def http_get(blink, url, stream=False, json=True, is_retry=False):
|
def http_get(blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT):
|
||||||
"""
|
"""
|
||||||
Perform an http get request.
|
Perform an http get request.
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ def http_get(blink, url, stream=False, json=True, is_retry=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def http_post(blink, url, is_retry=False):
|
def http_post(blink, url, is_retry=False, timeout=TIMEOUT):
|
||||||
"""
|
"""
|
||||||
Perform an http post request.
|
Perform an http post request.
|
||||||
|
|
||||||
|
|||||||
+38
-24
@@ -2,9 +2,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from requests import Request, Session, exceptions
|
from requests import Request, Session, exceptions
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
from blinkpy import api
|
from blinkpy import api
|
||||||
from blinkpy.helpers import util
|
from blinkpy.helpers import util
|
||||||
from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT
|
from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT, TIMEOUT
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class Auth:
|
class Auth:
|
||||||
"""Class to handle login communication."""
|
"""Class to handle login communication."""
|
||||||
|
|
||||||
def __init__(self, login_data=None, no_prompt=False, login_method="v4"):
|
def __init__(self, login_data=None, no_prompt=False):
|
||||||
"""
|
"""
|
||||||
Initialize auth handler.
|
Initialize auth handler.
|
||||||
|
|
||||||
@@ -22,7 +24,6 @@ class Auth:
|
|||||||
- password
|
- password
|
||||||
:param no_prompt: Should any user input prompts
|
:param no_prompt: Should any user input prompts
|
||||||
be supressed? True/FALSE
|
be supressed? True/FALSE
|
||||||
:param login_method: Choose the login endpoint to use. Default: v4. v3 uses email verification rather than a 2FA code.
|
|
||||||
"""
|
"""
|
||||||
if login_data is None:
|
if login_data is None:
|
||||||
login_data = {}
|
login_data = {}
|
||||||
@@ -32,7 +33,6 @@ class Auth:
|
|||||||
self.region_id = login_data.get("region_id", None)
|
self.region_id = login_data.get("region_id", None)
|
||||||
self.client_id = login_data.get("client_id", None)
|
self.client_id = login_data.get("client_id", None)
|
||||||
self.account_id = login_data.get("account_id", None)
|
self.account_id = login_data.get("account_id", None)
|
||||||
self.login_method = login_method
|
|
||||||
self.login_response = None
|
self.login_response = None
|
||||||
self.is_errored = False
|
self.is_errored = False
|
||||||
self.no_prompt = no_prompt
|
self.no_prompt = no_prompt
|
||||||
@@ -53,19 +53,27 @@ class Auth:
|
|||||||
"""Return authorization header."""
|
"""Return authorization header."""
|
||||||
if self.token is None:
|
if self.token is None:
|
||||||
return None
|
return None
|
||||||
return {"Host": self.host, "TOKEN_AUTH": self.token}
|
return {"TOKEN_AUTH": self.token}
|
||||||
|
|
||||||
@property
|
def create_session(self, opts=None):
|
||||||
def login_url(self):
|
|
||||||
"""Return login url."""
|
|
||||||
if self.login_method not in LOGIN_ENDPOINT:
|
|
||||||
return LOGIN_ENDPOINT["v4"]
|
|
||||||
return LOGIN_ENDPOINT[self.login_method]
|
|
||||||
|
|
||||||
def create_session(self):
|
|
||||||
"""Create a session for blink communication."""
|
"""Create a session for blink communication."""
|
||||||
|
if opts is None:
|
||||||
|
opts = {}
|
||||||
|
backoff = opts.get("backoff", 1)
|
||||||
|
retries = opts.get("retries", 3)
|
||||||
|
retry_list = opts.get("retry_list", [429, 500, 502, 503, 504])
|
||||||
sess = Session()
|
sess = Session()
|
||||||
sess.get = partial(sess.get, timeout=10)
|
assert_status_hook = [
|
||||||
|
lambda response, *args, **kwargs: response.raise_for_status()
|
||||||
|
]
|
||||||
|
sess.hooks["response"] = assert_status_hook
|
||||||
|
retry = Retry(
|
||||||
|
total=retries, backoff_factor=backoff, status_forcelist=retry_list
|
||||||
|
)
|
||||||
|
adapter = HTTPAdapter(max_retries=retry)
|
||||||
|
sess.mount("https://", adapter)
|
||||||
|
sess.mount("http://", adapter)
|
||||||
|
sess.get = partial(sess.get, timeout=TIMEOUT)
|
||||||
return sess
|
return sess
|
||||||
|
|
||||||
def prepare_request(self, url, headers, data, reqtype):
|
def prepare_request(self, url, headers, data, reqtype):
|
||||||
@@ -82,11 +90,11 @@ class Auth:
|
|||||||
|
|
||||||
self.data = util.validate_login_data(self.data)
|
self.data = util.validate_login_data(self.data)
|
||||||
|
|
||||||
def login(self):
|
def login(self, login_url=LOGIN_ENDPOINT):
|
||||||
"""Attempt login to blink servers."""
|
"""Attempt login to blink servers."""
|
||||||
self.validate_login()
|
self.validate_login()
|
||||||
_LOGGER.info("Attempting login with %s", self.login_url)
|
_LOGGER.info("Attempting login with %s", login_url)
|
||||||
response = api.request_login(self, self.login_url, self.data, is_retry=False,)
|
response = api.request_login(self, login_url, self.data, is_retry=False,)
|
||||||
try:
|
try:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -149,6 +157,7 @@ class Auth:
|
|||||||
stream=False,
|
stream=False,
|
||||||
json_resp=True,
|
json_resp=True,
|
||||||
is_retry=False,
|
is_retry=False,
|
||||||
|
timeout=TIMEOUT,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Perform server requests.
|
Perform server requests.
|
||||||
@@ -163,21 +172,25 @@ class Auth:
|
|||||||
"""
|
"""
|
||||||
req = self.prepare_request(url, headers, data, reqtype)
|
req = self.prepare_request(url, headers, data, reqtype)
|
||||||
try:
|
try:
|
||||||
response = self.session.send(req, stream=stream)
|
response = self.session.send(req, stream=stream, timeout=timeout)
|
||||||
return self.validate_response(response, json_resp)
|
return self.validate_response(response, json_resp)
|
||||||
except (exceptions.ConnectionError, exceptions.Timeout):
|
except (exceptions.ConnectionError, exceptions.Timeout):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Connection error. Endpoint %s possibly down or throttled. %s: %s",
|
"Connection error. Endpoint %s possibly down or throttled.", url,
|
||||||
url,
|
|
||||||
response.status_code,
|
|
||||||
response.reason,
|
|
||||||
)
|
)
|
||||||
except BlinkBadResponse:
|
except BlinkBadResponse:
|
||||||
|
code = None
|
||||||
|
reason = None
|
||||||
|
try:
|
||||||
|
code = response.status_code
|
||||||
|
reason = response.reason
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Expected json response from %s, but received: %s: %s",
|
"Expected json response from %s, but received: %s: %s",
|
||||||
url,
|
url,
|
||||||
response.status_code,
|
code,
|
||||||
response.reason,
|
reason,
|
||||||
)
|
)
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
try:
|
try:
|
||||||
@@ -191,6 +204,7 @@ class Auth:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
json_resp=json_resp,
|
json_resp=json_resp,
|
||||||
is_retry=True,
|
is_retry=True,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
_LOGGER.error("Unable to access %s after token refresh.", url)
|
_LOGGER.error("Unable to access %s after token refresh.", url)
|
||||||
except TokenRefreshFailed:
|
except TokenRefreshFailed:
|
||||||
|
|||||||
+8
-1
@@ -29,6 +29,7 @@ from blinkpy.helpers.constants import (
|
|||||||
DEFAULT_MOTION_INTERVAL,
|
DEFAULT_MOTION_INTERVAL,
|
||||||
DEFAULT_REFRESH,
|
DEFAULT_REFRESH,
|
||||||
MIN_THROTTLE_TIME,
|
MIN_THROTTLE_TIME,
|
||||||
|
TIMEOUT_MEDIA,
|
||||||
)
|
)
|
||||||
from blinkpy.helpers.constants import __version__
|
from blinkpy.helpers.constants import __version__
|
||||||
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
|
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
|
||||||
@@ -330,7 +331,13 @@ class Blink:
|
|||||||
_LOGGER.info("%s already exists, skipping...", filename)
|
_LOGGER.info("%s already exists, skipping...", filename)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response = api.http_get(self, url=clip_address, stream=True, json=False)
|
response = api.http_get(
|
||||||
|
self,
|
||||||
|
url=clip_address,
|
||||||
|
stream=True,
|
||||||
|
json=False,
|
||||||
|
timeout=TIMEOUT_MEDIA,
|
||||||
|
)
|
||||||
with open(filename, "wb") as vidfile:
|
with open(filename, "wb") as vidfile:
|
||||||
copyfileobj(response.raw, vidfile)
|
copyfileobj(response.raw, vidfile)
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -3,6 +3,7 @@
|
|||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
import logging
|
import logging
|
||||||
from blinkpy import api
|
from blinkpy import api
|
||||||
|
from blinkpy.helpers.constants import TIMEOUT_MEDIA
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -150,9 +151,7 @@ class BlinkCamera:
|
|||||||
if config.get("thumbnail", False):
|
if config.get("thumbnail", False):
|
||||||
thumb_addr = config["thumbnail"]
|
thumb_addr = config["thumbnail"]
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning("Could not find thumbnail for camera %s", self.name)
|
||||||
"Could not find thumbnail for camera %s", self.name, exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if thumb_addr is not None:
|
if thumb_addr is not None:
|
||||||
new_thumbnail = f"{self.sync.urls.base_url}{thumb_addr}.jpg"
|
new_thumbnail = f"{self.sync.urls.base_url}{thumb_addr}.jpg"
|
||||||
@@ -182,11 +181,19 @@ class BlinkCamera:
|
|||||||
|
|
||||||
if new_thumbnail is not None and (update_cached_image or force_cache):
|
if new_thumbnail is not None and (update_cached_image or force_cache):
|
||||||
self._cached_image = api.http_get(
|
self._cached_image = api.http_get(
|
||||||
self.sync.blink, url=self.thumbnail, stream=True, json=False
|
self.sync.blink,
|
||||||
|
url=self.thumbnail,
|
||||||
|
stream=True,
|
||||||
|
json=False,
|
||||||
|
timeout=TIMEOUT_MEDIA,
|
||||||
)
|
)
|
||||||
if clip_addr is not None and (update_cached_video or force_cache):
|
if clip_addr is not None and (update_cached_video or force_cache):
|
||||||
self._cached_video = api.http_get(
|
self._cached_video = api.http_get(
|
||||||
self.sync.blink, url=self.clip, stream=True, json=False
|
self.sync.blink,
|
||||||
|
url=self.clip,
|
||||||
|
stream=True,
|
||||||
|
json=False,
|
||||||
|
timeout=TIMEOUT_MEDIA,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_liveview(self):
|
def get_liveview(self):
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 16
|
MINOR_VERSION = 17
|
||||||
PATCH_VERSION = "0-rc10"
|
PATCH_VERSION = "0-rc0"
|
||||||
|
|
||||||
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
||||||
|
|
||||||
@@ -48,10 +48,7 @@ URLS
|
|||||||
BLINK_URL = "immedia-semi.com"
|
BLINK_URL = "immedia-semi.com"
|
||||||
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
|
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
|
||||||
BASE_URL = f"https://{DEFAULT_URL}"
|
BASE_URL = f"https://{DEFAULT_URL}"
|
||||||
LOGIN_ENDPOINT = {
|
LOGIN_ENDPOINT = f"{BASE_URL}/api/v4/account/login"
|
||||||
"v4": f"{BASE_URL}/api/v4/account/login",
|
|
||||||
"v3": f"{BASE_URL}/api/v3/login",
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Dictionaries
|
Dictionaries
|
||||||
@@ -68,3 +65,5 @@ DEFAULT_REFRESH = 30
|
|||||||
MIN_THROTTLE_TIME = 2
|
MIN_THROTTLE_TIME = 2
|
||||||
SIZE_NOTIFICATION_KEY = 152
|
SIZE_NOTIFICATION_KEY = 152
|
||||||
SIZE_UID = 16
|
SIZE_UID = 16
|
||||||
|
TIMEOUT = 10
|
||||||
|
TIMEOUT_MEDIA = 90
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ class BlinkSyncModule:
|
|||||||
self.serial = self.summary["serial"]
|
self.serial = self.summary["serial"]
|
||||||
self.status = self.summary["status"]
|
self.status = self.summary["status"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Could not extract some sync module info: %s", response)
|
||||||
"Could not extract some sync module info: %s", response, exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
is_ok = self.get_network_info()
|
is_ok = self.get_network_info()
|
||||||
self.check_new_videos()
|
self.check_new_videos()
|
||||||
@@ -158,7 +156,7 @@ class BlinkSyncModule:
|
|||||||
try:
|
try:
|
||||||
return response["event"]
|
return response["event"]
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
_LOGGER.error("Could not extract events: %s", response, exc_info=True)
|
_LOGGER.error("Could not extract events: %s", response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_camera_info(self, camera_id, **kwargs):
|
def get_camera_info(self, camera_id, **kwargs):
|
||||||
@@ -170,7 +168,7 @@ class BlinkSyncModule:
|
|||||||
try:
|
try:
|
||||||
return response["camera"][0]
|
return response["camera"][0]
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
_LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
|
_LOGGER.error("Could not extract camera info: %s", response)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_network_info(self):
|
def get_network_info(self):
|
||||||
|
|||||||
@@ -30,6 +30,29 @@ By default, the ``blink.auth.Auth`` class creates its own websession via its ``c
|
|||||||
blink.auth = Auth()
|
blink.auth = Auth()
|
||||||
blink.auth.session = YourCustomSession
|
blink.auth.session = YourCustomSession
|
||||||
|
|
||||||
|
|
||||||
|
Custom Retry Logic
|
||||||
|
--------------------
|
||||||
|
The built-in auth session via the ``create_session`` method allows for customizable retry intervals and conditions. These parameters are:
|
||||||
|
|
||||||
|
- retries
|
||||||
|
- backoff
|
||||||
|
- retry_list
|
||||||
|
|
||||||
|
``retries`` is the total number of retry attempts that each http request can do before timing out. ``backoff`` is a parameter that allows for non-linear retry times such that the time between retries is backoff*(2^(retries) - 1). ``retry_list`` is simply a list of status codes to force a retry. By default ``retries=3``, ``backoff=1``, and ``retry_list=[429, 500, 502, 503, 504]``. To override them, you need to add you overrides to a dictionary and use that to create a new session with the ``opts`` variable in the ``create_session`` method. The following example can serve as a guide where only the number of retries and backoff factor are overridden:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from blinkpy.blinkpy import Blink
|
||||||
|
from blinkpy.auth import Auth
|
||||||
|
|
||||||
|
blink = Blink()
|
||||||
|
blink.auth = Auth()
|
||||||
|
|
||||||
|
opts = {"retries": 10, "backoff": 2}
|
||||||
|
blink.auth.session = blink.auth.create_session(opts=opts)
|
||||||
|
|
||||||
|
|
||||||
Custom HTTP requests
|
Custom HTTP requests
|
||||||
---------------------
|
---------------------
|
||||||
In addition to custom sessions, custom blink server requests can be performed. This give you the ability to bypass the built-in ``Auth.query`` method. It also allows flexibility by giving you the option to pass your own url, rather than be limited to what is currently implemented in the ``blinkpy.api`` module.
|
In addition to custom sessions, custom blink server requests can be performed. This give you the ability to bypass the built-in ``Auth.query`` method. It also allows flexibility by giving you the option to pass your own url, rather than be limited to what is currently implemented in the ``blinkpy.api`` module.
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
python-dateutil~=2.8.1
|
python-dateutil>=2.8.1
|
||||||
requests~=2.24.0
|
requests>=2.24.0
|
||||||
python-slugify~=4.0.0
|
python-slugify>=4.0.1
|
||||||
testtools==2.4.0
|
testtools>=2.4.0
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
black==19.10b0
|
black==19.10b0
|
||||||
coverage==5.1
|
coverage==5.2.1
|
||||||
flake8==3.8.3
|
flake8==3.8.3
|
||||||
flake8-docstrings==1.5.0
|
flake8-docstrings==1.5.0
|
||||||
pre-commit==2.5.1
|
pre-commit==2.6.0
|
||||||
pylint==2.5.3
|
pylint==2.5.3
|
||||||
pydocstyle==5.0.2
|
pydocstyle==5.0.2
|
||||||
pytest==5.4.3
|
pytest==6.0.0
|
||||||
pytest-cov==2.10.0
|
pytest-cov==2.10.0
|
||||||
pytest-sugar==0.9.3
|
pytest-sugar==0.9.4
|
||||||
pytest-timeout==1.4.1
|
pytest-timeout==1.4.2
|
||||||
restructuredtext-lint==1.3.1
|
restructuredtext-lint==1.3.1
|
||||||
pygments==2.6.1
|
pygments==2.6.1
|
||||||
|
|||||||
@@ -3,23 +3,26 @@
|
|||||||
from os.path import abspath, dirname
|
from os.path import abspath, dirname
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from blinkpy.helpers.constants import (
|
from blinkpy.helpers.constants import (
|
||||||
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
|
__version__,
|
||||||
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
|
PROJECT_PACKAGE_NAME,
|
||||||
|
PROJECT_LICENSE,
|
||||||
|
PROJECT_URL,
|
||||||
|
PROJECT_EMAIL,
|
||||||
|
PROJECT_DESCRIPTION,
|
||||||
|
PROJECT_CLASSIFIERS,
|
||||||
|
PROJECT_AUTHOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
PROJECT_VERSION = __version__
|
PROJECT_VERSION = __version__
|
||||||
|
|
||||||
THIS_DIR = abspath(dirname(__file__))
|
THIS_DIR = abspath(dirname(__file__))
|
||||||
|
|
||||||
REQUIRES = [
|
with open(f"{THIS_DIR}/requirements.txt") as req_file:
|
||||||
"python-dateutil~=2.8.1",
|
REQUIRES = [line.rstrip() for line in req_file]
|
||||||
"requests~=2.23.0",
|
|
||||||
"python-slugify~=4.0.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
PACKAGES = find_packages(exclude=['tests*', 'docs'])
|
PACKAGES = find_packages(exclude=["tests*", "docs"])
|
||||||
|
|
||||||
with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
|
with open("{}/README.rst".format(THIS_DIR), encoding="utf-8") as readme_file:
|
||||||
LONG_DESCRIPTION = readme_file.read()
|
LONG_DESCRIPTION = readme_file.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@@ -31,11 +34,11 @@ setup(
|
|||||||
author_email=PROJECT_EMAIL,
|
author_email=PROJECT_EMAIL,
|
||||||
license=PROJECT_LICENSE,
|
license=PROJECT_LICENSE,
|
||||||
url=PROJECT_URL,
|
url=PROJECT_URL,
|
||||||
platforms='any',
|
platforms="any",
|
||||||
py_modules=['blinkpy'],
|
py_modules=["blinkpy"],
|
||||||
packages=PACKAGES,
|
packages=PACKAGES,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=REQUIRES,
|
install_requires=REQUIRES,
|
||||||
test_suite='tests',
|
test_suite="tests",
|
||||||
classifiers=PROJECT_CLASSIFIERS
|
classifiers=PROJECT_CLASSIFIERS,
|
||||||
)
|
)
|
||||||
|
|||||||
+44
-26
@@ -128,8 +128,7 @@ class TestAuth(unittest.TestCase):
|
|||||||
def test_header(self):
|
def test_header(self):
|
||||||
"""Test header data."""
|
"""Test header data."""
|
||||||
self.auth.token = "bar"
|
self.auth.token = "bar"
|
||||||
self.auth.host = "foo"
|
expected_header = {"TOKEN_AUTH": "bar"}
|
||||||
expected_header = {"Host": "foo", "TOKEN_AUTH": "bar"}
|
|
||||||
self.assertDictEqual(self.auth.header, expected_header)
|
self.assertDictEqual(self.auth.header, expected_header)
|
||||||
|
|
||||||
def test_header_no_token(self):
|
def test_header_no_token(self):
|
||||||
@@ -144,21 +143,6 @@ class TestAuth(unittest.TestCase):
|
|||||||
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
||||||
mock_req.return_value = fake_resp
|
mock_req.return_value = fake_resp
|
||||||
self.assertEqual(self.auth.login(), {"foo": "bar"})
|
self.assertEqual(self.auth.login(), {"foo": "bar"})
|
||||||
mock_req.assert_called_with(
|
|
||||||
self.auth, const.LOGIN_ENDPOINT["v4"], {}, is_retry=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch("blinkpy.auth.Auth.validate_login", return_value=None)
|
|
||||||
@mock.patch("blinkpy.auth.api.request_login")
|
|
||||||
def test_login_v3(self, mock_req, mock_validate):
|
|
||||||
"""Test login handling."""
|
|
||||||
auth_v3 = Auth(login_method="v3")
|
|
||||||
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
|
||||||
mock_req.return_value = fake_resp
|
|
||||||
self.assertEqual(auth_v3.login(), {"foo": "bar"})
|
|
||||||
mock_req.assert_called_with(
|
|
||||||
auth_v3, const.LOGIN_ENDPOINT["v3"], {}, is_retry=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch("blinkpy.auth.Auth.validate_login", return_value=None)
|
@mock.patch("blinkpy.auth.Auth.validate_login", return_value=None)
|
||||||
@mock.patch("blinkpy.auth.api.request_login")
|
@mock.patch("blinkpy.auth.api.request_login")
|
||||||
@@ -246,7 +230,7 @@ class TestAuth(unittest.TestCase):
|
|||||||
@mock.patch("blinkpy.auth.Auth.refresh_token")
|
@mock.patch("blinkpy.auth.Auth.refresh_token")
|
||||||
def test_query_retry_failed(self, mock_refresh, mock_validate):
|
def test_query_retry_failed(self, mock_refresh, mock_validate):
|
||||||
"""Check handling of failed retry request."""
|
"""Check handling of failed retry request."""
|
||||||
self.auth.seession = MockSession()
|
self.auth.session = MockSession()
|
||||||
mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
|
mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
|
||||||
mock_refresh.return_value = True
|
mock_refresh.return_value = True
|
||||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||||
@@ -254,14 +238,48 @@ class TestAuth(unittest.TestCase):
|
|||||||
mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
|
mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
|
||||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||||
|
|
||||||
def test_login_methods(self):
|
def test_default_session(self):
|
||||||
"""Test correct login url returned."""
|
"""Test default session creation."""
|
||||||
auth = Auth()
|
sess = self.auth.create_session()
|
||||||
self.assertEqual(auth.login_url, const.LOGIN_ENDPOINT["v4"])
|
adapter = sess.adapters["https://"]
|
||||||
auth.login_method = "v3"
|
self.assertEqual(adapter.max_retries.total, 3)
|
||||||
self.assertEqual(auth.login_url, const.LOGIN_ENDPOINT["v3"])
|
self.assertEqual(adapter.max_retries.backoff_factor, 1)
|
||||||
auth.login_method = "foobar"
|
self.assertEqual(
|
||||||
self.assertEqual(auth.login_url, const.LOGIN_ENDPOINT["v4"])
|
adapter.max_retries.status_forcelist, [429, 500, 502, 503, 504]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_session_full(self):
|
||||||
|
"""Test full custom session creation."""
|
||||||
|
opts = {"backoff": 2, "retries": 10, "retry_list": [404]}
|
||||||
|
sess = self.auth.create_session(opts=opts)
|
||||||
|
adapter = sess.adapters["https://"]
|
||||||
|
self.assertEqual(adapter.max_retries.total, 10)
|
||||||
|
self.assertEqual(adapter.max_retries.backoff_factor, 2)
|
||||||
|
self.assertEqual(adapter.max_retries.status_forcelist, [404])
|
||||||
|
|
||||||
|
def test_custom_session_partial(self):
|
||||||
|
"""Test partial custom session creation."""
|
||||||
|
opts1 = {"backoff": 2}
|
||||||
|
opts2 = {"retries": 5}
|
||||||
|
opts3 = {"retry_list": [101, 202]}
|
||||||
|
sess1 = self.auth.create_session(opts=opts1)
|
||||||
|
sess2 = self.auth.create_session(opts=opts2)
|
||||||
|
sess3 = self.auth.create_session(opts=opts3)
|
||||||
|
adapt1 = sess1.adapters["https://"]
|
||||||
|
adapt2 = sess2.adapters["https://"]
|
||||||
|
adapt3 = sess3.adapters["https://"]
|
||||||
|
|
||||||
|
self.assertEqual(adapt1.max_retries.total, 3)
|
||||||
|
self.assertEqual(adapt1.max_retries.backoff_factor, 2)
|
||||||
|
self.assertEqual(adapt1.max_retries.status_forcelist, [429, 500, 502, 503, 504])
|
||||||
|
|
||||||
|
self.assertEqual(adapt2.max_retries.total, 5)
|
||||||
|
self.assertEqual(adapt2.max_retries.backoff_factor, 1)
|
||||||
|
self.assertEqual(adapt2.max_retries.status_forcelist, [429, 500, 502, 503, 504])
|
||||||
|
|
||||||
|
self.assertEqual(adapt3.max_retries.total, 3)
|
||||||
|
self.assertEqual(adapt3.max_retries.backoff_factor, 1)
|
||||||
|
self.assertEqual(adapt3.max_retries.status_forcelist, [101, 202])
|
||||||
|
|
||||||
|
|
||||||
class MockSession:
|
class MockSession:
|
||||||
|
|||||||
@@ -117,10 +117,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
||||||
"temperature."
|
"temperature."
|
||||||
),
|
),
|
||||||
(
|
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"),
|
||||||
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
|
|
||||||
"\nNoneType: None"
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user