smscode 1.0.0__tar.gz

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 (72) hide show
  1. smscode-1.0.0/.gitignore +9 -0
  2. smscode-1.0.0/LICENSE +21 -0
  3. smscode-1.0.0/PKG-INFO +169 -0
  4. smscode-1.0.0/README.md +144 -0
  5. smscode-1.0.0/pyproject.toml +82 -0
  6. smscode-1.0.0/src/smscode/__init__.py +153 -0
  7. smscode-1.0.0/src/smscode/_async_transport.py +90 -0
  8. smscode-1.0.0/src/smscode/_decode.py +74 -0
  9. smscode-1.0.0/src/smscode/_transport.py +115 -0
  10. smscode-1.0.0/src/smscode/async_client.py +83 -0
  11. smscode-1.0.0/src/smscode/client.py +78 -0
  12. smscode-1.0.0/src/smscode/errors.py +224 -0
  13. smscode-1.0.0/src/smscode/idempotency.py +22 -0
  14. smscode-1.0.0/src/smscode/models.py +280 -0
  15. smscode-1.0.0/src/smscode/money.py +46 -0
  16. smscode-1.0.0/src/smscode/py.typed +1 -0
  17. smscode-1.0.0/src/smscode/resources/__init__.py +66 -0
  18. smscode-1.0.0/src/smscode/resources/balance.py +52 -0
  19. smscode-1.0.0/src/smscode/resources/catalog.py +359 -0
  20. smscode-1.0.0/src/smscode/resources/orders.py +625 -0
  21. smscode-1.0.0/src/smscode/resources/webhook.py +135 -0
  22. smscode-1.0.0/src/smscode/retry.py +66 -0
  23. smscode-1.0.0/src/smscode/types.py +35 -0
  24. smscode-1.0.0/src/smscode/wait.py +125 -0
  25. smscode-1.0.0/src/smscode/webhook.py +67 -0
  26. smscode-1.0.0/tests/contract_manifest.json +76 -0
  27. smscode-1.0.0/tests/fixtures/negative_v1_create_missing_orders.json +1 -0
  28. smscode-1.0.0/tests/fixtures/negative_v2_create_malformed_money.json +1 -0
  29. smscode-1.0.0/tests/fixtures/negative_v2_order_missing_capability.json +1 -0
  30. smscode-1.0.0/tests/fixtures/v1_balance.json +1 -0
  31. smscode-1.0.0/tests/fixtures/v1_cancel_result.json +1 -0
  32. smscode-1.0.0/tests/fixtures/v1_catalog_countries.json +1 -0
  33. smscode-1.0.0/tests/fixtures/v1_catalog_products.json +1 -0
  34. smscode-1.0.0/tests/fixtures/v1_catalog_services.json +1 -0
  35. smscode-1.0.0/tests/fixtures/v1_create_result.json +1 -0
  36. smscode-1.0.0/tests/fixtures/v1_exchange_rate.json +1 -0
  37. smscode-1.0.0/tests/fixtures/v1_finish_result.json +1 -0
  38. smscode-1.0.0/tests/fixtures/v1_order_statuses.json +1 -0
  39. smscode-1.0.0/tests/fixtures/v1_order_summary.json +1 -0
  40. smscode-1.0.0/tests/fixtures/v1_orders_list.json +1 -0
  41. smscode-1.0.0/tests/fixtures/v1_resend_result.json +1 -0
  42. smscode-1.0.0/tests/fixtures/v2_balance.json +1 -0
  43. smscode-1.0.0/tests/fixtures/v2_cancel_result.json +1 -0
  44. smscode-1.0.0/tests/fixtures/v2_catalog_products.json +1 -0
  45. smscode-1.0.0/tests/fixtures/v2_create_result.json +1 -0
  46. smscode-1.0.0/tests/fixtures/v2_exchange_rate.json +1 -0
  47. smscode-1.0.0/tests/fixtures/v2_order_summary.json +1 -0
  48. smscode-1.0.0/tests/fixtures/v2_orders_list.json +1 -0
  49. smscode-1.0.0/tests/fixtures/webhook_config.json +1 -0
  50. smscode-1.0.0/tests/fixtures/webhook_order_canceled.json +1 -0
  51. smscode-1.0.0/tests/fixtures/webhook_order_completed.json +1 -0
  52. smscode-1.0.0/tests/fixtures/webhook_order_expired.json +1 -0
  53. smscode-1.0.0/tests/fixtures/webhook_order_otp_received.json +1 -0
  54. smscode-1.0.0/tests/fixtures/webhook_test_event.json +1 -0
  55. smscode-1.0.0/tests/fixtures/webhook_test_result.json +1 -0
  56. smscode-1.0.0/tests/test_balance.py +70 -0
  57. smscode-1.0.0/tests/test_catalog.py +191 -0
  58. smscode-1.0.0/tests/test_client.py +55 -0
  59. smscode-1.0.0/tests/test_contract.py +168 -0
  60. smscode-1.0.0/tests/test_errors.py +85 -0
  61. smscode-1.0.0/tests/test_idempotency.py +15 -0
  62. smscode-1.0.0/tests/test_models.py +8 -0
  63. smscode-1.0.0/tests/test_money.py +25 -0
  64. smscode-1.0.0/tests/test_orders.py +229 -0
  65. smscode-1.0.0/tests/test_orders_idempotency.py +106 -0
  66. smscode-1.0.0/tests/test_package_install.py +93 -0
  67. smscode-1.0.0/tests/test_package_metadata.py +41 -0
  68. smscode-1.0.0/tests/test_retry.py +27 -0
  69. smscode-1.0.0/tests/test_types.py +25 -0
  70. smscode-1.0.0/tests/test_wait.py +150 -0
  71. smscode-1.0.0/tests/test_webhook.py +141 -0
  72. smscode-1.0.0/uv.lock +1098 -0
