Repository: Kinto/kinto-http.py

Issue:
Allow to pass `Basic <value>` as string in `auth` parameter
Current we currently support a tuple (`user`, `pass`), or `user:pass` or a `Bearer <value>` string.

https://github.com/Kinto/kinto-http.py/blob/409c2e6db280f8cefc0accbe514bcf025f6599e2/src/kinto_http/session.py#L39-L42

Discussion:
moses946: Thank you for sharing the details. Will work on them and send a new pr.

On Tue, 18 Feb 2025 at 12:53, Mathieu Leplatre ***@***.***>
wrote:

> ***@***.**** requested changes on this pull request.
>
> Thank you for taking the time to contribute!
>
> I gave a few points to improve before we can merge, but they are details
> ------------------------------
>
> In tests/test_session.py
> <https://github.com/Kinto/kinto-http.py/pull/384#discussion_r1959391395>:
>
> > @@ -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;")
>
> I know that this part very related, but we try to maintain a test per
> scope. Could you please split this test into a dedicated one?
> ------------------------------
>
> In src/kinto_http/__init__.py
> <https://github.com/Kinto/kinto-http.py/pull/384#discussion_r1959393378>:
>
> > @@ -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
>
> I think we need to rename this class into TokenAuth for better
> readability.
>
> And we can add:
>
> class BearerTokenAuth(TokenAuth):
>     pass
>
> for retrocompatibility
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/Kinto/kinto-http.py/pull/384#pullrequestreview-2622969846>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AQGHB4FLHPYXQRAPANX2D532QL7LBAVCNFSM6AAAAABXGJIMTKVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDMMRSHE3DSOBUGY>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

leplatrem: > send a new pr.

you should be able to push new commits to this one!
moses946: Worked on said improvements, awaiting merge approval.

Patch:
diff --git a/src/kinto_http/__init__.py b/src/kinto_http/__init__.py
index 87d1c60..9a516ee 100644
--- a/src/kinto_http/__init__.py
+++ b/src/kinto_http/__init__.py
@@ -18,6 +18,7 @@ logger = logging.getLogger("kinto_http")
 
 __all__ = (
     "BrowserOAuth",
+    "TokenAuth",
     "BearerTokenAuth",
     "Endpoints",
     "Session",
@@ -31,11 +32,16 @@ __all__ = (
 )
 
 
-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
         r.headers["Authorization"] = "{} {}".format(self.type, self.token)
         return r
+
+
+class BearerTokenAuth(TokenAuth):
+    pass
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/src/kinto_http/session.py b/src/kinto_http/session.py
index 3726f6c..393266a 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.TokenAuth(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:


Test patch:
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"<KintoAsyncClient {SERVER_URL}/" "buckets/homebrewing/collections/recipes>"
+    expected_repr = f"<KintoAsyncClient {SERVER_URL}/buckets/homebrewing/collections/recipes>"
     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"<KintoAsyncClient {SERVER_URL}/" "buckets/homebrewing>"
+    expected_repr = f"<KintoAsyncClient {SERVER_URL}/buckets/homebrewing>"
     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"<KintoClient {SERVER_URL}/" "buckets/homebrewing/collections/recipes>"
+    expected_repr = f"<KintoClient {SERVER_URL}/buckets/homebrewing/collections/recipes>"
     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"<KintoClient {SERVER_URL}/" "buckets/homebrewing>"
+    expected_repr = f"<KintoClient {SERVER_URL}/buckets/homebrewing>"
     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.
diff --git a/tests/test_session.py b/tests/test_session.py
index 8827bda..5b29752 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -246,6 +246,13 @@ 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="Basic asdfghjkl;")
+    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.BearerTokenAuth)
     assert session.auth.type == "Bearer+OIDC"