From fab3983adac9ad28c44a042f6e41f160d73d3be7 Mon Sep 17 00:00:00 2001 From: Moses Mwai Date: Sat, 15 Feb 2025 13:59:51 +0300 Subject: [PATCH 1/5] Allow basic auth-scheme --- src/kinto_http/__init__.py | 1 + src/kinto_http/session.py | 5 ++++- tests/test_session.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/kinto_http/__init__.py b/src/kinto_http/__init__.py index 87d1c60..1680cca 100644 --- a/src/kinto_http/__init__.py +++ b/src/kinto_http/__init__.py @@ -37,5 +37,6 @@ def __init__(self, token, type=None): self.type = type or "Bearer" def __call__(self, r): + # Sets auth-scheme to either Bearer or Basic r.headers["Authorization"] = "{} {}".format(self.type, self.token) return r diff --git a/src/kinto_http/session.py b/src/kinto_http/session.py index 3726f6c..863f6f1 100644 --- a/src/kinto_http/session.py +++ b/src/kinto_http/session.py @@ -45,10 +45,13 @@ def create_session(server_url=None, auth=None, session=None, **kwargs): # eg, "Bearer ghruhgrwyhg" _type, token = auth.split(" ", 1) auth = kinto_http.BearerTokenAuth(token, type=_type) + elif "basic" in auth.lower(): + _type, token = auth.split(" ", 1) + auth = kinto_http.BearerTokenAuth(token, type=_type) elif auth: # not empty raise ValueError( "Unsupported `auth` parameter value. Must be a tuple() or string " - "in the form of `user:pass` or `Bearer xyz`" + "in the form of `user:pass` or `Bearer xyz` or `Basic xyz`" ) if session is None: diff --git a/tests/test_session.py b/tests/test_session.py index 8827bda..4474df9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -250,6 +250,12 @@ def test_auth_can_be_passed_as_basic_header(session_setup: Tuple[MagicMock, Sess assert isinstance(session.auth, kinto_http.BearerTokenAuth) assert session.auth.type == "Bearer+OIDC" assert session.auth.token == "abcdef" + session = create_session(auth="Basic asdfghjkl;") + assert isinstance(session.auth, kinto_http.BearerTokenAuth) + assert session.auth.type == "Basic" + assert session.auth.token == "asdfghjkl;" + + def test_auth_cannot_be_an_empty_string(session_setup: Tuple[MagicMock, Session]): From 203caf8a511a64f5bbf1b340644947d055af6f52 Mon Sep 17 00:00:00 2001 From: Moses Mwai Date: Wed, 19 Feb 2025 18:59:25 +0300 Subject: [PATCH 2/5] refactor: improve code readability and maintainability --- src/kinto_http/__init__.py | 8 ++++++-- src/kinto_http/session.py | 4 ++-- tests/test_session.py | 13 +++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/kinto_http/__init__.py b/src/kinto_http/__init__.py index 1680cca..d1a47f7 100644 --- a/src/kinto_http/__init__.py +++ b/src/kinto_http/__init__.py @@ -18,6 +18,7 @@ __all__ = ( "BrowserOAuth", + "TokenAuth" "BearerTokenAuth", "Endpoints", "Session", @@ -31,12 +32,15 @@ ) -class BearerTokenAuth(requests.auth.AuthBase): +class TokenAuth(requests.auth.AuthBase): def __init__(self, token, type=None): self.token = token self.type = type or "Bearer" def __call__(self, r): - # Sets auth-scheme to either Bearer or Basic + # Sets auth-scheme to either Bearer or Basic r.headers["Authorization"] = "{} {}".format(self.type, self.token) return r + +class BearerTokenAuth(TokenAuth): + pass \ No newline at end of file diff --git a/src/kinto_http/session.py b/src/kinto_http/session.py index 863f6f1..ab815e2 100644 --- a/src/kinto_http/session.py +++ b/src/kinto_http/session.py @@ -44,10 +44,10 @@ def create_session(server_url=None, auth=None, session=None, **kwargs): elif "bearer" in auth.lower(): # eg, "Bearer ghruhgrwyhg" _type, token = auth.split(" ", 1) - auth = kinto_http.BearerTokenAuth(token, type=_type) + auth = kinto_http.TokenAuth(token, type=_type) elif "basic" in auth.lower(): _type, token = auth.split(" ", 1) - auth = kinto_http.BearerTokenAuth(token, type=_type) + auth = kinto_http.TokenAuth(token, type=_type) elif auth: # not empty raise ValueError( "Unsupported `auth` parameter value. Must be a tuple() or string " diff --git a/tests/test_session.py b/tests/test_session.py index 4474df9..b4bd99a 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -246,16 +246,17 @@ def test_auth_can_be_passed_as_colon_separate(session_setup: Tuple[MagicMock, Se def test_auth_can_be_passed_as_basic_header(session_setup: Tuple[MagicMock, Session]): - session = create_session(auth="Bearer+OIDC abcdef") - assert isinstance(session.auth, kinto_http.BearerTokenAuth) - assert session.auth.type == "Bearer+OIDC" - assert session.auth.token == "abcdef" session = create_session(auth="Basic asdfghjkl;") - assert isinstance(session.auth, kinto_http.BearerTokenAuth) + assert isinstance(session.auth, kinto_http.TokenAuth) assert session.auth.type == "Basic" assert session.auth.token == "asdfghjkl;" + - +def test_auth_can_be_passed_as_bearer(session_setup: Tuple[MagicMock, Session]): + session = create_session(auth="Bearer+OIDC abcdef") + assert isinstance(session.auth, kinto_http.TokenAuth) + assert session.auth.type == "Bearer+OIDC" + assert session.auth.token == "abcdef" def test_auth_cannot_be_an_empty_string(session_setup: Tuple[MagicMock, Session]): From d17d84785d98b8fb09763953d2d0ed01330f1f27 Mon Sep 17 00:00:00 2001 From: Moses Mwai Date: Sat, 1 Mar 2025 13:28:33 +0300 Subject: [PATCH 3/5] Revert to BearerTokenAuth class for bearer token --- src/kinto_http/session.py | 2 +- tests/test_session.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kinto_http/session.py b/src/kinto_http/session.py index ab815e2..393266a 100644 --- a/src/kinto_http/session.py +++ b/src/kinto_http/session.py @@ -44,7 +44,7 @@ def create_session(server_url=None, auth=None, session=None, **kwargs): elif "bearer" in auth.lower(): # eg, "Bearer ghruhgrwyhg" _type, token = auth.split(" ", 1) - auth = kinto_http.TokenAuth(token, type=_type) + auth = kinto_http.BearerTokenAuth(token, type=_type) elif "basic" in auth.lower(): _type, token = auth.split(" ", 1) auth = kinto_http.TokenAuth(token, type=_type) diff --git a/tests/test_session.py b/tests/test_session.py index b4bd99a..b0e09df 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -254,7 +254,7 @@ def test_auth_can_be_passed_as_basic_header(session_setup: Tuple[MagicMock, Sess def test_auth_can_be_passed_as_bearer(session_setup: Tuple[MagicMock, Session]): session = create_session(auth="Bearer+OIDC abcdef") - assert isinstance(session.auth, kinto_http.TokenAuth) + assert isinstance(session.auth, kinto_http.BearerTokenAuth) assert session.auth.type == "Bearer+OIDC" assert session.auth.token == "abcdef" From 76012d3c59e7cb5450e5e177ad04c5bb1c53113d Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 4 Mar 2025 12:18:06 +0100 Subject: [PATCH 4/5] 'make format' --- src/kinto_http/__init__.py | 7 ++++--- tests/test_session.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/kinto_http/__init__.py b/src/kinto_http/__init__.py index d1a47f7..9a516ee 100644 --- a/src/kinto_http/__init__.py +++ b/src/kinto_http/__init__.py @@ -18,7 +18,7 @@ __all__ = ( "BrowserOAuth", - "TokenAuth" + "TokenAuth", "BearerTokenAuth", "Endpoints", "Session", @@ -38,9 +38,10 @@ def __init__(self, token, type=None): self.type = type or "Bearer" def __call__(self, r): - # Sets auth-scheme to either Bearer or Basic + # Sets auth-scheme to either Bearer or Basic r.headers["Authorization"] = "{} {}".format(self.type, self.token) return r + class BearerTokenAuth(TokenAuth): - pass \ No newline at end of file + pass diff --git a/tests/test_session.py b/tests/test_session.py index b0e09df..5b29752 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -250,7 +250,7 @@ def test_auth_can_be_passed_as_basic_header(session_setup: Tuple[MagicMock, Sess assert isinstance(session.auth, kinto_http.TokenAuth) assert session.auth.type == "Basic" assert session.auth.token == "asdfghjkl;" - + def test_auth_can_be_passed_as_bearer(session_setup: Tuple[MagicMock, Session]): session = create_session(auth="Bearer+OIDC abcdef") From b454b71f2989bf76cb5a96397e06f8caf167cbad Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 4 Mar 2025 12:25:55 +0100 Subject: [PATCH 5/5] 'make format' with latest ruff --- src/kinto_http/cli_utils.py | 5 ++--- src/kinto_http/replication.py | 7 +++---- tests/support.py | 6 +++--- tests/test_async_client.py | 16 ++++++++-------- tests/test_client.py | 16 ++++++++-------- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/kinto_http/cli_utils.py b/src/kinto_http/cli_utils.py index aabd42e..480b331 100644 --- a/src/kinto_http/cli_utils.py +++ b/src/kinto_http/cli_utils.py @@ -75,7 +75,7 @@ def add_parser_options( parser.add_argument( "-a", "--auth", - help="BasicAuth credentials: `token:my-secret` or " "Authorization header: `Bearer token`", + help="BasicAuth credentials: `token:my-secret` or Authorization header: `Bearer token`", type=str, default=default_auth, action=AuthAction, @@ -97,8 +97,7 @@ def add_parser_options( parser.add_argument( "--retry-after", - help="Delay in seconds between retries when requests fail. " - "(default: provided by server)", + help="Delay in seconds between retries when requests fail. (default: provided by server)", type=int, default=default_retry_after, ) diff --git a/src/kinto_http/replication.py b/src/kinto_http/replication.py index 3d83339..b707baf 100644 --- a/src/kinto_http/replication.py +++ b/src/kinto_http/replication.py @@ -42,8 +42,7 @@ def get_arguments(): # pragma: nocover parser.add_argument( "--origin-auth", - help="The origin authentication credentials. " - "Will use the same as the remote if omitted", + help="The origin authentication credentials. Will use the same as the remote if omitted", action=cli_utils.AuthAction, default=None, ) @@ -51,14 +50,14 @@ def get_arguments(): # pragma: nocover parser.add_argument( "--origin-bucket", dest="origin_bucket", - help="The name of the origin bucket. " "Will use the same as the remote if omitted", + help="The name of the origin bucket. Will use the same as the remote if omitted", default=None, ) parser.add_argument( "--origin-collection", dest="origin_collection", - help="The name of the origin collection. " "Will use the same as the remote if omitted", + help="The name of the origin collection. Will use the same as the remote if omitted", default=None, ) cli_utils.set_parser_server_options(parser) diff --git a/tests/support.py b/tests/support.py index 2054a0d..15adc8a 100644 --- a/tests/support.py +++ b/tests/support.py @@ -51,9 +51,9 @@ def get_user_id(server_url: str, credentials: Tuple[str, str]) -> str: def assert_option_strings(parser, *option_strings_list): for option_strings in option_strings_list: - assert any( - [action.option_strings == option_strings for action in parser._actions] - ), f"{option_strings} not found" + assert any([action.option_strings == option_strings for action in parser._actions]), ( + f"{option_strings} not found" + ) def build_response(data, headers=None): diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 01ce9b2..9ad4398 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -43,13 +43,13 @@ def test_client_is_represented_properly_with_bucket_and_collection(async_client_ client = async_client_setup.clone( server_url=SERVER_URL, bucket="homebrewing", collection="recipes" ) - expected_repr = f"" + expected_repr = f"" assert str(client) == expected_repr def test_client_is_represented_properly_with_bucket(async_client_setup: Client): client = async_client_setup.clone(server_url=SERVER_URL, bucket="homebrewing") - expected_repr = f"" + expected_repr = f"" assert str(client) == expected_repr @@ -843,7 +843,7 @@ async def test_records_timestamp_is_cached_per_collection(record_async_setup: Cl async def test_pagination_is_followed(record_async_setup: Client): client = record_async_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ # First one returns a list of items with a pagination token. @@ -871,7 +871,7 @@ async def test_pagination_is_followed(record_async_setup: Client): async def test_pagination_is_followed_generator(record_async_setup: Client): client = record_async_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" response = [ # First one returns a list of items with a pagination token. @@ -897,7 +897,7 @@ async def test_pagination_is_followed_generator(record_async_setup: Client): async def test_pagination_is_followed_for_number_of_pages(record_async_setup: Client): client = record_async_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ # First one returns a list of items with a pagination token. @@ -923,7 +923,7 @@ async def test_pagination_is_followed_for_number_of_pages(record_async_setup: Cl async def test_pagination_is_not_followed_if_limit_is_specified(record_async_setup: Client): client = record_async_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ build_response( @@ -938,7 +938,7 @@ async def test_pagination_is_not_followed_if_limit_is_specified(record_async_set async def test_pagination_supports_if_none_match(record_async_setup: Client): client = record_async_setup - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ # First one returns a list of items with a pagination token. @@ -964,7 +964,7 @@ async def test_pagination_supports_if_none_match(record_async_setup: Client): async def test_pagination_generator_if_none_match(record_async_setup: Client): client = record_async_setup - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" response = [ # First one returns a list of items with a pagination token. diff --git a/tests/test_client.py b/tests/test_client.py index 4678957..2c724c3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -209,13 +209,13 @@ def test_batch_options_are_transmitted(client_setup: Client, mocker: MockerFixtu def test_client_is_represented_properly_with_bucket_and_collection(client_setup: Client): client = client_setup.clone(server_url=SERVER_URL, bucket="homebrewing", collection="recipes") - expected_repr = f"" + expected_repr = f"" assert str(client) == expected_repr def test_client_is_represented_properly_with_bucket(client_setup: Client): client = client_setup.clone(server_url=SERVER_URL, bucket="homebrewing") - expected_repr = f"" + expected_repr = f"" assert str(client) == expected_repr @@ -1003,7 +1003,7 @@ def test_records_timestamp_is_cached_per_collection(record_setup: Client): def test_pagination_is_followed(record_setup: Client): client = record_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ # First one returns a list of items with a pagination token. @@ -1031,7 +1031,7 @@ def test_pagination_is_followed(record_setup: Client): def test_pagination_is_followed_generator(record_setup: Client): client = record_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" response = [ # First one returns a list of items with a pagination token. @@ -1057,7 +1057,7 @@ def test_pagination_is_followed_generator(record_setup: Client): def test_pagination_is_followed_for_number_of_pages(record_setup: Client): client = record_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ # First one returns a list of items with a pagination token. @@ -1083,7 +1083,7 @@ def test_pagination_is_followed_for_number_of_pages(record_setup: Client): def test_pagination_is_not_followed_if_limit_is_specified(record_setup: Client): client = record_setup # Mock the calls to request. - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ build_response( @@ -1098,7 +1098,7 @@ def test_pagination_is_not_followed_if_limit_is_specified(record_setup: Client): def test_pagination_supports_if_none_match(record_setup: Client): client = record_setup - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" client.session.request.side_effect = [ # First one returns a list of items with a pagination token. @@ -1124,7 +1124,7 @@ def test_pagination_supports_if_none_match(record_setup: Client): def test_pagination_generator_if_none_match(record_setup: Client): client = record_setup - link = "http://example.org/buckets/buck/collections/coll/records/" "?token=1234" + link = "http://example.org/buckets/buck/collections/coll/records/?token=1234" response = [ # First one returns a list of items with a pagination token.