From a9d3d5568b76c9ba8bef2abcf433c1078708c419 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 20 Jul 2020 01:19:40 +0000 Subject: [PATCH 1/3] Adding retry options to auth class --- blinkpy/auth.py | 17 +++++++++++++---- tests/test_auth.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/blinkpy/auth.py b/blinkpy/auth.py index 1e20fd02..5cbf0f93 100644 --- a/blinkpy/auth.py +++ b/blinkpy/auth.py @@ -14,7 +14,7 @@ class Auth: """Class to handle login communication.""" - def __init__(self, login_data=None, no_prompt=False): + def __init__(self, login_data=None, no_prompt=False, retry_opts=None): """ Initialize auth handler. @@ -24,6 +24,10 @@ def __init__(self, login_data=None, no_prompt=False): - password :param no_prompt: Should any user input prompts be supressed? True/FALSE + :param retry_opts: Dictionary containing retry options: + - backoff: backoff factor for http request (backoff*(2^(total_retries)-1) + - retries: total retries to attempt + - retry_list: list of status codes to force retry """ if login_data is None: login_data = {} @@ -36,7 +40,7 @@ def __init__(self, login_data=None, no_prompt=False): self.login_response = None self.is_errored = False self.no_prompt = no_prompt - self.session = self.create_session() + self.session = self.create_session(opts=retry_opts) @property def login_attributes(self): @@ -55,15 +59,20 @@ def header(self): return None return {"TOKEN_AUTH": self.token} - def create_session(self): + def create_session(self, opts=None): """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() assert_status_hook = [ lambda response, *args, **kwargs: response.raise_for_status() ] sess.hooks["response"] = assert_status_hook retry = Retry( - total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504] + total=retries, backoff_factor=backoff, status_forcelist=retry_list ) adapter = HTTPAdapter(max_retries=retry) sess.mount("https://", adapter) diff --git a/tests/test_auth.py b/tests/test_auth.py index a755300b..0d053a85 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -238,6 +238,16 @@ def test_query_retry_failed(self, mock_refresh, mock_validate): mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed] self.assertEqual(self.auth.query(url="http://example.com"), None) + def test_default_session(self): + """Test default session creation.""" + sess = self.auth.create_session() + adapter = sess.adapters["https://"] + self.assertEqual(adapter.max_retries.total, 3) + self.assertEqual(adapter.max_retries.backoff_factor, 1) + self.assertEqual( + adapter.max_retries.status_forcelist, [429, 500, 502, 503, 504] + ) + class MockSession: """Object to mock a session.""" From 5fc315201a4d353791d1fdbeb1eb31552b3aff0d Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 20 Jul 2020 01:33:11 +0000 Subject: [PATCH 2/3] Add tests for retry options --- tests/test_auth.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_auth.py b/tests/test_auth.py index 0d053a85..1c85a6d7 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -248,6 +248,39 @@ def test_default_session(self): 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: """Object to mock a session.""" From fe8eac3dc6fcff2cb28116c6cd9d3a9cb327a56b Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 20 Jul 2020 01:42:07 +0000 Subject: [PATCH 3/3] Update documentation --- blinkpy/auth.py | 8 ++------ docs/advanced.rst | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/blinkpy/auth.py b/blinkpy/auth.py index 5cbf0f93..59950da4 100644 --- a/blinkpy/auth.py +++ b/blinkpy/auth.py @@ -14,7 +14,7 @@ class Auth: """Class to handle login communication.""" - def __init__(self, login_data=None, no_prompt=False, retry_opts=None): + def __init__(self, login_data=None, no_prompt=False): """ Initialize auth handler. @@ -24,10 +24,6 @@ def __init__(self, login_data=None, no_prompt=False, retry_opts=None): - password :param no_prompt: Should any user input prompts be supressed? True/FALSE - :param retry_opts: Dictionary containing retry options: - - backoff: backoff factor for http request (backoff*(2^(total_retries)-1) - - retries: total retries to attempt - - retry_list: list of status codes to force retry """ if login_data is None: login_data = {} @@ -40,7 +36,7 @@ def __init__(self, login_data=None, no_prompt=False, retry_opts=None): self.login_response = None self.is_errored = False self.no_prompt = no_prompt - self.session = self.create_session(opts=retry_opts) + self.session = self.create_session() @property def login_attributes(self): diff --git a/docs/advanced.rst b/docs/advanced.rst index 24fd4319..95bf6037 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -30,6 +30,29 @@ By default, the ``blink.auth.Auth`` class creates its own websession via its ``c blink.auth = Auth() 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 --------------------- 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.