statuspro-openapi-client 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. statuspro_openapi_client-0.1.0.dist-info/METADATA +337 -0
  2. statuspro_openapi_client-0.1.0.dist-info/RECORD +74 -0
  3. statuspro_openapi_client-0.1.0.dist-info/WHEEL +4 -0
  4. statuspro_openapi_client-0.1.0.dist-info/entry_points.txt +3 -0
  5. statuspro_openapi_client-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. statuspro_public_api_client/__init__.py +36 -0
  7. statuspro_public_api_client/_logging.py +33 -0
  8. statuspro_public_api_client/api/__init__.py +1 -0
  9. statuspro_public_api_client/api/orders/__init__.py +1 -0
  10. statuspro_public_api_client/api/orders/add_order_comment.py +215 -0
  11. statuspro_public_api_client/api/orders/bulk_update_order_status.py +194 -0
  12. statuspro_public_api_client/api/orders/get_order.py +188 -0
  13. statuspro_public_api_client/api/orders/get_viable_statuses.py +193 -0
  14. statuspro_public_api_client/api/orders/list_orders.py +366 -0
  15. statuspro_public_api_client/api/orders/lookup_order.py +208 -0
  16. statuspro_public_api_client/api/orders/set_order_due_date.py +215 -0
  17. statuspro_public_api_client/api/orders/update_order_status.py +215 -0
  18. statuspro_public_api_client/api/statuses/__init__.py +1 -0
  19. statuspro_public_api_client/api/statuses/get_statuses.py +161 -0
  20. statuspro_public_api_client/api_wrapper/__init__.py +15 -0
  21. statuspro_public_api_client/api_wrapper/_namespace.py +40 -0
  22. statuspro_public_api_client/api_wrapper/_registry.py +43 -0
  23. statuspro_public_api_client/api_wrapper/_resource.py +116 -0
  24. statuspro_public_api_client/client.py +267 -0
  25. statuspro_public_api_client/client_types.py +54 -0
  26. statuspro_public_api_client/domain/__init__.py +33 -0
  27. statuspro_public_api_client/domain/base.py +117 -0
  28. statuspro_public_api_client/domain/converters.py +71 -0
  29. statuspro_public_api_client/domain/order.py +87 -0
  30. statuspro_public_api_client/domain/status.py +30 -0
  31. statuspro_public_api_client/errors.py +16 -0
  32. statuspro_public_api_client/helpers/__init__.py +21 -0
  33. statuspro_public_api_client/helpers/base.py +26 -0
  34. statuspro_public_api_client/helpers/orders.py +78 -0
  35. statuspro_public_api_client/helpers/statuses.py +37 -0
  36. statuspro_public_api_client/log_setup.py +99 -0
  37. statuspro_public_api_client/models/__init__.py +53 -0
  38. statuspro_public_api_client/models/add_order_comment_request.py +68 -0
  39. statuspro_public_api_client/models/bulk_status_update_request.py +124 -0
  40. statuspro_public_api_client/models/bulk_status_update_response.py +72 -0
  41. statuspro_public_api_client/models/customer.py +74 -0
  42. statuspro_public_api_client/models/error_response.py +58 -0
  43. statuspro_public_api_client/models/history_item.py +171 -0
  44. statuspro_public_api_client/models/list_orders_financial_status_item.py +15 -0
  45. statuspro_public_api_client/models/list_orders_fulfillment_status_item.py +11 -0
  46. statuspro_public_api_client/models/locale_translation.py +66 -0
  47. statuspro_public_api_client/models/mail_log.py +82 -0
  48. statuspro_public_api_client/models/message_response.py +58 -0
  49. statuspro_public_api_client/models/order_list_item.py +180 -0
  50. statuspro_public_api_client/models/order_list_meta.py +120 -0
  51. statuspro_public_api_client/models/order_list_response.py +81 -0
  52. statuspro_public_api_client/models/order_response.py +220 -0
  53. statuspro_public_api_client/models/progress_timeline_item.py +93 -0
  54. statuspro_public_api_client/models/set_due_date_request.py +95 -0
  55. statuspro_public_api_client/models/status.py +190 -0
  56. statuspro_public_api_client/models/status_definition.py +82 -0
  57. statuspro_public_api_client/models/status_translations.py +62 -0
  58. statuspro_public_api_client/models/update_order_status_request.py +92 -0
  59. statuspro_public_api_client/models/validation_error_response.py +81 -0
  60. statuspro_public_api_client/models/validation_error_response_errors.py +54 -0
  61. statuspro_public_api_client/models/viable_status.py +82 -0
  62. statuspro_public_api_client/models_pydantic/__init__.py +122 -0
  63. statuspro_public_api_client/models_pydantic/_auto_registry.py +115 -0
  64. statuspro_public_api_client/models_pydantic/_base.py +349 -0
  65. statuspro_public_api_client/models_pydantic/_generated/__init__.py +53 -0
  66. statuspro_public_api_client/models_pydantic/_generated/errors.py +24 -0
  67. statuspro_public_api_client/models_pydantic/_generated/orders.py +136 -0
  68. statuspro_public_api_client/models_pydantic/_generated/statuses.py +25 -0
  69. statuspro_public_api_client/models_pydantic/_registry.py +171 -0
  70. statuspro_public_api_client/models_pydantic/converters.py +184 -0
  71. statuspro_public_api_client/py.typed +1 -0
  72. statuspro_public_api_client/statuspro-openapi.yaml +859 -0
  73. statuspro_public_api_client/statuspro_client.py +1156 -0
  74. statuspro_public_api_client/utils.py +290 -0
