threecommon 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.
- threecommon/__init__.py +79 -0
- threecommon/_core/__init__.py +6 -0
- threecommon/_core/headers.py +41 -0
- threecommon/_core/http_client.py +424 -0
- threecommon/_core/parse.py +77 -0
- threecommon/_core/retry.py +80 -0
- threecommon/_core/telemetry.py +77 -0
- threecommon/_core/url.py +31 -0
- threecommon/_generated/__init__.py +8 -0
- threecommon/_generated/models.py +614 -0
- threecommon/api_version.py +15 -0
- threecommon/client.py +184 -0
- threecommon/config.py +140 -0
- threecommon/errors/__init__.py +35 -0
- threecommon/errors/base.py +81 -0
- threecommon/errors/classes.py +75 -0
- threecommon/events/__init__.py +28 -0
- threecommon/events/service.py +170 -0
- threecommon/events/types.py +124 -0
- threecommon/filters/__init__.py +51 -0
- threecommon/filters/builder.py +188 -0
- threecommon/filters/types.py +69 -0
- threecommon/helpers.py +19 -0
- threecommon/invoices/__init__.py +40 -0
- threecommon/invoices/service.py +266 -0
- threecommon/invoices/types.py +195 -0
- threecommon/pagination/__init__.py +12 -0
- threecommon/pagination/auto_paginator.py +98 -0
- threecommon/py.typed +0 -0
- threecommon/version.py +10 -0
- threecommon-0.1.0.dist-info/METADATA +399 -0
- threecommon-0.1.0.dist-info/RECORD +34 -0
- threecommon-0.1.0.dist-info/WHEEL +4 -0
- threecommon-0.1.0.dist-info/licenses/LICENSE +21 -0
threecommon/client.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""SDK clients: [ThreeCommon][] (sync) and [AsyncThreeCommon][] (async).
|
|
2
|
+
|
|
3
|
+
Construct once per process; both classes are safe to share across threads and
|
|
4
|
+
tasks. Each instance owns one underlying ``httpx`` client unless you supply
|
|
5
|
+
your own via the ``http_client`` / ``async_http_client`` kwarg.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from types import TracebackType
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from threecommon._core.http_client import (
|
|
14
|
+
AsyncHTTPClient,
|
|
15
|
+
HTTPClient,
|
|
16
|
+
HTTPClientOptions,
|
|
17
|
+
)
|
|
18
|
+
from threecommon._core.retry import RetryPolicy
|
|
19
|
+
from threecommon._core.telemetry import Telemetry
|
|
20
|
+
from threecommon.config import RetryDelay, resolve_config
|
|
21
|
+
from threecommon.events.service import AsyncEventsService, EventsService
|
|
22
|
+
from threecommon.invoices.service import AsyncInvoicesService, InvoicesService
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ThreeCommon:
|
|
31
|
+
"""Synchronous entry point.
|
|
32
|
+
|
|
33
|
+
Construct with at minimum an API key, then call any resource method.
|
|
34
|
+
Closing is optional but recommended for short-lived scripts:
|
|
35
|
+
|
|
36
|
+
client = ThreeCommon(api_key="3co_...")
|
|
37
|
+
try:
|
|
38
|
+
result = client.events.list()
|
|
39
|
+
finally:
|
|
40
|
+
client.close()
|
|
41
|
+
|
|
42
|
+
Or use as a context manager:
|
|
43
|
+
|
|
44
|
+
with ThreeCommon(api_key="3co_...") as client:
|
|
45
|
+
result = client.events.list()
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
events: EventsService
|
|
49
|
+
"""Events resource — ``GET /v1/events``, ``GET /v1/events/{id}``, ``PATCH /v1/events/{id}``."""
|
|
50
|
+
|
|
51
|
+
invoices: InvoicesService
|
|
52
|
+
"""Invoices resource — list, retrieve, create, update, finalize, void, record_payment."""
|
|
53
|
+
|
|
54
|
+
_http: HTTPClient
|
|
55
|
+
_telemetry: Telemetry
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
api_key: str | None = None,
|
|
61
|
+
base_url: str | None = None,
|
|
62
|
+
api_version: str | None = None,
|
|
63
|
+
timeout_seconds: float | None = None,
|
|
64
|
+
max_retries: int | None = None,
|
|
65
|
+
retry_delay: RetryDelay | None = None,
|
|
66
|
+
http_client: httpx.Client | None = None,
|
|
67
|
+
logger: logging.Logger | None = None,
|
|
68
|
+
telemetry: bool | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
cfg = resolve_config(
|
|
71
|
+
api_key=api_key,
|
|
72
|
+
base_url=base_url,
|
|
73
|
+
api_version=api_version,
|
|
74
|
+
timeout_seconds=timeout_seconds,
|
|
75
|
+
max_retries=max_retries,
|
|
76
|
+
retry_delay=retry_delay,
|
|
77
|
+
http_client=http_client,
|
|
78
|
+
logger=logger,
|
|
79
|
+
telemetry=telemetry,
|
|
80
|
+
)
|
|
81
|
+
self._telemetry = Telemetry(enabled=cfg.telemetry)
|
|
82
|
+
self._http = HTTPClient(
|
|
83
|
+
HTTPClientOptions(
|
|
84
|
+
api_key=cfg.api_key,
|
|
85
|
+
base_url=cfg.base_url,
|
|
86
|
+
api_version=cfg.api_version,
|
|
87
|
+
timeout_seconds=cfg.timeout_seconds,
|
|
88
|
+
retry=RetryPolicy.from_delay(cfg.max_retries, cfg.retry_delay),
|
|
89
|
+
telemetry=self._telemetry,
|
|
90
|
+
logger=cfg.logger,
|
|
91
|
+
httpx_client=cfg.http_client,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
self.events = EventsService(self._http)
|
|
95
|
+
self.invoices = InvoicesService(self._http)
|
|
96
|
+
|
|
97
|
+
def close(self) -> None:
|
|
98
|
+
"""Close the underlying httpx client (no-op if you supplied your own)."""
|
|
99
|
+
self._http.close()
|
|
100
|
+
|
|
101
|
+
def disable_telemetry(self) -> None:
|
|
102
|
+
"""Stop sending the ``Threecommon-Client-Telemetry`` header at runtime."""
|
|
103
|
+
self._telemetry.disable()
|
|
104
|
+
|
|
105
|
+
def __enter__(self) -> ThreeCommon:
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def __exit__(
|
|
109
|
+
self,
|
|
110
|
+
exc_type: type[BaseException] | None,
|
|
111
|
+
exc: BaseException | None,
|
|
112
|
+
tb: TracebackType | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
del exc_type, exc, tb
|
|
115
|
+
self.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AsyncThreeCommon:
|
|
119
|
+
"""Asynchronous entry point. Same surface as [ThreeCommon] with `await`-able methods."""
|
|
120
|
+
|
|
121
|
+
events: AsyncEventsService
|
|
122
|
+
invoices: AsyncInvoicesService
|
|
123
|
+
|
|
124
|
+
_http: AsyncHTTPClient
|
|
125
|
+
_telemetry: Telemetry
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
api_key: str | None = None,
|
|
131
|
+
base_url: str | None = None,
|
|
132
|
+
api_version: str | None = None,
|
|
133
|
+
timeout_seconds: float | None = None,
|
|
134
|
+
max_retries: int | None = None,
|
|
135
|
+
retry_delay: RetryDelay | None = None,
|
|
136
|
+
async_http_client: httpx.AsyncClient | None = None,
|
|
137
|
+
logger: logging.Logger | None = None,
|
|
138
|
+
telemetry: bool | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
cfg = resolve_config(
|
|
141
|
+
api_key=api_key,
|
|
142
|
+
base_url=base_url,
|
|
143
|
+
api_version=api_version,
|
|
144
|
+
timeout_seconds=timeout_seconds,
|
|
145
|
+
max_retries=max_retries,
|
|
146
|
+
retry_delay=retry_delay,
|
|
147
|
+
async_http_client=async_http_client,
|
|
148
|
+
logger=logger,
|
|
149
|
+
telemetry=telemetry,
|
|
150
|
+
)
|
|
151
|
+
self._telemetry = Telemetry(enabled=cfg.telemetry)
|
|
152
|
+
self._http = AsyncHTTPClient(
|
|
153
|
+
HTTPClientOptions(
|
|
154
|
+
api_key=cfg.api_key,
|
|
155
|
+
base_url=cfg.base_url,
|
|
156
|
+
api_version=cfg.api_version,
|
|
157
|
+
timeout_seconds=cfg.timeout_seconds,
|
|
158
|
+
retry=RetryPolicy.from_delay(cfg.max_retries, cfg.retry_delay),
|
|
159
|
+
telemetry=self._telemetry,
|
|
160
|
+
logger=cfg.logger,
|
|
161
|
+
async_httpx_client=cfg.async_http_client,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
self.events = AsyncEventsService(self._http)
|
|
165
|
+
self.invoices = AsyncInvoicesService(self._http)
|
|
166
|
+
|
|
167
|
+
async def aclose(self) -> None:
|
|
168
|
+
"""Close the underlying async httpx client."""
|
|
169
|
+
await self._http.aclose()
|
|
170
|
+
|
|
171
|
+
def disable_telemetry(self) -> None:
|
|
172
|
+
self._telemetry.disable()
|
|
173
|
+
|
|
174
|
+
async def __aenter__(self) -> AsyncThreeCommon:
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
async def __aexit__(
|
|
178
|
+
self,
|
|
179
|
+
exc_type: type[BaseException] | None,
|
|
180
|
+
exc: BaseException | None,
|
|
181
|
+
tb: TracebackType | None,
|
|
182
|
+
) -> None:
|
|
183
|
+
del exc_type, exc, tb
|
|
184
|
+
await self.aclose()
|
threecommon/config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Resolved client configuration.
|
|
2
|
+
|
|
3
|
+
User do not construct [ClientConfig][threecommon.config.ClientConfig]
|
|
4
|
+
directly — they pass keyword arguments to [ThreeCommon][threecommon.ThreeCommon]
|
|
5
|
+
or [AsyncThreeCommon][threecommon.AsyncThreeCommon], which build a
|
|
6
|
+
``ClientConfig`` internally after validation.
|
|
7
|
+
|
|
8
|
+
This module also exposes the named defaults so the public docs can reference
|
|
9
|
+
them.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from threecommon.api_version import API_VERSION
|
|
19
|
+
from threecommon.errors.classes import ValidationError
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
#: Default API base URL.
|
|
28
|
+
DEFAULT_BASE_URL = "https://api.3common.com"
|
|
29
|
+
|
|
30
|
+
#: Default per-request deadline.
|
|
31
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
32
|
+
|
|
33
|
+
#: Default number of retry attempts for idempotent requests on retryable
|
|
34
|
+
#: failures (408, 425, 429, 5xx, network errors).
|
|
35
|
+
DEFAULT_MAX_RETRIES = 3
|
|
36
|
+
|
|
37
|
+
#: Environment variable consulted when ``api_key`` is not passed explicitly.
|
|
38
|
+
ENV_VAR_API_KEY = "THREECOMMON_API_KEY"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class RetryDelay:
|
|
43
|
+
"""Exponential-backoff schedule.
|
|
44
|
+
|
|
45
|
+
Backoff doubles each attempt, capped at :attr:`max_seconds`. When
|
|
46
|
+
:attr:`jitter` is true the SDK picks a random value in
|
|
47
|
+
``[0, capped]`` for the actual sleep.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
initial_seconds: float = 0.5
|
|
51
|
+
max_seconds: float = 8.0
|
|
52
|
+
jitter: bool = True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#: Default backoff schedule applied when ``retry_delay`` isn't passed.
|
|
56
|
+
DEFAULT_RETRY_DELAY = RetryDelay()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class ClientConfig:
|
|
61
|
+
"""Internal frozen view of the resolved client configuration.
|
|
62
|
+
|
|
63
|
+
Construct via :func:`resolve_config` rather than directly.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
api_key: str
|
|
67
|
+
base_url: str
|
|
68
|
+
api_version: str
|
|
69
|
+
timeout_seconds: float
|
|
70
|
+
max_retries: int
|
|
71
|
+
retry_delay: RetryDelay
|
|
72
|
+
http_client: httpx.Client | None = None
|
|
73
|
+
async_http_client: httpx.AsyncClient | None = None
|
|
74
|
+
logger: logging.Logger | None = None
|
|
75
|
+
telemetry: bool = True
|
|
76
|
+
user_agent_extra: str | None = field(default=None, repr=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_config(
|
|
80
|
+
*,
|
|
81
|
+
api_key: str | None,
|
|
82
|
+
base_url: str | None,
|
|
83
|
+
api_version: str | None,
|
|
84
|
+
timeout_seconds: float | None,
|
|
85
|
+
max_retries: int | None,
|
|
86
|
+
retry_delay: RetryDelay | None,
|
|
87
|
+
http_client: httpx.Client | None = None,
|
|
88
|
+
async_http_client: httpx.AsyncClient | None = None,
|
|
89
|
+
logger: logging.Logger | None = None,
|
|
90
|
+
telemetry: bool | None = None,
|
|
91
|
+
) -> ClientConfig:
|
|
92
|
+
"""Validate constructor kwargs and return a frozen [ClientConfig].
|
|
93
|
+
|
|
94
|
+
Raises [ValidationError][threecommon.ValidationError] for missing API
|
|
95
|
+
key or invalid numeric ranges; the message names the exact field so
|
|
96
|
+
customers don't have to read a stack trace.
|
|
97
|
+
"""
|
|
98
|
+
resolved_key = api_key or os.environ.get(ENV_VAR_API_KEY, "")
|
|
99
|
+
if not resolved_key:
|
|
100
|
+
raise ValidationError(
|
|
101
|
+
code="missing_api_key",
|
|
102
|
+
message=(
|
|
103
|
+
"An API key is required. Pass `api_key` to the ThreeCommon "
|
|
104
|
+
f"constructor, or set the {ENV_VAR_API_KEY} environment variable."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
resolved_base = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
109
|
+
if not resolved_base.startswith(("http://", "https://")):
|
|
110
|
+
raise ValidationError(
|
|
111
|
+
code="invalid_base_url",
|
|
112
|
+
message=f"base_url must start with http:// or https://; got {base_url!r}.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
resolved_timeout = timeout_seconds if timeout_seconds is not None else DEFAULT_TIMEOUT_SECONDS
|
|
116
|
+
if resolved_timeout <= 0:
|
|
117
|
+
raise ValidationError(
|
|
118
|
+
code="invalid_timeout",
|
|
119
|
+
message=f"timeout_seconds must be positive; got {resolved_timeout!r}.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
resolved_retries = max_retries if max_retries is not None else DEFAULT_MAX_RETRIES
|
|
123
|
+
if resolved_retries < 0:
|
|
124
|
+
raise ValidationError(
|
|
125
|
+
code="invalid_max_retries",
|
|
126
|
+
message=f"max_retries must be non-negative; got {resolved_retries!r}.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return ClientConfig(
|
|
130
|
+
api_key=resolved_key,
|
|
131
|
+
base_url=resolved_base,
|
|
132
|
+
api_version=api_version or API_VERSION,
|
|
133
|
+
timeout_seconds=resolved_timeout,
|
|
134
|
+
max_retries=resolved_retries,
|
|
135
|
+
retry_delay=retry_delay or DEFAULT_RETRY_DELAY,
|
|
136
|
+
http_client=http_client,
|
|
137
|
+
async_http_client=async_http_client,
|
|
138
|
+
logger=logger,
|
|
139
|
+
telemetry=True if telemetry is None else telemetry,
|
|
140
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Errors module — re-exports the base [APIError][threecommon.APIError] and
|
|
2
|
+
every typed subtype.
|
|
3
|
+
|
|
4
|
+
Customers usually catch via the root re-exports::
|
|
5
|
+
|
|
6
|
+
from threecommon import NotFoundError
|
|
7
|
+
|
|
8
|
+
but importing the submodule directly also works::
|
|
9
|
+
|
|
10
|
+
from threecommon.errors import NotFoundError
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from threecommon.errors.base import APIError
|
|
14
|
+
from threecommon.errors.classes import (
|
|
15
|
+
AuthError,
|
|
16
|
+
ConflictError,
|
|
17
|
+
ConnectionError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
PermissionError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
"APIError",
|
|
27
|
+
"AuthError",
|
|
28
|
+
"ConflictError",
|
|
29
|
+
"ConnectionError",
|
|
30
|
+
"NotFoundError",
|
|
31
|
+
"PermissionError",
|
|
32
|
+
"RateLimitError",
|
|
33
|
+
"ServerError",
|
|
34
|
+
"ValidationError",
|
|
35
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Base exception type carried by every error the SDK raises.
|
|
2
|
+
|
|
3
|
+
The HTTP-status-specific subtypes ([AuthError][threecommon.AuthError],
|
|
4
|
+
[NotFoundError][threecommon.NotFoundError], ...) all inherit from
|
|
5
|
+
[APIError][threecommon.APIError]. Branch on the subtype with a normal
|
|
6
|
+
`except` clause:
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
client.events.retrieve("evt_missing")
|
|
10
|
+
except threecommon.NotFoundError as e:
|
|
11
|
+
log.warning("missing event %s", e.request_id)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APIError(Exception):
|
|
20
|
+
"""Base class for every error raised by the SDK.
|
|
21
|
+
|
|
22
|
+
Every field is best-effort: ``http_status`` is ``None`` for connection
|
|
23
|
+
errors, ``request_id`` is ``None`` when the server didn't return one,
|
|
24
|
+
``raw_response`` is empty for non-text responses.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
code: str
|
|
28
|
+
"""Stable string matching the API's error.code field, e.g. ``not_found``."""
|
|
29
|
+
|
|
30
|
+
message: str
|
|
31
|
+
"""Human-readable description. Safe to surface to end users."""
|
|
32
|
+
|
|
33
|
+
http_status: int | None
|
|
34
|
+
"""Response status, or ``None`` if the error originated before any response."""
|
|
35
|
+
|
|
36
|
+
request_id: str | None
|
|
37
|
+
"""Value of the ``X-Request-ID`` response header, when present."""
|
|
38
|
+
|
|
39
|
+
details: dict[str, Any] | None
|
|
40
|
+
"""Parsed API ``error.details`` payload, when present."""
|
|
41
|
+
|
|
42
|
+
raw_response: str | None
|
|
43
|
+
"""Raw response body, retained for debugging."""
|
|
44
|
+
|
|
45
|
+
__cause__: BaseException | None
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
code: str,
|
|
51
|
+
message: str,
|
|
52
|
+
http_status: int | None = None,
|
|
53
|
+
request_id: str | None = None,
|
|
54
|
+
details: dict[str, Any] | None = None,
|
|
55
|
+
raw_response: str | None = None,
|
|
56
|
+
cause: BaseException | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
super().__init__(self._format(code, message, request_id))
|
|
59
|
+
self.code = code
|
|
60
|
+
self.message = message
|
|
61
|
+
self.http_status = http_status
|
|
62
|
+
self.request_id = request_id
|
|
63
|
+
self.details = details
|
|
64
|
+
self.raw_response = raw_response
|
|
65
|
+
if cause is not None:
|
|
66
|
+
self.__cause__ = cause
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _format(code: str, message: str, request_id: str | None) -> str:
|
|
70
|
+
if request_id:
|
|
71
|
+
return f"[{code}] {message} (request_id={request_id})"
|
|
72
|
+
return f"[{code}] {message}"
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return (
|
|
76
|
+
f"{self.__class__.__name__}("
|
|
77
|
+
f"code={self.code!r}, "
|
|
78
|
+
f"message={self.message!r}, "
|
|
79
|
+
f"http_status={self.http_status!r}, "
|
|
80
|
+
f"request_id={self.request_id!r})"
|
|
81
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""HTTP-status-specific exception subtypes.
|
|
2
|
+
|
|
3
|
+
All inherit from [APIError][threecommon.APIError]. Catch the subtype user care
|
|
4
|
+
about; the order of `except` clauses can go from specific to general.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from threecommon.errors.base import APIError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthError(APIError):
|
|
15
|
+
"""401 Unauthorized — invalid, missing, or expired API key."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PermissionError(APIError):
|
|
19
|
+
"""403 Forbidden — the API key lacks the scope required by the endpoint."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFoundError(APIError):
|
|
23
|
+
"""404 Not Found."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ValidationError(APIError):
|
|
27
|
+
"""400 Bad Request and 422 Unprocessable Entity — request validation failed."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConflictError(APIError):
|
|
31
|
+
"""409 Conflict — the request conflicts with current resource state."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RateLimitError(APIError):
|
|
35
|
+
"""429 Too Many Requests.
|
|
36
|
+
|
|
37
|
+
[retry_after_seconds][threecommon.RateLimitError.retry_after_seconds]
|
|
38
|
+
carries the parsed ``Retry-After`` header so user can implement their
|
|
39
|
+
own backoff; it is ``None`` when the server did not provide one.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
retry_after_seconds: float | None
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
code: str,
|
|
48
|
+
message: str,
|
|
49
|
+
http_status: int | None = None,
|
|
50
|
+
request_id: str | None = None,
|
|
51
|
+
details: dict[str, Any] | None = None,
|
|
52
|
+
raw_response: str | None = None,
|
|
53
|
+
cause: BaseException | None = None,
|
|
54
|
+
retry_after_seconds: float | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(
|
|
57
|
+
code=code,
|
|
58
|
+
message=message,
|
|
59
|
+
http_status=http_status,
|
|
60
|
+
request_id=request_id,
|
|
61
|
+
details=details,
|
|
62
|
+
raw_response=raw_response,
|
|
63
|
+
cause=cause,
|
|
64
|
+
)
|
|
65
|
+
self.retry_after_seconds = retry_after_seconds
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ServerError(APIError):
|
|
69
|
+
"""5xx — the API returned an unexpected server-side failure."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ConnectionError(APIError):
|
|
73
|
+
"""The request never completed: DNS failure, TCP reset, TLS error,
|
|
74
|
+
timeout, etc. The original cause is available via ``__cause__``.
|
|
75
|
+
"""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Events resource — sync and async clients plus public types.
|
|
2
|
+
|
|
3
|
+
Most callers reach this module through
|
|
4
|
+
[ThreeCommon.events][threecommon.ThreeCommon] /
|
|
5
|
+
[AsyncThreeCommon.events][threecommon.AsyncThreeCommon]; importing the
|
|
6
|
+
service classes directly is supported for advanced wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from threecommon.events.service import AsyncEventsService, EventsService
|
|
10
|
+
from threecommon.events.types import (
|
|
11
|
+
Event,
|
|
12
|
+
EventStatus,
|
|
13
|
+
ListEventsResponse,
|
|
14
|
+
ListParams,
|
|
15
|
+
RetrieveParams,
|
|
16
|
+
UpdateBody,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = (
|
|
20
|
+
"AsyncEventsService",
|
|
21
|
+
"Event",
|
|
22
|
+
"EventStatus",
|
|
23
|
+
"EventsService",
|
|
24
|
+
"ListEventsResponse",
|
|
25
|
+
"ListParams",
|
|
26
|
+
"RetrieveParams",
|
|
27
|
+
"UpdateBody",
|
|
28
|
+
)
|