Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v6
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Development setup

The library supports Python 3.9 to 3.14. Create a virtual environment and
The library supports Python 3.10 to 3.14. Create a virtual environment and
install the package with its development dependencies:

```bash
Expand Down
18 changes: 8 additions & 10 deletions apiclient/authentication_methods.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from __future__ import annotations
import http.cookiejar
from typing import TYPE_CHECKING, Dict, Optional, Union

from apiclient.utils.typing import BasicAuthType, OptionalStr
from typing import TYPE_CHECKING

if TYPE_CHECKING: # pragma: no cover
# Stupid way of getting around cyclic imports when
# using typehinting.
from apiclient import APIClient
from apiclient.utils.typing import BasicAuthType


class BaseAuthenticationMethod:
Expand All @@ -16,7 +14,7 @@ def get_headers(self) -> dict:
def get_query_params(self) -> dict:
return {}

def get_username_password_authentication(self) -> Optional[BasicAuthType]:
def get_username_password_authentication(self) -> BasicAuthType | None:
return None

def perform_initial_auth(self, client: "APIClient"):
Expand Down Expand Up @@ -51,15 +49,15 @@ def __init__(
self,
token: str,
parameter: str = "Authorization",
scheme: OptionalStr = "Bearer",
extra: Optional[Dict[str, str]] = None,
scheme: str | None = "Bearer",
extra: dict[str, str] | None = None,
):
self._token = token
self._parameter = parameter
self._scheme = scheme
self._extra = extra

def get_headers(self) -> Dict[str, str]:
def get_headers(self) -> dict[str, str]:
if self._scheme:
headers = {self._parameter: f"{self._scheme} {self._token}"}
else:
Expand All @@ -86,7 +84,7 @@ class CookieAuthentication(BaseAuthenticationMethod):
def __init__(
self,
auth_url: str,
authentication: Union[HeaderAuthentication, QueryParameterAuthentication, BasicAuthentication],
authentication: HeaderAuthentication | QueryParameterAuthentication | BasicAuthentication,
):
self._auth_url = auth_url
self._authentication = authentication
Expand Down
43 changes: 24 additions & 19 deletions apiclient/client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from __future__ import annotations
import logging
from copy import copy
from typing import Any, Optional, Type
from typing import TYPE_CHECKING

from apiclient.authentication_methods import BaseAuthenticationMethod, NoAuthentication
from apiclient.error_handlers import BaseErrorHandler, ErrorHandler
from apiclient.request_formatters import BaseRequestFormatter, NoOpRequestFormatter
from apiclient.request_strategies import BaseRequestStrategy, RequestStrategy
from apiclient.response_handlers import BaseResponseHandler, RequestsResponseHandler
from apiclient.utils.typing import JsonType, OptionalDict

if TYPE_CHECKING: # pragma: no cover
from typing import Any

from apiclient.utils.typing import JsonType

LOG = logging.getLogger(__name__)

Expand All @@ -18,11 +23,11 @@
class APIClient:
def __init__(
self,
authentication_method: Optional[BaseAuthenticationMethod] = None,
response_handler: Type[BaseResponseHandler] = RequestsResponseHandler,
request_formatter: Type[BaseRequestFormatter] = NoOpRequestFormatter,
error_handler: Type[BaseErrorHandler] = ErrorHandler,
request_strategy: Optional[BaseRequestStrategy] = None,
authentication_method: BaseAuthenticationMethod | None = None,
response_handler: type[BaseResponseHandler] = RequestsResponseHandler,
request_formatter: type[BaseRequestFormatter] = NoOpRequestFormatter,
error_handler: type[BaseErrorHandler] = ErrorHandler,
request_strategy: BaseRequestStrategy | None = None,
):
# Set default values
self._default_headers = {}
Expand Down Expand Up @@ -58,26 +63,26 @@ def set_authentication_method(self, authentication_method: BaseAuthenticationMet
def get_authentication_method(self) -> BaseAuthenticationMethod:
return self._authentication_method

def get_response_handler(self) -> Type[BaseResponseHandler]:
def get_response_handler(self) -> type[BaseResponseHandler]:
return self._response_handler

def set_response_handler(self, response_handler: Type[BaseResponseHandler]):
def set_response_handler(self, response_handler: type[BaseResponseHandler]):
if not (response_handler and issubclass(response_handler, BaseResponseHandler)):
raise RuntimeError("provided response_handler must be a subclass of BaseResponseHandler.")
self._response_handler = response_handler

def get_error_handler(self) -> Type[BaseErrorHandler]:
def get_error_handler(self) -> type[BaseErrorHandler]:
return self._error_handler

def set_error_handler(self, error_handler: Type[BaseErrorHandler]):
def set_error_handler(self, error_handler: type[BaseErrorHandler]):
if not (error_handler and issubclass(error_handler, BaseErrorHandler)):
raise RuntimeError("provided error_handler must be a subclass of BaseErrorHandler.")
self._error_handler = error_handler

def get_request_formatter(self) -> Type[BaseRequestFormatter]:
def get_request_formatter(self) -> type[BaseRequestFormatter]:
return self._request_formatter

def set_request_formatter(self, request_formatter: Type[BaseRequestFormatter]):
def set_request_formatter(self, request_formatter: type[BaseRequestFormatter]):
if not (request_formatter and issubclass(request_formatter, BaseRequestFormatter)):
raise RuntimeError("provided request_formatter must be a subclass of BaseRequestFormatter.")
self._request_formatter = request_formatter
Expand All @@ -100,7 +105,7 @@ def get_default_headers(self) -> dict:
def get_default_query_params(self) -> dict:
return self._authentication_method.get_query_params()

def get_default_username_password_authentication(self) -> Optional[tuple]:
def get_default_username_password_authentication(self) -> tuple | None:
return self._authentication_method.get_username_password_authentication()

def get_request_timeout(self) -> float:
Expand All @@ -111,27 +116,27 @@ def clone(self):
"""Enable Prototype pattern on client."""
return copy(self)

def post(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs):
def post(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs):
"""Send data and return response data from POST endpoint."""
LOG.debug("POST %s with %s", endpoint, data)
return self.get_request_strategy().post(endpoint, data=data, params=params, **kwargs)

def get(self, endpoint: str, params: OptionalDict = None, **kwargs):
def get(self, endpoint: str, params: dict | None = None, **kwargs):
"""Return response data from GET endpoint."""
LOG.debug("GET %s", endpoint)
return self.get_request_strategy().get(endpoint, params=params, **kwargs)

def put(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs):
def put(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs):
"""Send data to overwrite resource and return response data from PUT endpoint."""
LOG.debug("PUT %s with %s", endpoint, data)
return self.get_request_strategy().put(endpoint, data=data, params=params, **kwargs)

def patch(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs):
def patch(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs):
"""Send data to update resource and return response data from PATCH endpoint."""
LOG.debug("PATCH %s with %s", endpoint, data)
return self.get_request_strategy().patch(endpoint, data=data, params=params, **kwargs)

def delete(self, endpoint: str, params: OptionalDict = None, **kwargs):
def delete(self, endpoint: str, params: dict | None = None, **kwargs):
"""Remove resource with DELETE endpoint."""
LOG.debug("DELETE %s", endpoint)
return self.get_request_strategy().delete(endpoint, params=params, **kwargs)
7 changes: 6 additions & 1 deletion apiclient/error_handlers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING

from apiclient import exceptions
from apiclient.response import Response

if TYPE_CHECKING: # pragma: no cover
from apiclient.response import Response


class BaseErrorHandler:
Expand Down
7 changes: 2 additions & 5 deletions apiclient/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from apiclient.utils.typing import OptionalInt


class APIClientError(Exception):
"""General exception to denote that something went wrong when using the client.

Expand All @@ -19,10 +16,10 @@ class APIRequestError(APIClientError):
"""Exception to denote that something went wrong when making the request."""

message: str = ""
status_code: OptionalInt = None
status_code: int | None = None
info: str = ""

def __init__(self, message: str = "", status_code: OptionalInt = None, info: str = ""):
def __init__(self, message: str = "", status_code: int | None = None, info: str = ""):
self.message = self.message or message
self.status_code = self.status_code or status_code
self.info = self.info or info
Expand Down
18 changes: 10 additions & 8 deletions apiclient/paginators.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from __future__ import annotations
from contextlib import contextmanager
from functools import wraps
from typing import Callable
from typing import TYPE_CHECKING

from apiclient.client import APIClient
from apiclient.request_strategies import (
BaseRequestStrategy,
QueryParamPaginatedRequestStrategy,
UrlPaginatedRequestStrategy,
)
from apiclient.request_strategies import QueryParamPaginatedRequestStrategy, UrlPaginatedRequestStrategy

if TYPE_CHECKING: # pragma: no cover
from typing import Callable

from apiclient.client import APIClient
from apiclient.request_strategies import BaseRequestStrategy


@contextmanager
Expand All @@ -21,7 +23,7 @@ def set_strategy(client: APIClient, strategy: BaseRequestStrategy):
del temporary_client


def paginated(by_query_params: Callable = None, by_url: Callable = None):
def paginated(by_query_params: Callable | None = None, by_url: Callable | None = None):
"""Decorator to signal that the page is paginated."""
if by_query_params:
strategy = QueryParamPaginatedRequestStrategy(by_query_params)
Expand Down
11 changes: 7 additions & 4 deletions apiclient/request_formatters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations
import json
from typing import TYPE_CHECKING

from apiclient.utils.typing import OptionalJsonType, OptionalStr
if TYPE_CHECKING: # pragma: no cover
from apiclient.utils.typing import JsonType


class BaseRequestFormatter:
Expand All @@ -16,15 +19,15 @@ def get_headers(cls) -> dict:
return {}

@classmethod
def format(cls, data: OptionalJsonType):
def format(cls, data: JsonType | None):
raise NotImplementedError


class NoOpRequestFormatter(BaseRequestFormatter):
"""No action request formatter."""

@classmethod
def format(cls, data: OptionalJsonType) -> OptionalJsonType:
def format(cls, data: JsonType | None) -> JsonType | None:
return data


Expand All @@ -34,6 +37,6 @@ class JsonRequestFormatter(BaseRequestFormatter):
content_type = "application/json"

@classmethod
def format(cls, data: OptionalJsonType) -> OptionalStr:
def format(cls, data: JsonType | None) -> str | None:
if data:
return json.dumps(data)
Loading