Skip to content

Commit

Permalink
feat(client): add authentication parameter (#316)
Browse files Browse the repository at this point in the history
Fixes: #187
  • Loading branch information
afuetterer authored Apr 4, 2024
1 parent 517c7e0 commit 035c0fe
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 1 deletion.
27 changes: 27 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,30 @@ records = scythe.list_records(ignore_deleted=True)
!!! note
This works only using the [oaipmh_scythe.iterator.OAIItemIterator][]. If you use the
[oaipmh_scythe.iterator.OAIResponseIterator][], the resulting OAI responses will still contain the deleted records.

## Authentication

Certain OAI-PMH repositories may require authentication for accessing data or certain functionality. `oaipmh-scythe`
provides support for HTTP Basic authentication and other methods, which can be used to authenticate requests made by the
client.

The `auth` parameter of the Scythe client allows you to specify an authentication method when creating a new instance of
the client.

### HTTP Basic Authentication

To use HTTP basic authentication with the Scythe client, simply provide a tuple containing your username and password as
the value for the `auth` parameter when instantiating the Scythe object:

```python
auth = ("username", "password")
scythe = Scythe("https://example.org/oai2d", auth=auth)
```

!!! note
`oaipmh-scythe` uses [httpx](https://www.python-httpx.org) under the hood. The `auth` parameter accepts subclasses of
`httpx.Auth`, e.g. `httpx.BasicAuth`, `httpx.DigestAuth`, or `httpx.NetRCAuth`, see
[Authentication - HTTPX](https://www.python-httpx.org/advanced/authentication/) for further information

Once the authentication method is set, Scythe will use it to authenticate requests made by the client. This allows you
to access restricted data or functionality from an OAI-PMH repository without having to handle authentication manually.
9 changes: 8 additions & 1 deletion src/oaipmh_scythe/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from collections.abc import Iterable, Iterator
from types import TracebackType

from httpx._types import AuthTypes

logger = logging.getLogger(__name__)

USER_AGENT: str = f"oaipmh-scythe/{__version__}"
Expand Down Expand Up @@ -61,6 +63,7 @@ class Scythe:
default_retry_after: The default wait time (in seconds) between retries if no 'retry-after' header is present.
class_mapping: A mapping from OAI verbs to classes representing OAI items.
encoding: The character encoding for decoding responses. Defaults to the server's specified encoding.
auth: Optional authentication credentials for accessing the OAI-PMH interface.
timeout: The timeout (in seconds) for HTTP requests.
Examples:
Expand All @@ -81,6 +84,7 @@ def __init__(
default_retry_after: int = 60,
class_mapping: dict[str, type[OAIItem]] | None = None,
encoding: str | None = None,
auth: AuthTypes | None = None,
timeout: int = 60,
):
self.endpoint = endpoint
Expand All @@ -97,6 +101,7 @@ def __init__(
self.oai_namespace = OAI_NAMESPACE
self.class_mapping = class_mapping or DEFAULT_CLASS_MAP
self.encoding = encoding
self.auth = auth
self.timeout = timeout
self._client: httpx.Client | None = None

Expand All @@ -113,7 +118,9 @@ def client(self) -> httpx.Client:
"""
if self._client is None or self._client.is_closed:
headers = {"Accept": "text/xml; charset=utf-8", "user-agent": USER_AGENT}
self._client = httpx.Client(headers=headers, timeout=self.timeout, event_hooks={"response": [log_response]})
self._client = httpx.Client(
headers=headers, timeout=self.timeout, auth=self.auth, event_hooks={"response": [log_response]}
)
return self._client

def close(self) -> None:
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@


query = {"verb": "ListIdentifiers", "metadataPrefix": "oai_dc"}
auth = ("username", "password")


def test_invalid_http_method() -> None:
Expand Down Expand Up @@ -109,3 +110,20 @@ def test_retry_on_custom_code(scythe: Scythe, respx_mock: MockRouter, mocker) ->
scythe.harvest(query)
assert mock_route.call_count == 4
assert mock_sleep.call_count == 3


def test_no_auth_arguments():
with Scythe("https://zenodo.org/oai2d") as scythe:
assert scythe.client.auth is None


def test_auth_arguments():
with Scythe("https://zenodo.org/oai2d", auth=auth) as scythe:
assert scythe.client.auth


def test_auth_arguments_usage(respx_mock: MockRouter) -> None:
scythe = Scythe("https://zenodo.org/oai2d", auth=auth)
respx_mock.get("https://zenodo.org/oai2d").mock(return_value=httpx.Response(200))
oai_response = scythe.harvest(query)
assert oai_response.http_response.request.headers["authorization"]

0 comments on commit 035c0fe

Please sign in to comment.