eveses 0.1.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.
- eveses-0.1.0/PKG-INFO +200 -0
- eveses-0.1.0/README.md +174 -0
- eveses-0.1.0/eveses/__init__.py +66 -0
- eveses-0.1.0/eveses/activations.py +157 -0
- eveses-0.1.0/eveses/catalog.py +216 -0
- eveses-0.1.0/eveses/client.py +198 -0
- eveses-0.1.0/eveses/exceptions.py +76 -0
- eveses-0.1.0/eveses/wallet.py +47 -0
- eveses-0.1.0/eveses/webhooks.py +101 -0
- eveses-0.1.0/pyproject.toml +44 -0
- eveses-0.1.0/tests/test_catalog.py +153 -0
- eveses-0.1.0/tests/test_client.py +205 -0
eveses-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eveses
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Eveses SDK — activations, wallet, webhooks.
|
|
5
|
+
Project-URL: Homepage, https://eveses.com
|
|
6
|
+
Project-URL: Source, https://github.com/evesescom/python-sdk
|
|
7
|
+
Author: Eveses
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: activations,api,eveses,sdk,sms
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: requests>=2.28
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# eveses (Python SDK)
|
|
28
|
+
|
|
29
|
+
Official Python SDK for the [Eveses](https://eveses.com) developer API.
|
|
30
|
+
Activations, wallet, catalog (countries / services / pricing), and webhook signature verification.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install eveses
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.9+ and `requests`.
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
from eveses import Eveses
|
|
45
|
+
|
|
46
|
+
client = Eveses(api_key=os.environ["EVESES_API_KEY"])
|
|
47
|
+
|
|
48
|
+
order = client.activations.create(
|
|
49
|
+
country="ua",
|
|
50
|
+
service="telegram",
|
|
51
|
+
idempotency_key="my-uuid",
|
|
52
|
+
)
|
|
53
|
+
print(order.order_id, order.phone)
|
|
54
|
+
|
|
55
|
+
wallet = client.wallet.balance()
|
|
56
|
+
print(f"{wallet.available_balance / 100} {wallet.currency}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
Every request sends `Authorization: Bearer <api_key>`. Generate an API key from
|
|
62
|
+
your dashboard (`Settings → API keys`). The token is a Sanctum personal-access
|
|
63
|
+
token with `kind=api_key`.
|
|
64
|
+
|
|
65
|
+
## Activations
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
order = client.activations.create(
|
|
69
|
+
country="ua",
|
|
70
|
+
service="telegram",
|
|
71
|
+
mode="activation", # or "rent"
|
|
72
|
+
duration_minutes=60, # rent only
|
|
73
|
+
max_price_cents=100, # optional ceiling
|
|
74
|
+
idempotency_key="my-uuid", # also sent as Idempotency-Key header
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
fresh = client.activations.get(order.order_id)
|
|
78
|
+
sms = client.activations.sms(order.order_id)
|
|
79
|
+
# sms.stored — delivered to us via upstream webhook
|
|
80
|
+
# sms.fresh — pulled from upstream provider on demand
|
|
81
|
+
|
|
82
|
+
client.activations.cancel(order.order_id) # refund-where-supported
|
|
83
|
+
client.activations.finish(order.order_id) # mark consumed
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Catalog (countries / services / pricing)
|
|
87
|
+
|
|
88
|
+
Read-only metadata for driving order-creation UX. All three calls hit the
|
|
89
|
+
API-key-authenticated `/api/v1/numbers/*` routes, so the same Bearer token
|
|
90
|
+
that creates orders can populate selectors and price tables.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
countries = client.catalog.countries(mode="activation").countries
|
|
94
|
+
services = client.catalog.services(mode="activation", country="ua").services
|
|
95
|
+
pricing = client.catalog.pricing(mode="activation", country="ua", service="telegram")
|
|
96
|
+
# pricing.services[0].durations[0].price_cents → 50
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`mode` accepts ``"activation"`` or ``"rent"``. For rentals, pass
|
|
100
|
+
``duration_minutes=...`` to ``pricing(...)`` to filter to a single duration.
|
|
101
|
+
|
|
102
|
+
## Webhook verification
|
|
103
|
+
|
|
104
|
+
Eveses signs every outbound webhook delivery with HMAC-SHA256 over
|
|
105
|
+
`f"{timestamp}.{raw_body}"`. Two headers carry the proof:
|
|
106
|
+
|
|
107
|
+
- `X-Eveses-Signature` — e.g. `sha256=abc123…`
|
|
108
|
+
- `X-Eveses-Timestamp` — unix seconds
|
|
109
|
+
|
|
110
|
+
Pass the **raw** request body (bytes or str) — not the parsed JSON. Re-serialising
|
|
111
|
+
through `json.loads` / `json.dumps` reorders keys and breaks the signature.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Flask example
|
|
115
|
+
from flask import Flask, request
|
|
116
|
+
from eveses import Webhooks
|
|
117
|
+
|
|
118
|
+
app = Flask(__name__)
|
|
119
|
+
SECRET = os.environ["EVESES_WEBHOOK_SECRET"]
|
|
120
|
+
|
|
121
|
+
@app.post("/eveses-webhook")
|
|
122
|
+
def eveses_webhook():
|
|
123
|
+
raw = request.get_data() # bytes
|
|
124
|
+
if not Webhooks.verify(
|
|
125
|
+
raw,
|
|
126
|
+
request.headers.get("X-Eveses-Signature"),
|
|
127
|
+
SECRET,
|
|
128
|
+
timestamp=request.headers.get("X-Eveses-Timestamp"),
|
|
129
|
+
):
|
|
130
|
+
return "bad signature", 401
|
|
131
|
+
|
|
132
|
+
payload = request.get_json()
|
|
133
|
+
# handle payload["event"] / payload["data"] …
|
|
134
|
+
return "", 204
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A functional alias is also exported:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from eveses import verify_webhook
|
|
141
|
+
ok = verify_webhook(raw, sig_header, SECRET, timestamp=ts_header)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Errors
|
|
145
|
+
|
|
146
|
+
All non-2xx responses raise a typed subclass of `EvesesError`:
|
|
147
|
+
|
|
148
|
+
| Status | Class |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| 400 / 422 | `EvesesValidationError` (with `.errors`) |
|
|
151
|
+
| 401 | `EvesesAuthError` |
|
|
152
|
+
| 403 | `EvesesForbiddenError` |
|
|
153
|
+
| 404 | `EvesesNotFoundError` |
|
|
154
|
+
| 429 | `EvesesRateLimitError` (only after the 1 auto-retry is exhausted) |
|
|
155
|
+
| 5xx | `EvesesServerError` |
|
|
156
|
+
| other | `EvesesError` |
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from eveses import EvesesValidationError
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
client.activations.create(country="", service="")
|
|
163
|
+
except EvesesValidationError as e:
|
|
164
|
+
print(e.errors)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## API surface vs OpenAPI
|
|
168
|
+
|
|
169
|
+
The Eveses public OpenAPI spec exposes the customer-facing endpoints under
|
|
170
|
+
`/api/account/*` (legacy account scope) and `/api/v1/numbers/*` (new versioned
|
|
171
|
+
public API). For API-key consumers (`kind=api_key` Sanctum tokens), the v1
|
|
172
|
+
surface is currently a **thin wrapper** around the same controllers — orders
|
|
173
|
+
and wallet are still served from `/api/account/*`. This SDK targets the
|
|
174
|
+
account-scoped routes, which is where v1 reads & writes terminate today. When
|
|
175
|
+
v1 ships its own activations / wallet routes, you can override the base URL
|
|
176
|
+
without changing call sites; the response shapes are identical.
|
|
177
|
+
|
|
178
|
+
## Configuration
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
client = Eveses(
|
|
182
|
+
api_key="…",
|
|
183
|
+
base_url="https://api.eveses.com", # override per environment
|
|
184
|
+
timeout=30.0,
|
|
185
|
+
session=requests.Session(), # inject for tests / connection pooling
|
|
186
|
+
default_headers={"X-Trace-Id": "t1"},
|
|
187
|
+
user_agent="my-app/1.2.3",
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pip install -e '.[dev]'
|
|
195
|
+
python -m unittest discover -s tests
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
eveses-0.1.0/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# eveses (Python SDK)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Eveses](https://eveses.com) developer API.
|
|
4
|
+
Activations, wallet, catalog (countries / services / pricing), and webhook signature verification.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install eveses
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.9+ and `requests`.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import os
|
|
18
|
+
from eveses import Eveses
|
|
19
|
+
|
|
20
|
+
client = Eveses(api_key=os.environ["EVESES_API_KEY"])
|
|
21
|
+
|
|
22
|
+
order = client.activations.create(
|
|
23
|
+
country="ua",
|
|
24
|
+
service="telegram",
|
|
25
|
+
idempotency_key="my-uuid",
|
|
26
|
+
)
|
|
27
|
+
print(order.order_id, order.phone)
|
|
28
|
+
|
|
29
|
+
wallet = client.wallet.balance()
|
|
30
|
+
print(f"{wallet.available_balance / 100} {wallet.currency}")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Authentication
|
|
34
|
+
|
|
35
|
+
Every request sends `Authorization: Bearer <api_key>`. Generate an API key from
|
|
36
|
+
your dashboard (`Settings → API keys`). The token is a Sanctum personal-access
|
|
37
|
+
token with `kind=api_key`.
|
|
38
|
+
|
|
39
|
+
## Activations
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
order = client.activations.create(
|
|
43
|
+
country="ua",
|
|
44
|
+
service="telegram",
|
|
45
|
+
mode="activation", # or "rent"
|
|
46
|
+
duration_minutes=60, # rent only
|
|
47
|
+
max_price_cents=100, # optional ceiling
|
|
48
|
+
idempotency_key="my-uuid", # also sent as Idempotency-Key header
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
fresh = client.activations.get(order.order_id)
|
|
52
|
+
sms = client.activations.sms(order.order_id)
|
|
53
|
+
# sms.stored — delivered to us via upstream webhook
|
|
54
|
+
# sms.fresh — pulled from upstream provider on demand
|
|
55
|
+
|
|
56
|
+
client.activations.cancel(order.order_id) # refund-where-supported
|
|
57
|
+
client.activations.finish(order.order_id) # mark consumed
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Catalog (countries / services / pricing)
|
|
61
|
+
|
|
62
|
+
Read-only metadata for driving order-creation UX. All three calls hit the
|
|
63
|
+
API-key-authenticated `/api/v1/numbers/*` routes, so the same Bearer token
|
|
64
|
+
that creates orders can populate selectors and price tables.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
countries = client.catalog.countries(mode="activation").countries
|
|
68
|
+
services = client.catalog.services(mode="activation", country="ua").services
|
|
69
|
+
pricing = client.catalog.pricing(mode="activation", country="ua", service="telegram")
|
|
70
|
+
# pricing.services[0].durations[0].price_cents → 50
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`mode` accepts ``"activation"`` or ``"rent"``. For rentals, pass
|
|
74
|
+
``duration_minutes=...`` to ``pricing(...)`` to filter to a single duration.
|
|
75
|
+
|
|
76
|
+
## Webhook verification
|
|
77
|
+
|
|
78
|
+
Eveses signs every outbound webhook delivery with HMAC-SHA256 over
|
|
79
|
+
`f"{timestamp}.{raw_body}"`. Two headers carry the proof:
|
|
80
|
+
|
|
81
|
+
- `X-Eveses-Signature` — e.g. `sha256=abc123…`
|
|
82
|
+
- `X-Eveses-Timestamp` — unix seconds
|
|
83
|
+
|
|
84
|
+
Pass the **raw** request body (bytes or str) — not the parsed JSON. Re-serialising
|
|
85
|
+
through `json.loads` / `json.dumps` reorders keys and breaks the signature.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# Flask example
|
|
89
|
+
from flask import Flask, request
|
|
90
|
+
from eveses import Webhooks
|
|
91
|
+
|
|
92
|
+
app = Flask(__name__)
|
|
93
|
+
SECRET = os.environ["EVESES_WEBHOOK_SECRET"]
|
|
94
|
+
|
|
95
|
+
@app.post("/eveses-webhook")
|
|
96
|
+
def eveses_webhook():
|
|
97
|
+
raw = request.get_data() # bytes
|
|
98
|
+
if not Webhooks.verify(
|
|
99
|
+
raw,
|
|
100
|
+
request.headers.get("X-Eveses-Signature"),
|
|
101
|
+
SECRET,
|
|
102
|
+
timestamp=request.headers.get("X-Eveses-Timestamp"),
|
|
103
|
+
):
|
|
104
|
+
return "bad signature", 401
|
|
105
|
+
|
|
106
|
+
payload = request.get_json()
|
|
107
|
+
# handle payload["event"] / payload["data"] …
|
|
108
|
+
return "", 204
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A functional alias is also exported:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from eveses import verify_webhook
|
|
115
|
+
ok = verify_webhook(raw, sig_header, SECRET, timestamp=ts_header)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Errors
|
|
119
|
+
|
|
120
|
+
All non-2xx responses raise a typed subclass of `EvesesError`:
|
|
121
|
+
|
|
122
|
+
| Status | Class |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| 400 / 422 | `EvesesValidationError` (with `.errors`) |
|
|
125
|
+
| 401 | `EvesesAuthError` |
|
|
126
|
+
| 403 | `EvesesForbiddenError` |
|
|
127
|
+
| 404 | `EvesesNotFoundError` |
|
|
128
|
+
| 429 | `EvesesRateLimitError` (only after the 1 auto-retry is exhausted) |
|
|
129
|
+
| 5xx | `EvesesServerError` |
|
|
130
|
+
| other | `EvesesError` |
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from eveses import EvesesValidationError
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
client.activations.create(country="", service="")
|
|
137
|
+
except EvesesValidationError as e:
|
|
138
|
+
print(e.errors)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## API surface vs OpenAPI
|
|
142
|
+
|
|
143
|
+
The Eveses public OpenAPI spec exposes the customer-facing endpoints under
|
|
144
|
+
`/api/account/*` (legacy account scope) and `/api/v1/numbers/*` (new versioned
|
|
145
|
+
public API). For API-key consumers (`kind=api_key` Sanctum tokens), the v1
|
|
146
|
+
surface is currently a **thin wrapper** around the same controllers — orders
|
|
147
|
+
and wallet are still served from `/api/account/*`. This SDK targets the
|
|
148
|
+
account-scoped routes, which is where v1 reads & writes terminate today. When
|
|
149
|
+
v1 ships its own activations / wallet routes, you can override the base URL
|
|
150
|
+
without changing call sites; the response shapes are identical.
|
|
151
|
+
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
client = Eveses(
|
|
156
|
+
api_key="…",
|
|
157
|
+
base_url="https://api.eveses.com", # override per environment
|
|
158
|
+
timeout=30.0,
|
|
159
|
+
session=requests.Session(), # inject for tests / connection pooling
|
|
160
|
+
default_headers={"X-Trace-Id": "t1"},
|
|
161
|
+
user_agent="my-app/1.2.3",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install -e '.[dev]'
|
|
169
|
+
python -m unittest discover -s tests
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eveses — Official Python SDK.
|
|
3
|
+
|
|
4
|
+
Quickstart:
|
|
5
|
+
|
|
6
|
+
from eveses import Eveses
|
|
7
|
+
client = Eveses(api_key="sk_…")
|
|
8
|
+
order = client.activations.create(country="ua", service="telegram")
|
|
9
|
+
wallet = client.wallet.balance()
|
|
10
|
+
services = client.catalog.services(mode="activation", country="ua")
|
|
11
|
+
|
|
12
|
+
Webhook verification:
|
|
13
|
+
|
|
14
|
+
from eveses import Webhooks
|
|
15
|
+
ok = Webhooks.verify(raw_body, signature_header, secret, timestamp=ts_header)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .activations import Activations, Order, OrderSms, OrderSmsBundle
|
|
19
|
+
from .catalog import (
|
|
20
|
+
Catalog,
|
|
21
|
+
CatalogCountriesResponse,
|
|
22
|
+
CatalogPricingDuration,
|
|
23
|
+
CatalogPricingResponse,
|
|
24
|
+
CatalogServiceWithDurations,
|
|
25
|
+
CatalogServicesResponse,
|
|
26
|
+
)
|
|
27
|
+
from .client import Eveses
|
|
28
|
+
from .exceptions import (
|
|
29
|
+
EvesesAuthError,
|
|
30
|
+
EvesesError,
|
|
31
|
+
EvesesForbiddenError,
|
|
32
|
+
EvesesNotFoundError,
|
|
33
|
+
EvesesRateLimitError,
|
|
34
|
+
EvesesServerError,
|
|
35
|
+
EvesesValidationError,
|
|
36
|
+
)
|
|
37
|
+
from .wallet import Wallet, WalletBalance
|
|
38
|
+
from .webhooks import Webhooks, verify_webhook
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Eveses",
|
|
44
|
+
"Activations",
|
|
45
|
+
"Catalog",
|
|
46
|
+
"Wallet",
|
|
47
|
+
"Webhooks",
|
|
48
|
+
"verify_webhook",
|
|
49
|
+
"Order",
|
|
50
|
+
"OrderSms",
|
|
51
|
+
"OrderSmsBundle",
|
|
52
|
+
"WalletBalance",
|
|
53
|
+
"CatalogCountriesResponse",
|
|
54
|
+
"CatalogServicesResponse",
|
|
55
|
+
"CatalogPricingResponse",
|
|
56
|
+
"CatalogServiceWithDurations",
|
|
57
|
+
"CatalogPricingDuration",
|
|
58
|
+
"EvesesError",
|
|
59
|
+
"EvesesAuthError",
|
|
60
|
+
"EvesesForbiddenError",
|
|
61
|
+
"EvesesNotFoundError",
|
|
62
|
+
"EvesesValidationError",
|
|
63
|
+
"EvesesRateLimitError",
|
|
64
|
+
"EvesesServerError",
|
|
65
|
+
"__version__",
|
|
66
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Activations / orders namespace.
|
|
3
|
+
|
|
4
|
+
Note: the public OpenAPI spec exposes orders under `/api/account/orders/*`.
|
|
5
|
+
There is no dedicated `/api/v1/activations` route today; for API-key
|
|
6
|
+
consumers (kind=api_key Sanctum tokens), v1 is a thin wrapper around the
|
|
7
|
+
account-scoped controllers. This module hits the account-scoped paths.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
from .client import Eveses
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Order:
|
|
21
|
+
order_id: str
|
|
22
|
+
status: str
|
|
23
|
+
phone: Optional[str] = None
|
|
24
|
+
country: Optional[str] = None
|
|
25
|
+
service: Optional[str] = None
|
|
26
|
+
mode: Optional[str] = None
|
|
27
|
+
price_cents: Optional[int] = None
|
|
28
|
+
expires_at: Optional[str] = None
|
|
29
|
+
created_at: Optional[str] = None
|
|
30
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class OrderSms:
|
|
35
|
+
id: int
|
|
36
|
+
text: str
|
|
37
|
+
sender: Optional[str] = None
|
|
38
|
+
received_at: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class OrderSmsBundle:
|
|
43
|
+
order_id: str
|
|
44
|
+
stored: List[OrderSms]
|
|
45
|
+
fresh: List[OrderSms]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Activations:
|
|
49
|
+
"""Wrapper around `/api/account/orders/*`."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, client: "Eveses") -> None:
|
|
52
|
+
self._client = client
|
|
53
|
+
|
|
54
|
+
def create(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
country: str,
|
|
58
|
+
service: str,
|
|
59
|
+
mode: str = "activation",
|
|
60
|
+
duration_minutes: Optional[int] = None,
|
|
61
|
+
idempotency_key: Optional[str] = None,
|
|
62
|
+
max_price_cents: Optional[int] = None,
|
|
63
|
+
) -> Order:
|
|
64
|
+
"""Provision a number for a country/service. Returns the created order."""
|
|
65
|
+
body: Dict[str, Any] = {"mode": mode, "country": country, "service": service}
|
|
66
|
+
if duration_minutes is not None:
|
|
67
|
+
body["duration_minutes"] = duration_minutes
|
|
68
|
+
if idempotency_key is not None:
|
|
69
|
+
body["idempotency_key"] = idempotency_key
|
|
70
|
+
if max_price_cents is not None:
|
|
71
|
+
body["max_price_cents"] = max_price_cents
|
|
72
|
+
|
|
73
|
+
headers: Dict[str, str] = {}
|
|
74
|
+
if idempotency_key:
|
|
75
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
76
|
+
|
|
77
|
+
res = self._client.request(
|
|
78
|
+
"POST",
|
|
79
|
+
"/api/account/orders",
|
|
80
|
+
json_body=body,
|
|
81
|
+
headers=headers,
|
|
82
|
+
)
|
|
83
|
+
return _map_order(_unwrap(res))
|
|
84
|
+
|
|
85
|
+
def get(self, order_id: str) -> Order:
|
|
86
|
+
res = self._client.request("GET", f"/api/account/orders/{_quote(order_id)}")
|
|
87
|
+
return _map_order(_unwrap(res))
|
|
88
|
+
|
|
89
|
+
def cancel(self, order_id: str) -> Order:
|
|
90
|
+
"""Release the number and refund the user (where supported)."""
|
|
91
|
+
res = self._client.request("POST", f"/api/account/orders/{_quote(order_id)}/cancel")
|
|
92
|
+
return _map_order(_unwrap(res))
|
|
93
|
+
|
|
94
|
+
def finish(self, order_id: str) -> Order:
|
|
95
|
+
"""Mark the order completed once the SMS has been consumed."""
|
|
96
|
+
res = self._client.request("POST", f"/api/account/orders/{_quote(order_id)}/finish")
|
|
97
|
+
return _map_order(_unwrap(res))
|
|
98
|
+
|
|
99
|
+
def sms(self, order_id: str) -> OrderSmsBundle:
|
|
100
|
+
"""
|
|
101
|
+
Get all SMS messages for an order. Combines `stored` (delivered to us
|
|
102
|
+
via webhook) with `fresh` (pulled from the upstream provider on demand).
|
|
103
|
+
"""
|
|
104
|
+
res = self._client.request("GET", f"/api/account/orders/{_quote(order_id)}/sms")
|
|
105
|
+
data = _unwrap(res)
|
|
106
|
+
return OrderSmsBundle(
|
|
107
|
+
order_id=str(data.get("order_id") or order_id),
|
|
108
|
+
stored=[_map_sms(m) for m in (data.get("stored") or [])],
|
|
109
|
+
fresh=[_map_sms(m) for m in (data.get("fresh") or [])],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --------------------------------------------------------------- internals --
|
|
114
|
+
def _unwrap(payload: Any) -> Dict[str, Any]:
|
|
115
|
+
if isinstance(payload, dict) and isinstance(payload.get("data"), dict):
|
|
116
|
+
return payload["data"]
|
|
117
|
+
if isinstance(payload, dict):
|
|
118
|
+
return payload
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _map_order(d: Dict[str, Any]) -> Order:
|
|
123
|
+
return Order(
|
|
124
|
+
order_id=str(d.get("order_id") or ""),
|
|
125
|
+
status=str(d.get("status") or "pending"),
|
|
126
|
+
phone=_str_or_none(d.get("phone")),
|
|
127
|
+
country=_str_or_none(d.get("country")),
|
|
128
|
+
service=_str_or_none(d.get("service")),
|
|
129
|
+
mode=_str_or_none(d.get("mode")),
|
|
130
|
+
price_cents=_int_or_none(d.get("price_cents")),
|
|
131
|
+
expires_at=_str_or_none(d.get("expires_at")),
|
|
132
|
+
created_at=_str_or_none(d.get("created_at")),
|
|
133
|
+
raw=dict(d),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _map_sms(m: Dict[str, Any]) -> OrderSms:
|
|
138
|
+
return OrderSms(
|
|
139
|
+
id=int(m.get("id") or 0),
|
|
140
|
+
text=str(m.get("text") or ""),
|
|
141
|
+
sender=_str_or_none(m.get("sender")),
|
|
142
|
+
received_at=_str_or_none(m.get("received_at")),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _str_or_none(v: Any) -> Optional[str]:
|
|
147
|
+
return v if isinstance(v, str) else None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _int_or_none(v: Any) -> Optional[int]:
|
|
151
|
+
return v if isinstance(v, int) else None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _quote(value: str) -> str:
|
|
155
|
+
from urllib.parse import quote
|
|
156
|
+
|
|
157
|
+
return quote(value, safe="")
|