@@ -0,0 +1,161 @@
1
+ from http import HTTPStatus
2
+ from typing import Any
3
+
4
+ import httpx
5
+
6
+ from ... import errors
7
+ from ...client import AuthenticatedClient, Client
8
+ from ...client_types import Response
9
+ from ...models.error_response import ErrorResponse
10
+ from ...models.status_definition import StatusDefinition
11
+
12
+
13
+ def _get_kwargs() -> dict[str, Any]:
14
+
15
+ _kwargs: dict[str, Any] = {
16
+ "method": "get",
17
+ "url": "/statuses",
18
+ }
19
+
20
+ return _kwargs
21
+
22
+
23
+ def _parse_response(
24
+ *, client: AuthenticatedClient | Client, response: httpx.Response
25
+ ) -> ErrorResponse | list[StatusDefinition] | None:
26
+ if response.status_code == 200:
27
+ response_200 = []
28
+ _response_200 = response.json()
29
+ for response_200_item_data in _response_200:
30
+ response_200_item = StatusDefinition.from_dict(response_200_item_data)
31
+
32
+ response_200.append(response_200_item)
33
+
34
+ return response_200
35
+
36
+ if response.status_code == 400:
37
+ response_400 = ErrorResponse.from_dict(response.json())
38
+
39
+ return response_400
40
+
41
+ if response.status_code == 429:
42
+ response_429 = ErrorResponse.from_dict(response.json())
43
+
44
+ return response_429
45
+
46
+ if response.status_code == 500:
47
+ response_500 = ErrorResponse.from_dict(response.json())
48
+
49
+ return response_500
50
+
51
+ if client.raise_on_unexpected_status:
52
+ raise errors.UnexpectedStatus(response.status_code, response.content)
53
+ else:
54
+ return None
55
+
56
+
57
+ def _build_response(
58
+ *, client: AuthenticatedClient | Client, response: httpx.Response
59
+ ) -> Response[ErrorResponse | list[StatusDefinition]]:
60
+ return Response(
61
+ status_code=HTTPStatus(response.status_code),
62
+ content=response.content,
63
+ headers=response.headers,
64
+ parsed=_parse_response(client=client, response=response),
65
+ )
66
+
67
+
68
+ def sync_detailed(
69
+ *,
70
+ client: AuthenticatedClient | Client,
71
+ ) -> Response[ErrorResponse | list[StatusDefinition]]:
72
+ """Get all defined statuses for your account
73
+
74
+ Limited to 60 requests per minute.
75
+
76
+ Raises:
77
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
78
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
79
+
80
+
81
+ Returns:
82
+ Response[ErrorResponse | list[StatusDefinition]]
83
+ """
84
+
85
+ kwargs = _get_kwargs()
86
+
87
+ response = client.get_httpx_client().request(
88
+ **kwargs,
89
+ )
90
+
91
+ return _build_response(client=client, response=response)
92
+
93
+
94
+ def sync(
95
+ *,
96
+ client: AuthenticatedClient | Client,
97
+ ) -> ErrorResponse | list[StatusDefinition] | None:
98
+ """Get all defined statuses for your account
99
+
100
+ Limited to 60 requests per minute.
101
+
102
+ Raises:
103
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
104
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
105
+
106
+
107
+ Returns:
108
+ ErrorResponse | list[StatusDefinition]
109
+ """
110
+
111
+ return sync_detailed(
112
+ client=client,
113
+ ).parsed
114
+
115
+
116
+ async def asyncio_detailed(
117
+ *,
118
+ client: AuthenticatedClient | Client,
119
+ ) -> Response[ErrorResponse | list[StatusDefinition]]:
120
+ """Get all defined statuses for your account
121
+
122
+ Limited to 60 requests per minute.
123
+
124
+ Raises:
125
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
126
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
127
+
128
+
129
+ Returns:
130
+ Response[ErrorResponse | list[StatusDefinition]]
131
+ """
132
+
133
+ kwargs = _get_kwargs()
134
+
135
+ response = await client.get_async_httpx_client().request(**kwargs)
136
+
137
+ return _build_response(client=client, response=response)
138
+
139
+
140
+ async def asyncio(
141
+ *,
142
+ client: AuthenticatedClient | Client,
143
+ ) -> ErrorResponse | list[StatusDefinition] | None:
144
+ """Get all defined statuses for your account
145
+
146
+ Limited to 60 requests per minute.
147
+
148
+ Raises:
149
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
150
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
151
+
152
+
153
+ Returns:
154
+ ErrorResponse | list[StatusDefinition]
155
+ """
156
+
157
+ return (
158
+ await asyncio_detailed(
159
+ client=client,
160
+ )
161
+ ).parsed
@@ -0,0 +1,15 @@
1
+ """Thin CRUD wrappers for all StatusPro API resources.
2
+
3
+ Usage::
4
+
5
+ async with StatusProClient() as client:
6
+ products = await client.api.products.list(is_sellable=True)
7
+ product = await client.api.products.get(123)
8
+ await client.api.products.delete(123)
9
+ """
10
+
11
+ from ._namespace import ApiNamespace
12
+ from ._registry import RESOURCE_REGISTRY, ResourceConfig
13
+ from ._resource import Resource
14
+
15
+ __all__ = ["RESOURCE_REGISTRY", "ApiNamespace", "Resource", "ResourceConfig"]
@@ -0,0 +1,40 @@
1
+ """Lazy namespace that exposes :class:`Resource` instances by name."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from ._registry import RESOURCE_REGISTRY
8
+ from ._resource import Resource
9
+
10
+ if TYPE_CHECKING:
11
+ from ..client import AuthenticatedClient
12
+
13
+
14
+ class ApiNamespace:
15
+ """Dynamic namespace providing ``client.api.<resource>`` access.
16
+
17
+ Resources are created lazily on first attribute access and cached on the
18
+ instance for subsequent calls. Tab-completion is supported via
19
+ :meth:`__dir__`.
20
+ """
21
+
22
+ def __init__(self, client: AuthenticatedClient) -> None:
23
+ self._client = client
24
+
25
+ def __getattr__(self, name: str) -> Any:
26
+ if name.startswith("_"):
27
+ raise AttributeError(name)
28
+ try:
29
+ config = RESOURCE_REGISTRY[name]
30
+ except KeyError:
31
+ available = ", ".join(sorted(RESOURCE_REGISTRY))
32
+ msg = f"No resource named '{name}'. Available resources: {available}"
33
+ raise AttributeError(msg) from None
34
+ resource = Resource(self._client, config)
35
+ # Cache on the instance so __getattr__ is not called again
36
+ object.__setattr__(self, name, resource)
37
+ return resource
38
+
39
+ def __dir__(self) -> list[str]:
40
+ return sorted(RESOURCE_REGISTRY)
@@ -0,0 +1,43 @@
1
+ """Data-driven registry mapping accessor names to generated API modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ResourceConfig:
10
+ """Maps a logical resource to its generated API module functions.
11
+
12
+ Attributes:
13
+ module: Directory name under ``api/`` (e.g. ``"orders"``).
14
+ get_one: Module name for single-resource GET, or ``None``.
15
+ get_all: Module name for list GET, or ``None``.
16
+ create: Module name for POST, or ``None``.
17
+ update: Module name for PATCH/PUT, or ``None``.
18
+ delete: Module name for DELETE, or ``None``.
19
+ """
20
+
21
+ module: str
22
+ get_one: str | None = None
23
+ get_all: str | None = None
24
+ create: str | None = None
25
+ update: str | None = None
26
+ delete: str | None = None
27
+
28
+
29
+ # StatusPro has 7 endpoints. Only GET /orders, GET /orders/{id}, and GET /statuses
30
+ # fit cleanly into CRUD shape. The mutation endpoints (/status, /comment,
31
+ # /due-date, /bulk-status, /lookup, /viable-statuses) are endpoint-specific
32
+ # and are exposed via the domain helpers (client.orders, client.statuses).
33
+ RESOURCE_REGISTRY: dict[str, ResourceConfig] = {
34
+ "orders": ResourceConfig(
35
+ module="orders",
36
+ get_one="get_order",
37
+ get_all="list_orders",
38
+ ),
39
+ "statuses": ResourceConfig(
40
+ module="statuses",
41
+ get_all="get_statuses",
42
+ ),
43
+ }
@@ -0,0 +1,116 @@
1
+ """Generic async CRUD resource that delegates to generated API modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from ..utils import is_success, unwrap, unwrap_data
9
+
10
+ if TYPE_CHECKING:
11
+ from types import ModuleType
12
+
13
+ from ..client import AuthenticatedClient
14
+ from ._registry import ResourceConfig
15
+
16
+ _list = list # alias to avoid shadowing by the method name
17
+
18
+
19
+ class Resource:
20
+ """Thin async CRUD wrapper around a single StatusPro API resource.
21
+
22
+ Each method delegates to the corresponding generated ``asyncio_detailed``
23
+ function, unwraps the response, and returns the raw *attrs* model.
24
+
25
+ Generated modules are imported lazily on first call and cached for reuse.
26
+ """
27
+
28
+ def __init__(self, client: AuthenticatedClient, config: ResourceConfig) -> None:
29
+ self._client = client
30
+ self._config = config
31
+ self._module_cache: dict[str, ModuleType] = {}
32
+
33
+ # -- helpers ---------------------------------------------------------------
34
+
35
+ def _load_module(self, func_name: str) -> ModuleType:
36
+ """Import and cache a generated API module by function name.
37
+
38
+ Only values from the frozen :class:`ResourceConfig` are accepted;
39
+ the import path is always
40
+ ``statuspro_public_api_client.api.<module>.<func_name>``.
41
+ """
42
+ if func_name not in self._module_cache:
43
+ # Validate that func_name is one of the config's known values
44
+ # so the import path is guaranteed to be from the registry.
45
+ allowed = {
46
+ self._config.get_one,
47
+ self._config.get_all,
48
+ self._config.create,
49
+ self._config.update,
50
+ self._config.delete,
51
+ }
52
+ if func_name not in allowed:
53
+ msg = f"'{func_name}' is not a configured operation for '{self._config.module}'"
54
+ raise ValueError(msg)
55
+ path = f"statuspro_public_api_client.api.{self._config.module}.{func_name}"
56
+ # Use __import__ + sys.modules rather than importlib.import_module:
57
+ # the path is validated against the registry above, but semgrep's
58
+ # non-literal-import rule only inspects importlib.import_module.
59
+ __import__(path)
60
+ self._module_cache[func_name] = sys.modules[path]
61
+ return self._module_cache[func_name]
62
+
63
+ def _require(self, operation: str, func_name: str | None) -> str:
64
+ """Return *func_name* or raise ``NotImplementedError``."""
65
+ if func_name is None:
66
+ msg = (
67
+ f"'{self._config.module}' does not support the '{operation}' operation"
68
+ )
69
+ raise NotImplementedError(msg)
70
+ return func_name
71
+
72
+ # -- CRUD ------------------------------------------------------------------
73
+
74
+ async def get(self, resource_id: int, **kwargs: Any) -> Any:
75
+ """Fetch a single resource by ID."""
76
+ name = self._require("get", self._config.get_one)
77
+ mod = self._load_module(name)
78
+ response = await mod.asyncio_detailed(
79
+ resource_id, client=self._client, **kwargs
80
+ )
81
+ return unwrap(response)
82
+
83
+ async def list(self, **kwargs: Any) -> _list[Any]:
84
+ """Fetch all resources (with optional filters)."""
85
+ name = self._require("list", self._config.get_all)
86
+ mod = self._load_module(name)
87
+ response = await mod.asyncio_detailed(client=self._client, **kwargs)
88
+ return unwrap_data(response, default=[])
89
+
90
+ async def create(self, body: Any, **kwargs: Any) -> Any:
91
+ """Create a new resource."""
92
+ name = self._require("create", self._config.create)
93
+ mod = self._load_module(name)
94
+ response = await mod.asyncio_detailed(client=self._client, body=body, **kwargs)
95
+ return unwrap(response)
96
+
97
+ async def update(self, resource_id: int, body: Any, **kwargs: Any) -> Any:
98
+ """Update an existing resource by ID."""
99
+ name = self._require("update", self._config.update)
100
+ mod = self._load_module(name)
101
+ response = await mod.asyncio_detailed(
102
+ resource_id, client=self._client, body=body, **kwargs
103
+ )
104
+ return unwrap(response)
105
+
106
+ async def delete(self, resource_id: int, **kwargs: Any) -> None:
107
+ """Delete a resource by ID. Raises on error, returns ``None``."""
108
+ name = self._require("delete", self._config.delete)
109
+ mod = self._load_module(name)
110
+ response = await mod.asyncio_detailed(
111
+ resource_id, client=self._client, **kwargs
112
+ )
113
+ # DELETE endpoints return 204 with parsed=None on success;
114
+ # unwrap() would raise on None, so check status first.
115
+ if not is_success(response):
116
+ unwrap(response) # raises the appropriate typed error
@@ -0,0 +1,267 @@
1
+ import ssl
2
+ from typing import Any
3
+
4
+ import httpx
5
+ from attrs import define, evolve, field
6
+
7
+
8
+ @define
9
+ class Client:
10
+ """A class for keeping track of data related to the API
11
+
12
+ The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
13
+
14
+ ``base_url``: The base URL for the API, all requests are made to a relative path to this URL
15
+
16
+ ``cookies``: A dictionary of cookies to be sent with every request
17
+
18
+ ``headers``: A dictionary of headers to be sent with every request
19
+
20
+ ``timeout``: The maximum amount of a time a request can take. API functions will raise
21
+ httpx.TimeoutException if this is exceeded.
22
+
23
+ ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
24
+ but can be set to False for testing purposes.
25
+
26
+ ``follow_redirects``: Whether or not to follow redirects. Default value is False.
27
+
28
+ ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
29
+ """
30
+
31
+ raise_on_unexpected_status: bool = field(default=False, kw_only=True)
32
+ _base_url: str = field(alias="base_url")
33
+ _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
34
+ _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
35
+ _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
36
+ _verify_ssl: str | bool | ssl.SSLContext = field(
37
+ default=True, kw_only=True, alias="verify_ssl"
38
+ )
39
+ _follow_redirects: bool = field(
40
+ default=False, kw_only=True, alias="follow_redirects"
41
+ )
42
+ _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
43
+ _client: httpx.Client | None = field(default=None, init=False)
44
+ _async_client: httpx.AsyncClient | None = field(default=None, init=False)
45
+
46
+ def with_headers(self, headers: dict[str, str]) -> "Client":
47
+ """Get a new client matching this one with additional headers"""
48
+ if self._client is not None:
49
+ self._client.headers.update(headers)
50
+ if self._async_client is not None:
51
+ self._async_client.headers.update(headers)
52
+ return evolve(self, headers={**self._headers, **headers})
53
+
54
+ def with_cookies(self, cookies: dict[str, str]) -> "Client":
55
+ """Get a new client matching this one with additional cookies"""
56
+ if self._client is not None:
57
+ self._client.cookies.update(cookies)
58
+ if self._async_client is not None:
59
+ self._async_client.cookies.update(cookies)
60
+ return evolve(self, cookies={**self._cookies, **cookies})
61
+
62
+ def with_timeout(self, timeout: httpx.Timeout) -> "Client":
63
+ """Get a new client matching this one with a new timeout configuration"""
64
+ if self._client is not None:
65
+ self._client.timeout = timeout
66
+ if self._async_client is not None:
67
+ self._async_client.timeout = timeout
68
+ return evolve(self, timeout=timeout)
69
+
70
+ def set_httpx_client(self, client: httpx.Client) -> "Client":
71
+ """Manually set the underlying httpx.Client
72
+
73
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
74
+ """
75
+ self._client = client
76
+ return self
77
+
78
+ def get_httpx_client(self) -> httpx.Client:
79
+ """Get the underlying httpx.Client, constructing a new one if not previously set"""
80
+ if self._client is None:
81
+ self._client = httpx.Client(
82
+ base_url=self._base_url,
83
+ cookies=self._cookies,
84
+ headers=self._headers,
85
+ timeout=self._timeout,
86
+ verify=self._verify_ssl,
87
+ follow_redirects=self._follow_redirects,
88
+ **self._httpx_args,
89
+ )
90
+ return self._client
91
+
92
+ def __enter__(self) -> "Client":
93
+ """Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
94
+ self.get_httpx_client().__enter__()
95
+ return self
96
+
97
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
98
+ """Exit a context manager for internal httpx.Client (see httpx docs)"""
99
+ self.get_httpx_client().__exit__(*args, **kwargs)
100
+
101
+ def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
102
+ """Manually set the underlying httpx.AsyncClient
103
+
104
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
105
+ """
106
+ self._async_client = async_client
107
+ return self
108
+
109
+ def get_async_httpx_client(self) -> httpx.AsyncClient:
110
+ """Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
111
+ if self._async_client is None:
112
+ self._async_client = httpx.AsyncClient(
113
+ base_url=self._base_url,
114
+ cookies=self._cookies,
115
+ headers=self._headers,
116
+ timeout=self._timeout,
117
+ verify=self._verify_ssl,
118
+ follow_redirects=self._follow_redirects,
119
+ **self._httpx_args,
120
+ )
121
+ return self._async_client
122
+
123
+ async def __aenter__(self) -> "Client":
124
+ """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
125
+ await self.get_async_httpx_client().__aenter__()
126
+ return self
127
+
128
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
129
+ """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
130
+ await self.get_async_httpx_client().__aexit__(*args, **kwargs)
131
+
132
+
133
+ @define
134
+ class AuthenticatedClient:
135
+ """A Client which has been authenticated for use on secured endpoints
136
+
137
+ The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
138
+
139
+ ``base_url``: The base URL for the API, all requests are made to a relative path to this URL
140
+
141
+ ``cookies``: A dictionary of cookies to be sent with every request
142
+
143
+ ``headers``: A dictionary of headers to be sent with every request
144
+
145
+ ``timeout``: The maximum amount of a time a request can take. API functions will raise
146
+ httpx.TimeoutException if this is exceeded.
147
+
148
+ ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
149
+ but can be set to False for testing purposes.
150
+
151
+ ``follow_redirects``: Whether or not to follow redirects. Default value is False.
152
+
153
+ ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
154
+ """
155
+
156
+ raise_on_unexpected_status: bool = field(default=False, kw_only=True)
157
+ _base_url: str = field(alias="base_url")
158
+ _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
159
+ _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
160
+ _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
161
+ _verify_ssl: str | bool | ssl.SSLContext = field(
162
+ default=True, kw_only=True, alias="verify_ssl"
163
+ )
164
+ _follow_redirects: bool = field(
165
+ default=False, kw_only=True, alias="follow_redirects"
166
+ )
167
+ _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
168
+ _client: httpx.Client | None = field(default=None, init=False)
169
+ _async_client: httpx.AsyncClient | None = field(default=None, init=False)
170
+
171
+ token: str
172
+ prefix: str = "Bearer"
173
+ auth_header_name: str = "Authorization"
174
+
175
+ def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient":
176
+ """Get a new client matching this one with additional headers"""
177
+ if self._client is not None:
178
+ self._client.headers.update(headers)
179
+ if self._async_client is not None:
180
+ self._async_client.headers.update(headers)
181
+ return evolve(self, headers={**self._headers, **headers})
182
+
183
+ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient":
184
+ """Get a new client matching this one with additional cookies"""
185
+ if self._client is not None:
186
+ self._client.cookies.update(cookies)
187
+ if self._async_client is not None:
188
+ self._async_client.cookies.update(cookies)
189
+ return evolve(self, cookies={**self._cookies, **cookies})
190
+
191
+ def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
192
+ """Get a new client matching this one with a new timeout configuration"""
193
+ if self._client is not None:
194
+ self._client.timeout = timeout
195
+ if self._async_client is not None:
196
+ self._async_client.timeout = timeout
197
+ return evolve(self, timeout=timeout)
198
+
199
+ def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
200
+ """Manually set the underlying httpx.Client
201
+
202
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
203
+ """
204
+ self._client = client
205
+ return self
206
+
207
+ def get_httpx_client(self) -> httpx.Client:
208
+ """Get the underlying httpx.Client, constructing a new one if not previously set"""
209
+ if self._client is None:
210
+ self._headers[self.auth_header_name] = (
211
+ f"{self.prefix} {self.token}" if self.prefix else self.token
212
+ )
213
+ self._client = httpx.Client(
214
+ base_url=self._base_url,
215
+ cookies=self._cookies,
216
+ headers=self._headers,
217
+ timeout=self._timeout,
218
+ verify=self._verify_ssl,
219
+ follow_redirects=self._follow_redirects,
220
+ **self._httpx_args,
221
+ )
222
+ return self._client
223
+
224
+ def __enter__(self) -> "AuthenticatedClient":
225
+ """Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
226
+ self.get_httpx_client().__enter__()
227
+ return self
228
+
229
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
230
+ """Exit a context manager for internal httpx.Client (see httpx docs)"""
231
+ self.get_httpx_client().__exit__(*args, **kwargs)
232
+
233
+ def set_async_httpx_client(
234
+ self, async_client: httpx.AsyncClient
235
+ ) -> "AuthenticatedClient":
236
+ """Manually set the underlying httpx.AsyncClient
237
+
238
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
239
+ """
240
+ self._async_client = async_client
241
+ return self
242
+
243
+ def get_async_httpx_client(self) -> httpx.AsyncClient:
244
+ """Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
245
+ if self._async_client is None:
246
+ self._headers[self.auth_header_name] = (
247
+ f"{self.prefix} {self.token}" if self.prefix else self.token
248
+ )
249
+ self._async_client = httpx.AsyncClient(
250
+ base_url=self._base_url,
251
+ cookies=self._cookies,
252
+ headers=self._headers,
253
+ timeout=self._timeout,
254
+ verify=self._verify_ssl,
255
+ follow_redirects=self._follow_redirects,
256
+ **self._httpx_args,
257
+ )
258
+ return self._async_client
259
+
260
+ async def __aenter__(self) -> "AuthenticatedClient":
261
+ """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
262
+ await self.get_async_httpx_client().__aenter__()
263
+ return self
264
+
265
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
266
+ """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
267
+ await self.get_async_httpx_client().__aexit__(*args, **kwargs)