voicetel-api 2.2.10__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 (36) hide show
  1. voicetel_api-2.2.10/LICENSE +21 -0
  2. voicetel_api-2.2.10/PKG-INFO +300 -0
  3. voicetel_api-2.2.10/README.md +265 -0
  4. voicetel_api-2.2.10/pyproject.toml +86 -0
  5. voicetel_api-2.2.10/src/voicetel/__init__.py +46 -0
  6. voicetel_api-2.2.10/src/voicetel/_http.py +265 -0
  7. voicetel_api-2.2.10/src/voicetel/_version.py +1 -0
  8. voicetel_api-2.2.10/src/voicetel/auth.py +57 -0
  9. voicetel_api-2.2.10/src/voicetel/client.py +154 -0
  10. voicetel_api-2.2.10/src/voicetel/exceptions.py +88 -0
  11. voicetel_api-2.2.10/src/voicetel/models/__init__.py +352 -0
  12. voicetel_api-2.2.10/src/voicetel/models/_base.py +37 -0
  13. voicetel_api-2.2.10/src/voicetel/models/account.py +254 -0
  14. voicetel_api-2.2.10/src/voicetel/models/acl.py +66 -0
  15. voicetel_api-2.2.10/src/voicetel/models/authentication.py +69 -0
  16. voicetel_api-2.2.10/src/voicetel/models/common.py +34 -0
  17. voicetel_api-2.2.10/src/voicetel/models/e911.py +97 -0
  18. voicetel_api-2.2.10/src/voicetel/models/gateways.py +84 -0
  19. voicetel_api-2.2.10/src/voicetel/models/inumbering.py +241 -0
  20. voicetel_api-2.2.10/src/voicetel/models/lookups.py +43 -0
  21. voicetel_api-2.2.10/src/voicetel/models/messaging.py +164 -0
  22. voicetel_api-2.2.10/src/voicetel/models/numbers.py +327 -0
  23. voicetel_api-2.2.10/src/voicetel/models/support.py +265 -0
  24. voicetel_api-2.2.10/src/voicetel/py.typed +0 -0
  25. voicetel_api-2.2.10/src/voicetel/resources/__init__.py +38 -0
  26. voicetel_api-2.2.10/src/voicetel/resources/_base.py +29 -0
  27. voicetel_api-2.2.10/src/voicetel/resources/account.py +148 -0
  28. voicetel_api-2.2.10/src/voicetel/resources/acl.py +36 -0
  29. voicetel_api-2.2.10/src/voicetel/resources/authentication.py +26 -0
  30. voicetel_api-2.2.10/src/voicetel/resources/e911.py +72 -0
  31. voicetel_api-2.2.10/src/voicetel/resources/gateways.py +77 -0
  32. voicetel_api-2.2.10/src/voicetel/resources/inumbering.py +152 -0
  33. voicetel_api-2.2.10/src/voicetel/resources/lookups.py +29 -0
  34. voicetel_api-2.2.10/src/voicetel/resources/messaging.py +132 -0
  35. voicetel_api-2.2.10/src/voicetel/resources/numbers.py +394 -0
  36. voicetel_api-2.2.10/src/voicetel/resources/support.py +108 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VoiceTel Communications
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.
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: voicetel-api
3
+ Version: 2.2.10
4
+ Summary: Official Python SDK for the VoiceTel REST API
5
+ Project-URL: Homepage, https://github.com/voicetel/python-sdk
6
+ Project-URL: Documentation, https://voicetel.com/docs/api/v2.2/
7
+ Project-URL: Repository, https://github.com/voicetel/python-sdk
8
+ Project-URL: Issues, https://github.com/voicetel/python-sdk/issues
9
+ Author-email: VoiceTel <support@voicetel.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: e911,sip,sms,telephony,voicetel,voip
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Communications :: Telephony
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: pydantic>=2.6
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.10; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: respx>=0.21; extra == 'dev'
33
+ Requires-Dist: ruff>=0.5; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # 📞 VoiceTel Python SDK
37
+
38
+ The official Python client for the [VoiceTel REST API](https://voicetel.com/docs/api/v2.2/) — provision numbers, place orders, validate e911, send messages, and manage your account, all with type-safe, async-ready Python.
39
+
40
+ ![Version](https://img.shields.io/badge/version-2.2.10-blue)
41
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
42
+ ![License](https://img.shields.io/badge/license-MIT-green)
43
+ ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
44
+ ![Typed](https://img.shields.io/badge/typed-pydantic--v2-blue)
45
+
46
+ ## 📚 Table of Contents
47
+
48
+ - [Features](#-features)
49
+ - [Installation](#-installation)
50
+ - [Quickstart](#-quickstart)
51
+ - [Authentication](#-authentication)
52
+ - [Resource Reference](#-resource-reference)
53
+ - [Error Handling](#-error-handling)
54
+ - [Async Support](#-async-support)
55
+ - [Rate Limits](#-rate-limits)
56
+ - [Development](#-development)
57
+ - [API Documentation](#-api-documentation)
58
+ - [Contributors](#-contributors)
59
+ - [Sponsors](#-sponsors)
60
+ - [License](#-license)
61
+
62
+ ## ✨ Features
63
+
64
+ ### 🛡️ Strongly Typed End-to-End
65
+ - **Pydantic v2 models** for every one of the 73 API operations — request bodies validated before they leave your machine, responses validated when they arrive.
66
+ - **mypy-strict clean.** Full type coverage, including async, generics, and discriminated unions.
67
+ - **Autocomplete everywhere.** Your IDE knows the shape of every field — no more guessing what's in `result["data"]["numbers"]`.
68
+
69
+ ### ⚡ Sync + Async, Same Surface
70
+ - `Client` for blocking calls, `AsyncClient` for `await`-based async — identical method names, identical return types.
71
+ - Built on `httpx` — supports HTTP/2, connection pooling, and custom transports if you need them.
72
+
73
+ ### 🔁 Production-Grade Transport
74
+ - **Automatic retry** with exponential backoff on 429 / 5xx — honors `Retry-After` headers.
75
+ - **Configurable timeouts** per client or per call.
76
+ - **Bearer auth** managed for you; password→key exchange handled by `client.login()`.
77
+ - **Structured exception hierarchy** — `RateLimitError`, `AuthenticationError`, `NotFoundError`, etc. all subclasses of `ApiError` you can catch broadly or narrowly.
78
+
79
+ ### 📞 Complete API Coverage
80
+ - **Numbers** — list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
81
+ - **Account** — profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
82
+ - **e911** — record provisioning, address validation, lookup, removal.
83
+ - **Gateways** — list, create, update, delete, view bound numbers.
84
+ - **Messaging** — SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
85
+ - **Lookups** — CNAM and LRN dips.
86
+ - **iNumbering** — inventory search, coverage queries, number orders, port-in submissions, port-out availability checks.
87
+ - **Support** — ticket create / read / update / delete, threaded messages, replies.
88
+ - **ACL** — IP allowlist management with structured 409 conflict bodies.
89
+ - **Authentication** — switch between Digest, IP-only, or hybrid modes; rotate passwords.
90
+
91
+ ### 🧪 Battle-Tested
92
+ - **108 unit tests** at **100% statement + branch coverage.**
93
+ - **Integration test suite** that runs read-only operations against `api.voicetel.com` — gated by env vars, safe for CI.
94
+ - **No mocks-pretending-to-be-tests.** Mocked HTTP layer with `respx`, real Pydantic validation on every fixture so spec drift gets caught.
95
+
96
+ ### 📦 Clean Distribution
97
+ - Zero codegen footprint — every byte hand-written.
98
+ - Built with `hatchling`; ships as wheel + sdist.
99
+ - `py.typed` marker — downstream type checkers see your imports natively.
100
+
101
+ ## 🚀 Installation
102
+
103
+ ```bash
104
+ pip install voicetel-api
105
+ ```
106
+
107
+ Requires Python 3.10 or later. Tested against 3.10, 3.11, 3.12, and 3.13.
108
+
109
+ ## 🏁 Quickstart
110
+
111
+ ```python
112
+ from voicetel import Client
113
+
114
+ with Client() as c:
115
+ # Exchange username + password for an API key (one-time per session)
116
+ c.login(username=1000000001, password="hunter2")
117
+
118
+ # Typed responses — your IDE knows what `me` is.
119
+ me = c.account.get()
120
+ print(f"Balance: ${me.cash:.2f} | Caller ID: {me.callerId}")
121
+
122
+ # List your numbers
123
+ for n in c.numbers.list().numbers:
124
+ print(f"{n.number} route={n.route} cnam={n.cnam} sms={n.smsEnabled}")
125
+ ```
126
+
127
+ Or, if you already have an API key:
128
+
129
+ ```python
130
+ from voicetel import Client
131
+
132
+ with Client(api_key="32hex...") as c:
133
+ coverage = c.inumbering.coverage(state="NJ")
134
+ for bucket in coverage.coverage:
135
+ print(f"{bucket.npa}-{bucket.nxx}: {bucket.count} TNs available")
136
+ ```
137
+
138
+ ## 🔑 Authentication
139
+
140
+ Every endpoint requires `Authorization: Bearer <apikey>` **except** `POST /v2.2/account/api-key`, which exchanges username + password for a fresh key. `Client.login()` (and `AsyncClient.login()`) handles the exchange and installs the returned key on the transport.
141
+
142
+ Re-fetch the API key after any password change — the old one is invalidated.
143
+
144
+ > Don't have credentials yet? Get them at **[voicetel.com/docs/api/v2.2/credentials](https://voicetel.com/docs/api/v2.2/credentials/)**.
145
+
146
+ ```python
147
+ from voicetel import Client
148
+
149
+ with Client() as c:
150
+ key = c.login(username=1000000001, password="hunter2")
151
+ # `key` is the new 32-hex bearer; the client already has it installed.
152
+ ```
153
+
154
+ ## 🗺️ Resource Reference
155
+
156
+ | Resource | Operations | Example |
157
+ |---|---|---|
158
+ | `client.account` | Profile, CDR, credits, payments, MRC, signup, recovery, sub-accounts | `c.account.cdr(start=t1, end=t2)` |
159
+ | `client.acl` | IP allowlist (CIDR entries) | `c.acl.add(AclModifyRequest(acl=[...]))` |
160
+ | `client.authentication` | SIP/HTTP auth mode + password | `c.authentication.update(AuthPutRequest(authType=1))` |
161
+ | `client.e911` | Records, address validation, provisioning | `c.e911.validate(E911AddressRequest(...))` |
162
+ | `client.gateways` | Termination routes | `c.gateways.list()` |
163
+ | `client.inumbering` | Inventory, orders, port-ins | `c.inumbering.search_inventory(npa=201)` |
164
+ | `client.lookups` | CNAM & LRN dips | `c.lookups.lrn("2015551234", ani="2012548000")` |
165
+ | `client.messaging` | SMS/MMS, 10DLC brands & campaigns | `c.messaging.send(MessageSendRequest(...))` |
166
+ | `client.numbers` | All operations on TNs on the account | `c.numbers.assign_campaign("2015551234", ...)` |
167
+ | `client.support` | Tickets, replies, attachments | `c.support.create(TicketCreateRequest(...))` |
168
+
169
+ Every method that takes a request body accepts a typed Pydantic model imported from `voicetel.models`:
170
+
171
+ ```python
172
+ from voicetel import Client
173
+ from voicetel.models import (
174
+ MessageSendRequest,
175
+ NumberCampaignAssignRequest,
176
+ PortSubmitRequest,
177
+ )
178
+
179
+ with Client(api_key=key) as c:
180
+ sent = c.messaging.send(MessageSendRequest(
181
+ fromNumber="2012548000",
182
+ toNumber="2015551234",
183
+ text="Your code is 482917",
184
+ ))
185
+ print(f"Sent: {sent.id} ({sent.parts} segment(s))")
186
+
187
+ c.numbers.assign_campaign(
188
+ "2015551234",
189
+ NumberCampaignAssignRequest(campaignId="CABC123"),
190
+ )
191
+ ```
192
+
193
+ ## 🚨 Error Handling
194
+
195
+ All HTTP errors raise subclasses of `voicetel.ApiError`. Catch broadly or narrowly:
196
+
197
+ | Status | Exception |
198
+ |--------|------------------------|
199
+ | 400 | `BadRequestError` |
200
+ | 401 | `AuthenticationError` |
201
+ | 403 | `PermissionDeniedError`|
202
+ | 404 | `NotFoundError` |
203
+ | 409 | `ConflictError` |
204
+ | 429 | `RateLimitError` |
205
+ | 5xx | `ServerError` |
206
+ | other | `ApiError` |
207
+
208
+ ```python
209
+ from voicetel import Client, NotFoundError, RateLimitError
210
+
211
+ with Client(api_key=key) as c:
212
+ try:
213
+ n = c.numbers.get("9999999999")
214
+ except NotFoundError:
215
+ print("That number isn't on your account.")
216
+ except RateLimitError as e:
217
+ print(f"Slow down — retry in {e.body.get('retryAfter', '?')}s")
218
+ ```
219
+
220
+ ## ⚡ Async Support
221
+
222
+ Identical surface to `Client`, with `await`-based methods:
223
+
224
+ ```python
225
+ import asyncio
226
+ from voicetel import AsyncClient
227
+
228
+ async def fetch_state(numbers: list[str]) -> None:
229
+ async with AsyncClient(api_key="...") as c:
230
+ state = await c.messaging.numbers_state(numbers=numbers)
231
+ for s in state.numbers:
232
+ print(f"{s.number}: network={s.network} campaign={s.campaign}")
233
+
234
+ asyncio.run(fetch_state(["2015551234", "2015551235"]))
235
+ ```
236
+
237
+ ## ⏱️ Rate Limits
238
+
239
+ These endpoints are limited to **6 requests per hour per IP**:
240
+
241
+ - `account/info`
242
+ - `account/mrc` (`client.account.recurring_charges()`)
243
+ - `account/cdr` (`client.account.cdr()`)
244
+ - `account/api-key` (`client.login()`)
245
+
246
+ The SDK automatically retries 429 responses with `Retry-After` honored, up to `max_retries` (default `2`). To bump it:
247
+
248
+ ```python
249
+ Client(api_key=key, max_retries=4, timeout=60.0)
250
+ ```
251
+
252
+ ## 🛠️ Development
253
+
254
+ ```bash
255
+ git clone https://github.com/voicetel/python-sdk
256
+ cd python-sdk
257
+ python -m venv .venv && source .venv/bin/activate
258
+ pip install -e ".[dev]"
259
+
260
+ # Unit tests (fast, no network)
261
+ pytest tests/unit
262
+
263
+ # 100% coverage gate
264
+ pytest tests/unit --cov --cov-fail-under=100
265
+
266
+ # Lint + type-check
267
+ ruff check src tests
268
+ mypy src
269
+
270
+ # Integration tests (live api.voicetel.com, read-only)
271
+ cp .env.example .env # fill in VOICETEL_USERNAME / VOICETEL_PASSWORD
272
+ pytest tests/integration
273
+
274
+ # Build wheel + sdist
275
+ python -m build
276
+ twine check dist/*
277
+ ```
278
+
279
+ ## 📖 API Documentation
280
+
281
+ - **Reference docs:** [voicetel.com/docs/api/v2.2/](https://voicetel.com/docs/api/v2.2/)
282
+ - **Interactive playground:** [voicetel.com/docs/api/v2.2/playground/](https://voicetel.com/docs/api/v2.2/playground/) — try the API in your browser without writing any code
283
+ - **API credentials:** [voicetel.com/docs/api/v2.2/credentials/](https://voicetel.com/docs/api/v2.2/credentials/)
284
+ - **Type definitions:** see the `voicetel.models` module — every wire shape has a Pydantic model.
285
+
286
+ ## 🙌 Contributors
287
+
288
+ - [Michael Mavroudis](https://github.com/mavroudis) — Lead Developer
289
+
290
+ Contributions welcome. Open an issue describing the change you want to make, or send a pull request against `main`.
291
+
292
+ ## 💖 Sponsors
293
+
294
+ | Sponsor | Contribution |
295
+ |---------|--------------|
296
+ | [VoiceTel Communications](https://voicetel.com) | Primary development and production hosting |
297
+
298
+ ## 📄 License
299
+
300
+ This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,265 @@
1
+ # 📞 VoiceTel Python SDK
2
+
3
+ The official Python client for the [VoiceTel REST API](https://voicetel.com/docs/api/v2.2/) — provision numbers, place orders, validate e911, send messages, and manage your account, all with type-safe, async-ready Python.
4
+
5
+ ![Version](https://img.shields.io/badge/version-2.2.10-blue)
6
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
7
+ ![License](https://img.shields.io/badge/license-MIT-green)
8
+ ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
9
+ ![Typed](https://img.shields.io/badge/typed-pydantic--v2-blue)
10
+
11
+ ## 📚 Table of Contents
12
+
13
+ - [Features](#-features)
14
+ - [Installation](#-installation)
15
+ - [Quickstart](#-quickstart)
16
+ - [Authentication](#-authentication)
17
+ - [Resource Reference](#-resource-reference)
18
+ - [Error Handling](#-error-handling)
19
+ - [Async Support](#-async-support)
20
+ - [Rate Limits](#-rate-limits)
21
+ - [Development](#-development)
22
+ - [API Documentation](#-api-documentation)
23
+ - [Contributors](#-contributors)
24
+ - [Sponsors](#-sponsors)
25
+ - [License](#-license)
26
+
27
+ ## ✨ Features
28
+
29
+ ### 🛡️ Strongly Typed End-to-End
30
+ - **Pydantic v2 models** for every one of the 73 API operations — request bodies validated before they leave your machine, responses validated when they arrive.
31
+ - **mypy-strict clean.** Full type coverage, including async, generics, and discriminated unions.
32
+ - **Autocomplete everywhere.** Your IDE knows the shape of every field — no more guessing what's in `result["data"]["numbers"]`.
33
+
34
+ ### ⚡ Sync + Async, Same Surface
35
+ - `Client` for blocking calls, `AsyncClient` for `await`-based async — identical method names, identical return types.
36
+ - Built on `httpx` — supports HTTP/2, connection pooling, and custom transports if you need them.
37
+
38
+ ### 🔁 Production-Grade Transport
39
+ - **Automatic retry** with exponential backoff on 429 / 5xx — honors `Retry-After` headers.
40
+ - **Configurable timeouts** per client or per call.
41
+ - **Bearer auth** managed for you; password→key exchange handled by `client.login()`.
42
+ - **Structured exception hierarchy** — `RateLimitError`, `AuthenticationError`, `NotFoundError`, etc. all subclasses of `ApiError` you can catch broadly or narrowly.
43
+
44
+ ### 📞 Complete API Coverage
45
+ - **Numbers** — list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
46
+ - **Account** — profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
47
+ - **e911** — record provisioning, address validation, lookup, removal.
48
+ - **Gateways** — list, create, update, delete, view bound numbers.
49
+ - **Messaging** — SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
50
+ - **Lookups** — CNAM and LRN dips.
51
+ - **iNumbering** — inventory search, coverage queries, number orders, port-in submissions, port-out availability checks.
52
+ - **Support** — ticket create / read / update / delete, threaded messages, replies.
53
+ - **ACL** — IP allowlist management with structured 409 conflict bodies.
54
+ - **Authentication** — switch between Digest, IP-only, or hybrid modes; rotate passwords.
55
+
56
+ ### 🧪 Battle-Tested
57
+ - **108 unit tests** at **100% statement + branch coverage.**
58
+ - **Integration test suite** that runs read-only operations against `api.voicetel.com` — gated by env vars, safe for CI.
59
+ - **No mocks-pretending-to-be-tests.** Mocked HTTP layer with `respx`, real Pydantic validation on every fixture so spec drift gets caught.
60
+
61
+ ### 📦 Clean Distribution
62
+ - Zero codegen footprint — every byte hand-written.
63
+ - Built with `hatchling`; ships as wheel + sdist.
64
+ - `py.typed` marker — downstream type checkers see your imports natively.
65
+
66
+ ## 🚀 Installation
67
+
68
+ ```bash
69
+ pip install voicetel-api
70
+ ```
71
+
72
+ Requires Python 3.10 or later. Tested against 3.10, 3.11, 3.12, and 3.13.
73
+
74
+ ## 🏁 Quickstart
75
+
76
+ ```python
77
+ from voicetel import Client
78
+
79
+ with Client() as c:
80
+ # Exchange username + password for an API key (one-time per session)
81
+ c.login(username=1000000001, password="hunter2")
82
+
83
+ # Typed responses — your IDE knows what `me` is.
84
+ me = c.account.get()
85
+ print(f"Balance: ${me.cash:.2f} | Caller ID: {me.callerId}")
86
+
87
+ # List your numbers
88
+ for n in c.numbers.list().numbers:
89
+ print(f"{n.number} route={n.route} cnam={n.cnam} sms={n.smsEnabled}")
90
+ ```
91
+
92
+ Or, if you already have an API key:
93
+
94
+ ```python
95
+ from voicetel import Client
96
+
97
+ with Client(api_key="32hex...") as c:
98
+ coverage = c.inumbering.coverage(state="NJ")
99
+ for bucket in coverage.coverage:
100
+ print(f"{bucket.npa}-{bucket.nxx}: {bucket.count} TNs available")
101
+ ```
102
+
103
+ ## 🔑 Authentication
104
+
105
+ Every endpoint requires `Authorization: Bearer <apikey>` **except** `POST /v2.2/account/api-key`, which exchanges username + password for a fresh key. `Client.login()` (and `AsyncClient.login()`) handles the exchange and installs the returned key on the transport.
106
+
107
+ Re-fetch the API key after any password change — the old one is invalidated.
108
+
109
+ > Don't have credentials yet? Get them at **[voicetel.com/docs/api/v2.2/credentials](https://voicetel.com/docs/api/v2.2/credentials/)**.
110
+
111
+ ```python
112
+ from voicetel import Client
113
+
114
+ with Client() as c:
115
+ key = c.login(username=1000000001, password="hunter2")
116
+ # `key` is the new 32-hex bearer; the client already has it installed.
117
+ ```
118
+
119
+ ## 🗺️ Resource Reference
120
+
121
+ | Resource | Operations | Example |
122
+ |---|---|---|
123
+ | `client.account` | Profile, CDR, credits, payments, MRC, signup, recovery, sub-accounts | `c.account.cdr(start=t1, end=t2)` |
124
+ | `client.acl` | IP allowlist (CIDR entries) | `c.acl.add(AclModifyRequest(acl=[...]))` |
125
+ | `client.authentication` | SIP/HTTP auth mode + password | `c.authentication.update(AuthPutRequest(authType=1))` |
126
+ | `client.e911` | Records, address validation, provisioning | `c.e911.validate(E911AddressRequest(...))` |
127
+ | `client.gateways` | Termination routes | `c.gateways.list()` |
128
+ | `client.inumbering` | Inventory, orders, port-ins | `c.inumbering.search_inventory(npa=201)` |
129
+ | `client.lookups` | CNAM & LRN dips | `c.lookups.lrn("2015551234", ani="2012548000")` |
130
+ | `client.messaging` | SMS/MMS, 10DLC brands & campaigns | `c.messaging.send(MessageSendRequest(...))` |
131
+ | `client.numbers` | All operations on TNs on the account | `c.numbers.assign_campaign("2015551234", ...)` |
132
+ | `client.support` | Tickets, replies, attachments | `c.support.create(TicketCreateRequest(...))` |
133
+
134
+ Every method that takes a request body accepts a typed Pydantic model imported from `voicetel.models`:
135
+
136
+ ```python
137
+ from voicetel import Client
138
+ from voicetel.models import (
139
+ MessageSendRequest,
140
+ NumberCampaignAssignRequest,
141
+ PortSubmitRequest,
142
+ )
143
+
144
+ with Client(api_key=key) as c:
145
+ sent = c.messaging.send(MessageSendRequest(
146
+ fromNumber="2012548000",
147
+ toNumber="2015551234",
148
+ text="Your code is 482917",
149
+ ))
150
+ print(f"Sent: {sent.id} ({sent.parts} segment(s))")
151
+
152
+ c.numbers.assign_campaign(
153
+ "2015551234",
154
+ NumberCampaignAssignRequest(campaignId="CABC123"),
155
+ )
156
+ ```
157
+
158
+ ## 🚨 Error Handling
159
+
160
+ All HTTP errors raise subclasses of `voicetel.ApiError`. Catch broadly or narrowly:
161
+
162
+ | Status | Exception |
163
+ |--------|------------------------|
164
+ | 400 | `BadRequestError` |
165
+ | 401 | `AuthenticationError` |
166
+ | 403 | `PermissionDeniedError`|
167
+ | 404 | `NotFoundError` |
168
+ | 409 | `ConflictError` |
169
+ | 429 | `RateLimitError` |
170
+ | 5xx | `ServerError` |
171
+ | other | `ApiError` |
172
+
173
+ ```python
174
+ from voicetel import Client, NotFoundError, RateLimitError
175
+
176
+ with Client(api_key=key) as c:
177
+ try:
178
+ n = c.numbers.get("9999999999")
179
+ except NotFoundError:
180
+ print("That number isn't on your account.")
181
+ except RateLimitError as e:
182
+ print(f"Slow down — retry in {e.body.get('retryAfter', '?')}s")
183
+ ```
184
+
185
+ ## ⚡ Async Support
186
+
187
+ Identical surface to `Client`, with `await`-based methods:
188
+
189
+ ```python
190
+ import asyncio
191
+ from voicetel import AsyncClient
192
+
193
+ async def fetch_state(numbers: list[str]) -> None:
194
+ async with AsyncClient(api_key="...") as c:
195
+ state = await c.messaging.numbers_state(numbers=numbers)
196
+ for s in state.numbers:
197
+ print(f"{s.number}: network={s.network} campaign={s.campaign}")
198
+
199
+ asyncio.run(fetch_state(["2015551234", "2015551235"]))
200
+ ```
201
+
202
+ ## ⏱️ Rate Limits
203
+
204
+ These endpoints are limited to **6 requests per hour per IP**:
205
+
206
+ - `account/info`
207
+ - `account/mrc` (`client.account.recurring_charges()`)
208
+ - `account/cdr` (`client.account.cdr()`)
209
+ - `account/api-key` (`client.login()`)
210
+
211
+ The SDK automatically retries 429 responses with `Retry-After` honored, up to `max_retries` (default `2`). To bump it:
212
+
213
+ ```python
214
+ Client(api_key=key, max_retries=4, timeout=60.0)
215
+ ```
216
+
217
+ ## 🛠️ Development
218
+
219
+ ```bash
220
+ git clone https://github.com/voicetel/python-sdk
221
+ cd python-sdk
222
+ python -m venv .venv && source .venv/bin/activate
223
+ pip install -e ".[dev]"
224
+
225
+ # Unit tests (fast, no network)
226
+ pytest tests/unit
227
+
228
+ # 100% coverage gate
229
+ pytest tests/unit --cov --cov-fail-under=100
230
+
231
+ # Lint + type-check
232
+ ruff check src tests
233
+ mypy src
234
+
235
+ # Integration tests (live api.voicetel.com, read-only)
236
+ cp .env.example .env # fill in VOICETEL_USERNAME / VOICETEL_PASSWORD
237
+ pytest tests/integration
238
+
239
+ # Build wheel + sdist
240
+ python -m build
241
+ twine check dist/*
242
+ ```
243
+
244
+ ## 📖 API Documentation
245
+
246
+ - **Reference docs:** [voicetel.com/docs/api/v2.2/](https://voicetel.com/docs/api/v2.2/)
247
+ - **Interactive playground:** [voicetel.com/docs/api/v2.2/playground/](https://voicetel.com/docs/api/v2.2/playground/) — try the API in your browser without writing any code
248
+ - **API credentials:** [voicetel.com/docs/api/v2.2/credentials/](https://voicetel.com/docs/api/v2.2/credentials/)
249
+ - **Type definitions:** see the `voicetel.models` module — every wire shape has a Pydantic model.
250
+
251
+ ## 🙌 Contributors
252
+
253
+ - [Michael Mavroudis](https://github.com/mavroudis) — Lead Developer
254
+
255
+ Contributions welcome. Open an issue describing the change you want to make, or send a pull request against `main`.
256
+
257
+ ## 💖 Sponsors
258
+
259
+ | Sponsor | Contribution |
260
+ |---------|--------------|
261
+ | [VoiceTel Communications](https://voicetel.com) | Primary development and production hosting |
262
+
263
+ ## 📄 License
264
+
265
+ This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "voicetel-api"
7
+ version = "2.2.10"
8
+ description = "Official Python SDK for the VoiceTel REST API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "VoiceTel", email = "support@voicetel.com" }]
13
+ keywords = ["voicetel", "telephony", "sip", "sms", "e911", "voip"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Communications :: Telephony",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.27",
29
+ "pydantic>=2.6",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/voicetel/python-sdk"
34
+ Documentation = "https://voicetel.com/docs/api/v2.2/"
35
+ Repository = "https://github.com/voicetel/python-sdk"
36
+ Issues = "https://github.com/voicetel/python-sdk/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=8.0",
41
+ "pytest-asyncio>=0.23",
42
+ "pytest-cov>=5.0",
43
+ "respx>=0.21",
44
+ "ruff>=0.5",
45
+ "mypy>=1.10",
46
+ ]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/voicetel"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ include = ["src/voicetel", "README.md", "LICENSE"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ asyncio_mode = "auto"
57
+ markers = [
58
+ "integration: hits the live api.voicetel.com (requires VOICETEL_USERNAME and VOICETEL_PASSWORD env vars)",
59
+ ]
60
+ addopts = "-ra --strict-markers"
61
+
62
+ [tool.coverage.run]
63
+ source = ["src/voicetel"]
64
+ branch = true
65
+
66
+ [tool.coverage.report]
67
+ exclude_lines = [
68
+ "pragma: no cover",
69
+ "if TYPE_CHECKING:",
70
+ "raise NotImplementedError",
71
+ "\\.\\.\\.",
72
+ ]
73
+ show_missing = true
74
+ skip_covered = false
75
+
76
+ [tool.ruff]
77
+ line-length = 110
78
+ target-version = "py310"
79
+
80
+ [tool.ruff.lint]
81
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
82
+
83
+ [tool.mypy]
84
+ python_version = "3.10"
85
+ strict = true
86
+ files = ["src/voicetel"]