diff --git a/README.md b/README.md index 7cebeea..e4342b5 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,19 @@ async with AppClient(app_id, privkey) as session: resp = await session.get("/octocat") ``` +### No Authentication + +Finally you can create a client without any authentication. This is mainly +provided for cases where supplying an authentication method is optional, e.g to +increase rate limits. This allows for simpler implementations. + +```python +from simple_github import PublicClient + +async with PublicClient() as session: + resp = await session.get("/octocat") +``` + ### Query the REST API simple-github provides only a very basic wrapper around Github's REST API. You can diff --git a/src/simple_github/__init__.py b/src/simple_github/__init__.py index d7e2512..c82f749 100644 --- a/src/simple_github/__init__.py +++ b/src/simple_github/__init__.py @@ -1,7 +1,7 @@ import asyncio from typing import List, Optional, Union -from .auth import AppAuth, AppInstallationAuth, TokenAuth +from .auth import AppAuth, AppInstallationAuth, PublicAuth, TokenAuth from .client import AsyncClient, Client, SyncClient @@ -60,3 +60,18 @@ def TokenClient(token: str) -> Client: auth = TokenAuth(token) return AsyncClient(auth=auth) if is_async else SyncClient(auth=auth) + + +def PublicClient() -> Client: + """Convenience function to create an unauthenticated `Client` instance. + + Returns: + Client: A client without any authentication.""" + try: + asyncio.get_running_loop() + is_async = True + except RuntimeError: + is_async = False + + auth = PublicAuth() + return AsyncClient(auth=auth) if is_async else SyncClient(auth=auth) diff --git a/src/simple_github/auth.py b/src/simple_github/auth.py index 2336b15..4a8f8da 100644 --- a/src/simple_github/auth.py +++ b/src/simple_github/auth.py @@ -23,6 +23,13 @@ async def close(self) -> None: pass +class PublicAuth(Auth): + """Shim for unauthenticated API access.""" + + async def get_token(self) -> str: + return "" + + class TokenAuth(Auth): def __init__(self, token: str): """Authentication for an access token. diff --git a/src/simple_github/client.py b/src/simple_github/client.py index 582ef0c..692698b 100644 --- a/src/simple_github/client.py +++ b/src/simple_github/client.py @@ -104,8 +104,10 @@ def _get_gql_session(self) -> SyncClientSession: headers = { "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", } + if token: + headers["Authorization"] = f"Bearer {token}" + transport = RequestsHTTPTransport(url=GITHUB_GRAPHQL_ENDPOINT, headers=headers) self._gql_client = GqlClient( transport=transport, fetch_schema_from_transport=False @@ -242,8 +244,10 @@ async def _get_gql_session(self) -> ReconnectingAsyncClientSession: headers = { "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", } + if token: + headers["Authorization"] = f"Bearer {token}" + transport = AIOHTTPTransport(url=GITHUB_GRAPHQL_ENDPOINT, headers=headers) self._gql_client = GqlClient( transport=transport, fetch_schema_from_transport=False diff --git a/test/test_auth.py b/test/test_auth.py index 50ea85a..044217c 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -4,10 +4,16 @@ import jwt import pytest -from simple_github.auth import AppAuth, AppInstallationAuth, TokenAuth +from simple_github.auth import AppAuth, AppInstallationAuth, PublicAuth, TokenAuth from simple_github.client import GITHUB_API_ENDPOINT +@pytest.mark.asyncio +async def test_public_auth_get_token(): + auth = PublicAuth() + assert await auth.get_token() == "" + + @pytest.mark.asyncio async def test_token_auth_get_token(): token = "123" diff --git a/test/test_client.py b/test/test_client.py index 0a18cd9..6d1672c 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -58,6 +58,16 @@ async def test_async_client_get_session(async_client): } +@pytest.mark.asyncio +async def test_async_client_get_session_no_token(async_client): + client = async_client + client.auth._token = "" + session = await client._get_aiohttp_session() + assert dict(session._default_headers) == { + "Accept": "application/vnd.github+json", + } + + def test_sync_client_get_session(sync_client): client = sync_client assert client._gql_client is None @@ -87,6 +97,15 @@ def test_sync_client_get_session(sync_client): } +def test_sync_client_get_session_no_token(sync_client): + client = sync_client + client.auth._token = "" + client._get_requests_session() + assert dict(client._gql_session.transport.headers) == { + "Accept": "application/vnd.github+json", + } + + @pytest.mark.asyncio async def test_async_client_rest(aioresponses, async_client): client = async_client diff --git a/test/test_simple_github.py b/test/test_simple_github.py index 95268fb..4456c78 100644 --- a/test/test_simple_github.py +++ b/test/test_simple_github.py @@ -1,9 +1,21 @@ import pytest -from simple_github import AppClient, TokenClient +from simple_github import AppClient, PublicClient, TokenClient from simple_github.client import GITHUB_API_ENDPOINT +@pytest.mark.asyncio +async def test_public_client(aioresponses): + aioresponses.get( + f"{GITHUB_API_ENDPOINT}/octocat", status=200, payload={"foo": "bar"} + ) + + async with PublicClient() as client: + resp = await client.get("/octocat") + result = await resp.json() + assert result == {"foo": "bar"} + + @pytest.mark.asyncio async def test_token_client(aioresponses): aioresponses.get( diff --git a/uv.lock b/uv.lock index dcaa474..34caa2c 100644 --- a/uv.lock +++ b/uv.lock @@ -1382,7 +1382,7 @@ wheels = [ [[package]] name = "simple-github" -version = "1.0.0" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "aiohttp", extra = ["speedups"] },