smscode 1.0.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.
- smscode/__init__.py +153 -0
- smscode/_async_transport.py +90 -0
- smscode/_decode.py +74 -0
- smscode/_transport.py +115 -0
- smscode/async_client.py +83 -0
- smscode/client.py +78 -0
- smscode/errors.py +224 -0
- smscode/idempotency.py +22 -0
- smscode/models.py +280 -0
- smscode/money.py +46 -0
- smscode/py.typed +1 -0
- smscode/resources/__init__.py +66 -0
- smscode/resources/balance.py +52 -0
- smscode/resources/catalog.py +359 -0
- smscode/resources/orders.py +625 -0
- smscode/resources/webhook.py +135 -0
- smscode/retry.py +66 -0
- smscode/types.py +35 -0
- smscode/wait.py +125 -0
- smscode/webhook.py +67 -0
- smscode-1.0.0.dist-info/METADATA +169 -0
- smscode-1.0.0.dist-info/RECORD +24 -0
- smscode-1.0.0.dist-info/WHEEL +4 -0
- smscode-1.0.0.dist-info/licenses/LICENSE +21 -0
smscode/__init__.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
|
|
3
|
+
from smscode.async_client import AsyncSmscodeClient
|
|
4
|
+
from smscode.client import SmscodeClient
|
|
5
|
+
from smscode.errors import (
|
|
6
|
+
CancelTooEarlyError,
|
|
7
|
+
ConflictError,
|
|
8
|
+
ForbiddenError,
|
|
9
|
+
FxRateUnavailableError,
|
|
10
|
+
IdempotencyKeyReuseError,
|
|
11
|
+
InsufficientBalanceError,
|
|
12
|
+
InternalError,
|
|
13
|
+
InvalidMoneyError,
|
|
14
|
+
InvalidResponseError,
|
|
15
|
+
NetworkError,
|
|
16
|
+
NoOfferAvailableError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
OrderTerminalError,
|
|
19
|
+
OtpTimeoutError,
|
|
20
|
+
PayloadTooLargeError,
|
|
21
|
+
ProviderError,
|
|
22
|
+
RateLimitError,
|
|
23
|
+
RequestCancelledError,
|
|
24
|
+
RequestInProgressError,
|
|
25
|
+
ServiceUnavailableError,
|
|
26
|
+
SmscodeError,
|
|
27
|
+
TempBannedError,
|
|
28
|
+
TimeoutError,
|
|
29
|
+
UnauthorizedError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
map_error,
|
|
32
|
+
)
|
|
33
|
+
from smscode.idempotency import (
|
|
34
|
+
IDEMPOTENCY_KEY_PATTERN,
|
|
35
|
+
is_valid_idempotency_key,
|
|
36
|
+
resolve_idempotency_key,
|
|
37
|
+
)
|
|
38
|
+
from smscode.models import (
|
|
39
|
+
BalanceV1,
|
|
40
|
+
BalanceV2,
|
|
41
|
+
CancelResult,
|
|
42
|
+
CancelResultV2,
|
|
43
|
+
Country,
|
|
44
|
+
CreatedOrder,
|
|
45
|
+
CreateOrderResult,
|
|
46
|
+
CreateOrderResultV1,
|
|
47
|
+
CreateOrderResultV2,
|
|
48
|
+
ExchangeRate,
|
|
49
|
+
FinishResult,
|
|
50
|
+
Order,
|
|
51
|
+
OrderCapabilities,
|
|
52
|
+
OrdersList,
|
|
53
|
+
OrdersListV2,
|
|
54
|
+
Product,
|
|
55
|
+
ProductsPage,
|
|
56
|
+
ProductsPageV1,
|
|
57
|
+
ProductsPageV2,
|
|
58
|
+
ProductV2,
|
|
59
|
+
ResendResult,
|
|
60
|
+
Service,
|
|
61
|
+
V2Fx,
|
|
62
|
+
WebhookConfig,
|
|
63
|
+
WebhookTestResult,
|
|
64
|
+
)
|
|
65
|
+
from smscode.money import Money, parse_money
|
|
66
|
+
from smscode.retry import RetryPolicy, async_with_retry, with_retry
|
|
67
|
+
from smscode.types import ApiResult, RequestOptions
|
|
68
|
+
from smscode.wait import OtpResult, async_wait_for_otp, wait_for_otp
|
|
69
|
+
from smscode.webhook import (
|
|
70
|
+
WebhookEvent,
|
|
71
|
+
is_webhook_event,
|
|
72
|
+
parse_webhook_event,
|
|
73
|
+
verify_webhook_signature,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
VERSION = metadata.version("smscode")
|
|
77
|
+
__version__ = VERSION
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"IDEMPOTENCY_KEY_PATTERN",
|
|
82
|
+
"VERSION",
|
|
83
|
+
"ApiResult",
|
|
84
|
+
"AsyncSmscodeClient",
|
|
85
|
+
"BalanceV1",
|
|
86
|
+
"BalanceV2",
|
|
87
|
+
"CancelResult",
|
|
88
|
+
"CancelResultV2",
|
|
89
|
+
"CancelTooEarlyError",
|
|
90
|
+
"ConflictError",
|
|
91
|
+
"Country",
|
|
92
|
+
"CreateOrderResult",
|
|
93
|
+
"CreateOrderResultV1",
|
|
94
|
+
"CreateOrderResultV2",
|
|
95
|
+
"CreatedOrder",
|
|
96
|
+
"ExchangeRate",
|
|
97
|
+
"FinishResult",
|
|
98
|
+
"ForbiddenError",
|
|
99
|
+
"FxRateUnavailableError",
|
|
100
|
+
"IdempotencyKeyReuseError",
|
|
101
|
+
"InsufficientBalanceError",
|
|
102
|
+
"InternalError",
|
|
103
|
+
"InvalidMoneyError",
|
|
104
|
+
"InvalidResponseError",
|
|
105
|
+
"Money",
|
|
106
|
+
"NetworkError",
|
|
107
|
+
"NoOfferAvailableError",
|
|
108
|
+
"NotFoundError",
|
|
109
|
+
"Order",
|
|
110
|
+
"OrderCapabilities",
|
|
111
|
+
"OrderTerminalError",
|
|
112
|
+
"OrdersList",
|
|
113
|
+
"OrdersListV2",
|
|
114
|
+
"OtpResult",
|
|
115
|
+
"OtpTimeoutError",
|
|
116
|
+
"PayloadTooLargeError",
|
|
117
|
+
"Product",
|
|
118
|
+
"ProductV2",
|
|
119
|
+
"ProductsPage",
|
|
120
|
+
"ProductsPageV1",
|
|
121
|
+
"ProductsPageV2",
|
|
122
|
+
"ProviderError",
|
|
123
|
+
"RateLimitError",
|
|
124
|
+
"RequestCancelledError",
|
|
125
|
+
"RequestInProgressError",
|
|
126
|
+
"RequestOptions",
|
|
127
|
+
"ResendResult",
|
|
128
|
+
"RetryPolicy",
|
|
129
|
+
"Service",
|
|
130
|
+
"ServiceUnavailableError",
|
|
131
|
+
"SmscodeClient",
|
|
132
|
+
"SmscodeError",
|
|
133
|
+
"TempBannedError",
|
|
134
|
+
"TimeoutError",
|
|
135
|
+
"UnauthorizedError",
|
|
136
|
+
"V2Fx",
|
|
137
|
+
"ValidationError",
|
|
138
|
+
"WebhookConfig",
|
|
139
|
+
"WebhookEvent",
|
|
140
|
+
"WebhookTestResult",
|
|
141
|
+
"__version__",
|
|
142
|
+
"async_wait_for_otp",
|
|
143
|
+
"async_with_retry",
|
|
144
|
+
"is_valid_idempotency_key",
|
|
145
|
+
"is_webhook_event",
|
|
146
|
+
"map_error",
|
|
147
|
+
"parse_money",
|
|
148
|
+
"parse_webhook_event",
|
|
149
|
+
"resolve_idempotency_key",
|
|
150
|
+
"verify_webhook_signature",
|
|
151
|
+
"wait_for_otp",
|
|
152
|
+
"with_retry",
|
|
153
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from smscode._decode import decode_response, retry_after_from_error
|
|
9
|
+
from smscode._transport import DEFAULT_BASE_URL, build_url, clean_params, is_retryable
|
|
10
|
+
from smscode.errors import NetworkError, TimeoutError
|
|
11
|
+
from smscode.retry import RetryPolicy, async_with_retry
|
|
12
|
+
from smscode.types import ApiResult, HttpMethod, QueryValue
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncTransport:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
token: str,
|
|
20
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
21
|
+
timeout_ms: int = 0,
|
|
22
|
+
max_retries: int = 0,
|
|
23
|
+
client: httpx.AsyncClient | None = None,
|
|
24
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
if not token:
|
|
27
|
+
raise TypeError("AsyncSmscodeClient requires a token.")
|
|
28
|
+
if client is not None and transport is not None:
|
|
29
|
+
raise TypeError("Pass either client or transport, not both.")
|
|
30
|
+
self._token = token
|
|
31
|
+
self._base_url = base_url
|
|
32
|
+
self._max_retries = max(0, max_retries)
|
|
33
|
+
self._owns_client = client is None
|
|
34
|
+
timeout = None if timeout_ms <= 0 else timeout_ms / 1000
|
|
35
|
+
self._client = client or httpx.AsyncClient(timeout=timeout, transport=transport)
|
|
36
|
+
|
|
37
|
+
async def aclose(self) -> None:
|
|
38
|
+
if self._owns_client:
|
|
39
|
+
await self._client.aclose()
|
|
40
|
+
|
|
41
|
+
async def request(
|
|
42
|
+
self,
|
|
43
|
+
method: HttpMethod,
|
|
44
|
+
path: str,
|
|
45
|
+
*,
|
|
46
|
+
params: Mapping[str, QueryValue] | None = None,
|
|
47
|
+
json: Any | None = None,
|
|
48
|
+
headers: Mapping[str, str] | None = None,
|
|
49
|
+
retry: int | None = None,
|
|
50
|
+
) -> ApiResult[Any]:
|
|
51
|
+
max_retries = self._max_retries if retry is None else max(0, retry)
|
|
52
|
+
policy = RetryPolicy(
|
|
53
|
+
max_retries=max_retries,
|
|
54
|
+
retry_on=is_retryable,
|
|
55
|
+
retry_after=retry_after_from_error,
|
|
56
|
+
)
|
|
57
|
+
return await async_with_retry(
|
|
58
|
+
lambda: self._attempt(method, path, params=params, json=json, headers=headers),
|
|
59
|
+
policy,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def _attempt(
|
|
63
|
+
self,
|
|
64
|
+
method: HttpMethod,
|
|
65
|
+
path: str,
|
|
66
|
+
*,
|
|
67
|
+
params: Mapping[str, QueryValue] | None,
|
|
68
|
+
json: Any | None,
|
|
69
|
+
headers: Mapping[str, str] | None,
|
|
70
|
+
) -> ApiResult[Any]:
|
|
71
|
+
request_headers = {
|
|
72
|
+
"Authorization": f"Bearer {self._token}",
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
}
|
|
76
|
+
if headers is not None:
|
|
77
|
+
request_headers.update(headers)
|
|
78
|
+
try:
|
|
79
|
+
response = await self._client.request(
|
|
80
|
+
method,
|
|
81
|
+
build_url(self._base_url, path),
|
|
82
|
+
params=clean_params(params),
|
|
83
|
+
json=json,
|
|
84
|
+
headers=request_headers,
|
|
85
|
+
)
|
|
86
|
+
except httpx.TimeoutException as exc:
|
|
87
|
+
raise TimeoutError(str(exc) or "SMSCode request timed out") from exc
|
|
88
|
+
except httpx.RequestError as exc:
|
|
89
|
+
raise NetworkError(str(exc) or "SMSCode network request failed") from exc
|
|
90
|
+
return decode_response(response)
|
smscode/_decode.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from datetime import timezone
|
|
5
|
+
from email.utils import parsedate_to_datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from smscode.errors import SmscodeError, map_error
|
|
11
|
+
from smscode.types import ApiResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def retry_after_seconds(headers: httpx.Headers) -> float | None:
|
|
15
|
+
value = headers.get("Retry-After")
|
|
16
|
+
if value is None:
|
|
17
|
+
return None
|
|
18
|
+
try:
|
|
19
|
+
seconds = float(value)
|
|
20
|
+
except ValueError:
|
|
21
|
+
try:
|
|
22
|
+
retry_at = parsedate_to_datetime(value)
|
|
23
|
+
except (TypeError, ValueError):
|
|
24
|
+
return None
|
|
25
|
+
if retry_at.tzinfo is None:
|
|
26
|
+
retry_at = retry_at.replace(tzinfo=timezone.utc)
|
|
27
|
+
date_header = headers.get("Date")
|
|
28
|
+
if date_header is None:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
response_date = parsedate_to_datetime(date_header)
|
|
32
|
+
except (TypeError, ValueError):
|
|
33
|
+
return None
|
|
34
|
+
if response_date.tzinfo is None:
|
|
35
|
+
response_date = response_date.replace(tzinfo=timezone.utc)
|
|
36
|
+
return max(0.0, (retry_at - response_date).total_seconds())
|
|
37
|
+
return max(0.0, seconds)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def retry_after_from_error(err: Exception) -> float | None:
|
|
41
|
+
if isinstance(err, SmscodeError):
|
|
42
|
+
return err.retry_after_seconds
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def decode_response(response: httpx.Response) -> ApiResult[Any]:
|
|
47
|
+
request_id = response.headers.get("X-Request-Id")
|
|
48
|
+
payload = _json_or_none(response)
|
|
49
|
+
envelope = payload if isinstance(payload, Mapping) else None
|
|
50
|
+
is_failure = not response.is_success or (
|
|
51
|
+
envelope is not None and envelope.get("success") is False
|
|
52
|
+
)
|
|
53
|
+
if is_failure:
|
|
54
|
+
raise map_error(
|
|
55
|
+
status=response.status_code,
|
|
56
|
+
payload=envelope,
|
|
57
|
+
request_id=request_id,
|
|
58
|
+
retry_after_seconds=retry_after_seconds(response.headers),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
meta_value = envelope.get("meta") if envelope is not None else None
|
|
62
|
+
meta = dict(meta_value) if isinstance(meta_value, Mapping) else None
|
|
63
|
+
data = envelope.get("data") if envelope is not None and "data" in envelope else payload
|
|
64
|
+
return ApiResult(data=data, meta=meta, request_id=request_id, status=response.status_code)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _json_or_none(response: httpx.Response) -> object | None:
|
|
68
|
+
if not response.content:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
parsed: object = response.json()
|
|
72
|
+
return parsed
|
|
73
|
+
except ValueError:
|
|
74
|
+
return None
|
smscode/_transport.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from smscode._decode import decode_response, retry_after_from_error
|
|
9
|
+
from smscode.errors import NetworkError, SmscodeError, TimeoutError
|
|
10
|
+
from smscode.retry import RetryPolicy, with_retry
|
|
11
|
+
from smscode.types import ApiResult, HttpMethod, QueryValue
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.smscode.gg"
|
|
14
|
+
RETRYABLE_STATUSES = {429, 503}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_url(base_url: str, path: str) -> str:
|
|
18
|
+
base = base_url.rstrip("/")
|
|
19
|
+
rel = path if path.startswith("/") else f"/{path}"
|
|
20
|
+
return f"{base}{rel}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clean_params(params: Mapping[str, QueryValue] | None) -> dict[str, str] | None:
|
|
24
|
+
if params is None:
|
|
25
|
+
return None
|
|
26
|
+
cleaned: dict[str, str] = {}
|
|
27
|
+
for key, value in params.items():
|
|
28
|
+
if value is None:
|
|
29
|
+
continue
|
|
30
|
+
cleaned[key] = str(value).lower() if isinstance(value, bool) else str(value)
|
|
31
|
+
return cleaned
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SyncTransport:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
token: str,
|
|
39
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
40
|
+
timeout_ms: int = 0,
|
|
41
|
+
max_retries: int = 0,
|
|
42
|
+
client: httpx.Client | None = None,
|
|
43
|
+
transport: httpx.BaseTransport | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
if not token:
|
|
46
|
+
raise TypeError("SmscodeClient requires a token.")
|
|
47
|
+
if client is not None and transport is not None:
|
|
48
|
+
raise TypeError("Pass either client or transport, not both.")
|
|
49
|
+
self._token = token
|
|
50
|
+
self._base_url = base_url
|
|
51
|
+
self._max_retries = max(0, max_retries)
|
|
52
|
+
self._owns_client = client is None
|
|
53
|
+
timeout = None if timeout_ms <= 0 else timeout_ms / 1000
|
|
54
|
+
self._client = client or httpx.Client(timeout=timeout, transport=transport)
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
if self._owns_client:
|
|
58
|
+
self._client.close()
|
|
59
|
+
|
|
60
|
+
def request(
|
|
61
|
+
self,
|
|
62
|
+
method: HttpMethod,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
params: Mapping[str, QueryValue] | None = None,
|
|
66
|
+
json: Any | None = None,
|
|
67
|
+
headers: Mapping[str, str] | None = None,
|
|
68
|
+
retry: int | None = None,
|
|
69
|
+
) -> ApiResult[Any]:
|
|
70
|
+
max_retries = self._max_retries if retry is None else max(0, retry)
|
|
71
|
+
policy = RetryPolicy(
|
|
72
|
+
max_retries=max_retries,
|
|
73
|
+
retry_on=is_retryable,
|
|
74
|
+
retry_after=retry_after_from_error,
|
|
75
|
+
)
|
|
76
|
+
return with_retry(
|
|
77
|
+
lambda: self._attempt(method, path, params=params, json=json, headers=headers),
|
|
78
|
+
policy,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _attempt(
|
|
82
|
+
self,
|
|
83
|
+
method: HttpMethod,
|
|
84
|
+
path: str,
|
|
85
|
+
*,
|
|
86
|
+
params: Mapping[str, QueryValue] | None,
|
|
87
|
+
json: Any | None,
|
|
88
|
+
headers: Mapping[str, str] | None,
|
|
89
|
+
) -> ApiResult[Any]:
|
|
90
|
+
request_headers = {
|
|
91
|
+
"Authorization": f"Bearer {self._token}",
|
|
92
|
+
"Accept": "application/json",
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
}
|
|
95
|
+
if headers is not None:
|
|
96
|
+
request_headers.update(headers)
|
|
97
|
+
try:
|
|
98
|
+
response = self._client.request(
|
|
99
|
+
method,
|
|
100
|
+
build_url(self._base_url, path),
|
|
101
|
+
params=clean_params(params),
|
|
102
|
+
json=json,
|
|
103
|
+
headers=request_headers,
|
|
104
|
+
)
|
|
105
|
+
except httpx.TimeoutException as exc:
|
|
106
|
+
raise TimeoutError(str(exc) or "SMSCode request timed out") from exc
|
|
107
|
+
except httpx.RequestError as exc:
|
|
108
|
+
raise NetworkError(str(exc) or "SMSCode network request failed") from exc
|
|
109
|
+
return decode_response(response)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_retryable(err: Exception) -> bool:
|
|
113
|
+
if isinstance(err, NetworkError):
|
|
114
|
+
return True
|
|
115
|
+
return isinstance(err, SmscodeError) and err.http_status in RETRYABLE_STATUSES
|
smscode/async_client.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from smscode._async_transport import AsyncTransport
|
|
10
|
+
from smscode._transport import DEFAULT_BASE_URL
|
|
11
|
+
from smscode.resources import (
|
|
12
|
+
AsyncV1CatalogBalanceNamespace,
|
|
13
|
+
AsyncV1CatalogResource,
|
|
14
|
+
AsyncV2CatalogResource,
|
|
15
|
+
)
|
|
16
|
+
from smscode.resources.balance import AsyncV1BalanceResource, AsyncV2BalanceResource
|
|
17
|
+
from smscode.resources.orders import AsyncV1OrdersResource, AsyncV2OrdersResource
|
|
18
|
+
from smscode.resources.webhook import AsyncV1WebhookResource, AsyncV2WebhookResource
|
|
19
|
+
from smscode.types import ApiResult, HttpMethod, QueryValue
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncSmscodeClient:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
token: str,
|
|
27
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
28
|
+
timeout_ms: int = 0,
|
|
29
|
+
max_retries: int = 0,
|
|
30
|
+
client: httpx.AsyncClient | None = None,
|
|
31
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._transport = AsyncTransport(
|
|
34
|
+
token=token,
|
|
35
|
+
base_url=base_url,
|
|
36
|
+
timeout_ms=timeout_ms,
|
|
37
|
+
max_retries=max_retries,
|
|
38
|
+
client=client,
|
|
39
|
+
transport=transport,
|
|
40
|
+
)
|
|
41
|
+
self.catalog = AsyncV2CatalogResource(self.request)
|
|
42
|
+
self.balance = AsyncV2BalanceResource(self.request)
|
|
43
|
+
self.orders = AsyncV2OrdersResource(self.request)
|
|
44
|
+
self.webhook = AsyncV2WebhookResource(self.request)
|
|
45
|
+
self.v1 = AsyncV1CatalogBalanceNamespace(
|
|
46
|
+
catalog=AsyncV1CatalogResource(self.request),
|
|
47
|
+
balance=AsyncV1BalanceResource(self.request),
|
|
48
|
+
orders=AsyncV1OrdersResource(self.request),
|
|
49
|
+
webhook=AsyncV1WebhookResource(self.request),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def __aenter__(self) -> AsyncSmscodeClient:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(
|
|
56
|
+
self,
|
|
57
|
+
exc_type: type[BaseException] | None,
|
|
58
|
+
exc: BaseException | None,
|
|
59
|
+
traceback: TracebackType | None,
|
|
60
|
+
) -> None:
|
|
61
|
+
await self.aclose()
|
|
62
|
+
|
|
63
|
+
async def aclose(self) -> None:
|
|
64
|
+
await self._transport.aclose()
|
|
65
|
+
|
|
66
|
+
async def request(
|
|
67
|
+
self,
|
|
68
|
+
method: HttpMethod,
|
|
69
|
+
path: str,
|
|
70
|
+
*,
|
|
71
|
+
params: Mapping[str, QueryValue] | None = None,
|
|
72
|
+
json: Any | None = None,
|
|
73
|
+
headers: Mapping[str, str] | None = None,
|
|
74
|
+
retry: int | None = None,
|
|
75
|
+
) -> ApiResult[Any]:
|
|
76
|
+
return await self._transport.request(
|
|
77
|
+
method,
|
|
78
|
+
path,
|
|
79
|
+
params=params,
|
|
80
|
+
json=json,
|
|
81
|
+
headers=headers,
|
|
82
|
+
retry=retry,
|
|
83
|
+
)
|
smscode/client.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from smscode._transport import DEFAULT_BASE_URL, SyncTransport
|
|
10
|
+
from smscode.resources import V1CatalogBalanceNamespace, V1CatalogResource, V2CatalogResource
|
|
11
|
+
from smscode.resources.balance import V1BalanceResource, V2BalanceResource
|
|
12
|
+
from smscode.resources.orders import V1OrdersResource, V2OrdersResource
|
|
13
|
+
from smscode.resources.webhook import V1WebhookResource, V2WebhookResource
|
|
14
|
+
from smscode.types import ApiResult, HttpMethod, QueryValue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SmscodeClient:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
token: str,
|
|
22
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
23
|
+
timeout_ms: int = 0,
|
|
24
|
+
max_retries: int = 0,
|
|
25
|
+
client: httpx.Client | None = None,
|
|
26
|
+
transport: httpx.BaseTransport | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._transport = SyncTransport(
|
|
29
|
+
token=token,
|
|
30
|
+
base_url=base_url,
|
|
31
|
+
timeout_ms=timeout_ms,
|
|
32
|
+
max_retries=max_retries,
|
|
33
|
+
client=client,
|
|
34
|
+
transport=transport,
|
|
35
|
+
)
|
|
36
|
+
self.catalog = V2CatalogResource(self.request)
|
|
37
|
+
self.balance = V2BalanceResource(self.request)
|
|
38
|
+
self.orders = V2OrdersResource(self.request)
|
|
39
|
+
self.webhook = V2WebhookResource(self.request)
|
|
40
|
+
self.v1 = V1CatalogBalanceNamespace(
|
|
41
|
+
catalog=V1CatalogResource(self.request),
|
|
42
|
+
balance=V1BalanceResource(self.request),
|
|
43
|
+
orders=V1OrdersResource(self.request),
|
|
44
|
+
webhook=V1WebhookResource(self.request),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> SmscodeClient:
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(
|
|
51
|
+
self,
|
|
52
|
+
exc_type: type[BaseException] | None,
|
|
53
|
+
exc: BaseException | None,
|
|
54
|
+
traceback: TracebackType | None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.close()
|
|
57
|
+
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
self._transport.close()
|
|
60
|
+
|
|
61
|
+
def request(
|
|
62
|
+
self,
|
|
63
|
+
method: HttpMethod,
|
|
64
|
+
path: str,
|
|
65
|
+
*,
|
|
66
|
+
params: Mapping[str, QueryValue] | None = None,
|
|
67
|
+
json: Any | None = None,
|
|
68
|
+
headers: Mapping[str, str] | None = None,
|
|
69
|
+
retry: int | None = None,
|
|
70
|
+
) -> ApiResult[Any]:
|
|
71
|
+
return self._transport.request(
|
|
72
|
+
method,
|
|
73
|
+
path,
|
|
74
|
+
params=params,
|
|
75
|
+
json=json,
|
|
76
|
+
headers=headers,
|
|
77
|
+
retry=retry,
|
|
78
|
+
)
|