@@ -0,0 +1,9 @@
1
+ dist/
2
+ build/
3
+ .venv/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ __pycache__/
9
+ *.pyc
smscode-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SMSCode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
smscode-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: smscode
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the SMSCode virtual number API.
5
+ Project-URL: Homepage, https://smscode.gg
6
+ Project-URL: Documentation, https://smscode.gg/docs/ai.md
7
+ Project-URL: Repository, https://github.com/smscode-gg/sdks
8
+ Project-URL: Issues, https://github.com/smscode-gg/sdks/issues
9
+ Author-email: SMSCode <dev@smscode.gg>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: 2fa,api-client,online-sms,otp,phone-verification,python,receive-sms-online,sdk,sms,sms-otp,sms-verification,smscode,temporary-phone-number,virtual-number,virtual-phone-number
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx<1,>=0.27
24
+ Description-Content-Type: text/markdown
25
+
26
+ # smscode
27
+
28
+ Official Python SDK for the SMSCode virtual-number API.
29
+
30
+ Use it to rent temporary phone numbers, receive SMS OTP verification codes, and
31
+ manage order lifecycle from Python services, bots, and automations.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install smscode
37
+ ```
38
+
39
+ Requires Python 3.10+.
40
+
41
+ ## Quick start
42
+
43
+ `SmscodeClient` uses the USD-native `/v2` API by default. Money values are typed
44
+ objects with the exact IDR ledger amount preserved as `canonical_amount`.
45
+
46
+ ```py
47
+ import os
48
+
49
+ from smscode import OtpTimeoutError, OrderTerminalError, SmscodeClient
50
+
51
+ client = SmscodeClient(token=os.environ["SMSCODE_TOKEN"])
52
+
53
+ body = {
54
+ "catalog_product_id": int(os.environ["SMSCODE_CATALOG_PRODUCT_ID"]),
55
+ "max_price": "0.50", # /v2 uses a USD decimal string, never a float
56
+ "quantity": 1,
57
+ }
58
+
59
+ with client:
60
+ created = client.orders.create(body)
61
+ order = created.orders[0]
62
+ order_id = int(order["id"])
63
+
64
+ try:
65
+ otp = client.orders.wait_for_otp(order_id, timeout_ms=120_000)
66
+ print("OTP:", otp.otp_code)
67
+ # Submit otp.otp_code in your target app here.
68
+ client.orders.finish(order_id)
69
+ except (OtpTimeoutError, OrderTerminalError):
70
+ # No OTP evidence arrived. Cancel remains available only in that case.
71
+ client.orders.cancel(order_id)
72
+ ```
73
+
74
+ ## Async client
75
+
76
+ The async client has the same surface and uses `httpx.AsyncClient` internally.
77
+
78
+ ```py
79
+ import os
80
+
81
+ from smscode import AsyncSmscodeClient
82
+
83
+
84
+ async def main() -> None:
85
+ async with AsyncSmscodeClient(token=os.environ["SMSCODE_TOKEN"]) as client:
86
+ balance = await client.balance.get()
87
+ print(balance.balance.amount, balance.balance.currency)
88
+ ```
89
+
90
+ ## Resend and wait for a new OTP
91
+
92
+ `finish` does not require a new OTP after resend; the order is finishable once it
93
+ has OTP evidence. If your integration needs to wait for a different post-resend
94
+ code, pass the previous code as `after_code`.
95
+
96
+ ```py
97
+ first = client.orders.wait_for_otp(order_id)
98
+
99
+ client.orders.resend(order_id)
100
+
101
+ second = client.orders.wait_for_otp(
102
+ order_id,
103
+ after_code=first.otp_code,
104
+ timeout_ms=120_000,
105
+ )
106
+
107
+ print("new OTP:", second.otp_code)
108
+ # Submit second.otp_code in your target app here, then finish.
109
+ client.orders.finish(order_id)
110
+ ```
111
+
112
+ If the provider sends the same digits again, code-based polling cannot
113
+ distinguish it from the previous OTP.
114
+
115
+ ## Idempotent order create
116
+
117
+ Order create is money-sensitive. The SDK resolves an idempotency key before the
118
+ request, sends it as `idempotency-key`, and attaches it to create errors.
119
+
120
+ ```py
121
+ from smscode import SmscodeError
122
+
123
+ try:
124
+ created = client.orders.create(body)
125
+ except SmscodeError as err:
126
+ if err.idempotency_key is None:
127
+ raise
128
+ # Retry the exact same body with the same key. Never mint a fresh key for
129
+ # the same attempted create.
130
+ created = client.orders.create(body, idempotency_key=err.idempotency_key)
131
+ ```
132
+
133
+ ## Webhooks
134
+
135
+ Verify webhook signatures against the raw request body before parsing JSON.
136
+
137
+ ```py
138
+ from smscode import parse_webhook_event, verify_webhook_signature
139
+
140
+
141
+ def handle_webhook(raw_body: bytes, signature_header: str | None, secret: str) -> int:
142
+ if not verify_webhook_signature(raw_body, signature_header or "", secret):
143
+ return 401
144
+
145
+ event = parse_webhook_event(raw_body)
146
+ if event["event"] == "order.otp_received":
147
+ print(event["data"]["otp_code"])
148
+ return 204
149
+ ```
150
+
151
+ ## `/v1` namespace
152
+
153
+ Use `.v1` only when you intentionally want legacy IDR-only shapes.
154
+
155
+ ```py
156
+ with SmscodeClient(token=os.environ["SMSCODE_TOKEN"]) as client:
157
+ balance_v2 = client.balance.get()
158
+ balance_v1 = client.v1.balance.get()
159
+ ```
160
+
161
+ ## Error handling
162
+
163
+ Every API error is a typed `SmscodeError` subclass. Branch on the class or
164
+ `err.code`, not on `err.message`. `RateLimitError` and retryable server errors
165
+ carry `retry_after_seconds` when the API sends `Retry-After`.
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,144 @@
1
+ # smscode
2
+
3
+ Official Python SDK for the SMSCode virtual-number API.
4
+
5
+ Use it to rent temporary phone numbers, receive SMS OTP verification codes, and
6
+ manage order lifecycle from Python services, bots, and automations.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install smscode
12
+ ```
13
+
14
+ Requires Python 3.10+.
15
+
16
+ ## Quick start
17
+
18
+ `SmscodeClient` uses the USD-native `/v2` API by default. Money values are typed
19
+ objects with the exact IDR ledger amount preserved as `canonical_amount`.
20
+
21
+ ```py
22
+ import os
23
+
24
+ from smscode import OtpTimeoutError, OrderTerminalError, SmscodeClient
25
+
26
+ client = SmscodeClient(token=os.environ["SMSCODE_TOKEN"])
27
+
28
+ body = {
29
+ "catalog_product_id": int(os.environ["SMSCODE_CATALOG_PRODUCT_ID"]),
30
+ "max_price": "0.50", # /v2 uses a USD decimal string, never a float
31
+ "quantity": 1,
32
+ }
33
+
34
+ with client:
35
+ created = client.orders.create(body)
36
+ order = created.orders[0]
37
+ order_id = int(order["id"])
38
+
39
+ try:
40
+ otp = client.orders.wait_for_otp(order_id, timeout_ms=120_000)
41
+ print("OTP:", otp.otp_code)
42
+ # Submit otp.otp_code in your target app here.
43
+ client.orders.finish(order_id)
44
+ except (OtpTimeoutError, OrderTerminalError):
45
+ # No OTP evidence arrived. Cancel remains available only in that case.
46
+ client.orders.cancel(order_id)
47
+ ```
48
+
49
+ ## Async client
50
+
51
+ The async client has the same surface and uses `httpx.AsyncClient` internally.
52
+
53
+ ```py
54
+ import os
55
+
56
+ from smscode import AsyncSmscodeClient
57
+
58
+
59
+ async def main() -> None:
60
+ async with AsyncSmscodeClient(token=os.environ["SMSCODE_TOKEN"]) as client:
61
+ balance = await client.balance.get()
62
+ print(balance.balance.amount, balance.balance.currency)
63
+ ```
64
+
65
+ ## Resend and wait for a new OTP
66
+
67
+ `finish` does not require a new OTP after resend; the order is finishable once it
68
+ has OTP evidence. If your integration needs to wait for a different post-resend
69
+ code, pass the previous code as `after_code`.
70
+
71
+ ```py
72
+ first = client.orders.wait_for_otp(order_id)
73
+
74
+ client.orders.resend(order_id)
75
+
76
+ second = client.orders.wait_for_otp(
77
+ order_id,
78
+ after_code=first.otp_code,
79
+ timeout_ms=120_000,
80
+ )
81
+
82
+ print("new OTP:", second.otp_code)
83
+ # Submit second.otp_code in your target app here, then finish.
84
+ client.orders.finish(order_id)
85
+ ```
86
+
87
+ If the provider sends the same digits again, code-based polling cannot
88
+ distinguish it from the previous OTP.
89
+
90
+ ## Idempotent order create
91
+
92
+ Order create is money-sensitive. The SDK resolves an idempotency key before the
93
+ request, sends it as `idempotency-key`, and attaches it to create errors.
94
+
95
+ ```py
96
+ from smscode import SmscodeError
97
+
98
+ try:
99
+ created = client.orders.create(body)
100
+ except SmscodeError as err:
101
+ if err.idempotency_key is None:
102
+ raise
103
+ # Retry the exact same body with the same key. Never mint a fresh key for
104
+ # the same attempted create.
105
+ created = client.orders.create(body, idempotency_key=err.idempotency_key)
106
+ ```
107
+
108
+ ## Webhooks
109
+
110
+ Verify webhook signatures against the raw request body before parsing JSON.
111
+
112
+ ```py
113
+ from smscode import parse_webhook_event, verify_webhook_signature
114
+
115
+
116
+ def handle_webhook(raw_body: bytes, signature_header: str | None, secret: str) -> int:
117
+ if not verify_webhook_signature(raw_body, signature_header or "", secret):
118
+ return 401
119
+
120
+ event = parse_webhook_event(raw_body)
121
+ if event["event"] == "order.otp_received":
122
+ print(event["data"]["otp_code"])
123
+ return 204
124
+ ```
125
+
126
+ ## `/v1` namespace
127
+
128
+ Use `.v1` only when you intentionally want legacy IDR-only shapes.
129
+
130
+ ```py
131
+ with SmscodeClient(token=os.environ["SMSCODE_TOKEN"]) as client:
132
+ balance_v2 = client.balance.get()
133
+ balance_v1 = client.v1.balance.get()
134
+ ```
135
+
136
+ ## Error handling
137
+
138
+ Every API error is a typed `SmscodeError` subclass. Branch on the class or
139
+ `err.code`, not on `err.message`. `RateLimitError` and retryable server errors
140
+ carry `retry_after_seconds` when the API sends `Retry-After`.
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "smscode"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for the SMSCode virtual number API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "SMSCode", email = "dev@smscode.gg" }]
13
+ keywords = [
14
+ "smscode",
15
+ "sms",
16
+ "otp",
17
+ "virtual-number",
18
+ "phone-verification",
19
+ "2fa",
20
+ "sms-verification",
21
+ "api-client",
22
+ "sdk",
23
+ "python",
24
+ "receive-sms-online",
25
+ "sms-otp",
26
+ "temporary-phone-number",
27
+ "virtual-phone-number",
28
+ "online-sms",
29
+ ]
30
+ classifiers = [
31
+ "Development Status :: 5 - Production/Stable",
32
+ "Intended Audience :: Developers",
33
+ "License :: OSI Approved :: MIT License",
34
+ "Programming Language :: Python :: 3",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Programming Language :: Python :: 3.13",
39
+ "Typing :: Typed",
40
+ ]
41
+ dependencies = ["httpx>=0.27,<1"]
42
+
43
+ [project.urls]
44
+ Homepage = "https://smscode.gg"
45
+ Documentation = "https://smscode.gg/docs/ai.md"
46
+ Repository = "https://github.com/smscode-gg/sdks"
47
+ Issues = "https://github.com/smscode-gg/sdks/issues"
48
+
49
+ [dependency-groups]
50
+ test = ["pytest>=8", "pytest-asyncio>=0.23", "respx>=0.21", "pyyaml>=6"]
51
+ lint = ["ruff>=0.8"]
52
+ type = ["mypy>=1.11"]
53
+ build = ["build>=1.2", "twine>=5"]
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/smscode"]
57
+
58
+ [tool.hatch.build.targets.sdist]
59
+ include = [
60
+ "/src/smscode",
61
+ "/tests",
62
+ "/LICENSE",
63
+ "/README.md",
64
+ "/pyproject.toml",
65
+ "/uv.lock",
66
+ ]
67
+
68
+ [tool.ruff]
69
+ line-length = 100
70
+ target-version = "py310"
71
+
72
+ [tool.ruff.lint]
73
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
74
+
75
+ [tool.mypy]
76
+ python_version = "3.10"
77
+ strict = true
78
+ packages = ["smscode"]
79
+
80
+ [tool.pytest.ini_options]
81
+ testpaths = ["tests"]
82
+ asyncio_mode = "auto"
@@ -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)