digitalocean-python-sdk 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.
Files changed (28) hide show
  1. digitalocean_python_sdk-0.1.0/.gitignore +14 -0
  2. digitalocean_python_sdk-0.1.0/CHANGELOG.md +17 -0
  3. digitalocean_python_sdk-0.1.0/LICENSE +21 -0
  4. digitalocean_python_sdk-0.1.0/PKG-INFO +356 -0
  5. digitalocean_python_sdk-0.1.0/README.md +314 -0
  6. digitalocean_python_sdk-0.1.0/examples/digitalocean.toml +4 -0
  7. digitalocean_python_sdk-0.1.0/examples/subdomain.py +28 -0
  8. digitalocean_python_sdk-0.1.0/pyproject.toml +65 -0
  9. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/__init__.py +42 -0
  10. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/_base.py +246 -0
  11. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/_version.py +1 -0
  12. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/client.py +33 -0
  13. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/config.py +67 -0
  14. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/errors.py +131 -0
  15. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/messages.py +63 -0
  16. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/messages.yaml +28 -0
  17. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/models.py +157 -0
  18. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/py.typed +0 -0
  19. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/resources/__init__.py +4 -0
  20. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/resources/_paging.py +35 -0
  21. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/resources/dns.py +226 -0
  22. digitalocean_python_sdk-0.1.0/src/digitalocean_sdk/resources/domains.py +76 -0
  23. digitalocean_python_sdk-0.1.0/tests/conftest.py +69 -0
  24. digitalocean_python_sdk-0.1.0/tests/test_async.py +32 -0
  25. digitalocean_python_sdk-0.1.0/tests/test_client.py +106 -0
  26. digitalocean_python_sdk-0.1.0/tests/test_dns.py +150 -0
  27. digitalocean_python_sdk-0.1.0/tests/test_domains.py +53 -0
  28. digitalocean_python_sdk-0.1.0/tests/test_messages.py +21 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ - Sync `DigitalOcean` and async `AsyncDigitalOcean` clients
8
+ - Domains: list, iter, get, exists, create, delete
9
+ - DNS: list, iter, get, exists, create, add, update (PATCH or PUT), delete, delete_all, replace
10
+ - DNS shortcuts: point, unpoint, alias, set_txt
11
+ - Lists follow every page, filters by type and name run on DigitalOcean's side
12
+ - Relative and fully qualified record names both work, host targets get their trailing dot
13
+ - `account()` to check a token, `request()` for any other endpoint
14
+ - Typed errors with DigitalOcean's error id and request id, automatic retries for rate limits and server errors
15
+ - Configuration through goodconf: code, `DIGITALOCEAN_*` environment variables or a TOML, YAML or JSON file
16
+ - Every user facing message lives in `messages.yaml` and can be overridden at runtime
17
+ - API version and every endpoint path come from the settings and can be overridden
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qiyascc
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,356 @@
1
+ Metadata-Version: 2.5
2
+ Name: digitalocean-python-sdk
3
+ Version: 0.1.0
4
+ Summary: A typed Python client for the DigitalOcean Domains and DNS API, sync and async.
5
+ Project-URL: Homepage, https://theqiyas.cc/projects/digitalocean-python-sdk/
6
+ Project-URL: Source, https://github.com/qiyascc/digitalocean-python-sdk
7
+ Project-URL: Author, https://theqiyas.cc
8
+ Project-URL: Issues, https://github.com/qiyascc/digitalocean-python-sdk/issues
9
+ Project-URL: Changelog, https://github.com/qiyascc/digitalocean-python-sdk/blob/main/CHANGELOG.md
10
+ Author-email: qiyascc <qiyasturk@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,digitalocean,dns,domains,sdk,subdomain
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
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 :: Internet :: Name Service (DNS)
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: goodconf>=7.1
26
+ Requires-Dist: httpx<1,>=0.24
27
+ Requires-Dist: pydantic>=2.7
28
+ Requires-Dist: pyyaml>=6
29
+ Provides-Extra: dev
30
+ Requires-Dist: goodconf[toml]>=7.1; extra == 'dev'
31
+ Requires-Dist: goodconf[yaml]>=7.1; extra == 'dev'
32
+ Requires-Dist: mypy>=1.8; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
34
+ Requires-Dist: pytest>=7; extra == 'dev'
35
+ Requires-Dist: ruff>=0.4; extra == 'dev'
36
+ Requires-Dist: types-pyyaml; extra == 'dev'
37
+ Provides-Extra: toml
38
+ Requires-Dist: goodconf[toml]>=7.1; extra == 'toml'
39
+ Provides-Extra: yaml
40
+ Requires-Dist: goodconf[yaml]>=7.1; extra == 'yaml'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # digitalocean-python-sdk
44
+
45
+ A small, typed Python client for the DigitalOcean Domains and DNS API. It works with both sync and async code, waits and retries when DigitalOcean rate limits you, and returns plain dataclasses, not raw JSON.
46
+
47
+ ```python
48
+ from digitalocean_sdk import DigitalOcean
49
+
50
+ with DigitalOcean("DIGITALOCEAN_TOKEN") as do:
51
+ do.dns.point("example.com", "shop", "203.0.113.10")
52
+ ```
53
+
54
+ That call makes `shop.example.com` resolve to `203.0.113.10`. If the record already exists it is updated, if it is missing it is created, and extra `A` records with the same name are removed.
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install digitalocean-python-sdk
60
+ ```
61
+
62
+ Requires Python 3.10 or newer. Dependencies: [httpx](https://www.python-httpx.org/), [goodconf](https://github.com/lincolnloop/goodconf), pydantic and PyYAML.
63
+
64
+ The import name is `digitalocean_sdk`, so it can live next to the older `python-digitalocean` package without clashing.
65
+
66
+ ## Configuration
67
+
68
+ Settings are loaded with [goodconf](https://github.com/lincolnloop/goodconf), so you can configure the client from code, from environment variables or from a config file. Values passed in code win over the environment, and the environment wins over the file.
69
+
70
+ Create a personal access token at [cloud.digitalocean.com/account/api/tokens](https://cloud.digitalocean.com/account/api/tokens). For DNS work the token needs the `domain` scopes (read, create, update, delete). Then pick whichever option suits you:
71
+
72
+ ```python
73
+ do = DigitalOcean("dop_v1_...")
74
+ ```
75
+
76
+ ```bash
77
+ export DIGITALOCEAN_TOKEN=dop_v1_...
78
+ ```
79
+
80
+ ```toml
81
+ token = "dop_v1_..."
82
+ timeout = 15
83
+ default_ttl = 1800
84
+ ```
85
+
86
+ `DIGITALOCEAN_TOKEN` is the same variable `doctl` and Terraform use, so an existing setup usually works as is.
87
+
88
+ With no arguments, the client looks for `digitalocean.toml`, `digitalocean.yaml`, `digitalocean.yml` or `digitalocean.json` in the working directory. To load a file from somewhere else, set `DIGITALOCEAN_CONFIG=/path/to/file`, or pass `config_file=` to the client. YAML files need the `yaml` extra, and TOML files on Python 3.10 need the `toml` extra:
89
+
90
+ ```bash
91
+ pip install "digitalocean-python-sdk[yaml]"
92
+ ```
93
+
94
+ | Setting | Env variable | Default |
95
+ | --- | --- | --- |
96
+ | `token` | `DIGITALOCEAN_TOKEN` | |
97
+ | `base_url` | `DIGITALOCEAN_BASE_URL` | `https://api.digitalocean.com` |
98
+ | `api_version` | `DIGITALOCEAN_API_VERSION` | `v2` |
99
+ | `endpoints.*` | `DIGITALOCEAN_ENDPOINTS__<NAME>` | see below |
100
+ | `timeout` | `DIGITALOCEAN_TIMEOUT` | `30` |
101
+ | `max_retries` | `DIGITALOCEAN_MAX_RETRIES` | `3` |
102
+ | `max_retry_wait` | `DIGITALOCEAN_MAX_RETRY_WAIT` | `60` |
103
+ | `backoff_base` | `DIGITALOCEAN_BACKOFF_BASE` | `0.5` |
104
+ | `backoff_cap` | `DIGITALOCEAN_BACKOFF_CAP` | `8` |
105
+ | `page_size` | `DIGITALOCEAN_PAGE_SIZE` | `200` (the API maximum) |
106
+ | `default_ttl` | `DIGITALOCEAN_DEFAULT_TTL` | `1800` |
107
+ | `user_agent` | `DIGITALOCEAN_USER_AGENT` | `digitalocean-python-sdk/{version}` |
108
+
109
+ Any setting can also be passed straight to the client, for example `DigitalOcean(timeout=10, max_retries=5)`. If you build settings yourself, hand them over with `settings=`:
110
+
111
+ ```python
112
+ from digitalocean_sdk import DigitalOcean, load_settings
113
+
114
+ settings = load_settings("/etc/myapp/digitalocean.toml", default_ttl=300)
115
+ do = DigitalOcean(settings=settings)
116
+ ```
117
+
118
+ ### Endpoints
119
+
120
+ API paths are part of the settings too, so nothing is pinned in the code. Each one is a template relative to the base URL. `{version}` comes from `api_version`, and the other placeholders are filled in and URL encoded for each call.
121
+
122
+ | Name | Default |
123
+ | --- | --- |
124
+ | `account` | `/{version}/account` |
125
+ | `domains` | `/{version}/domains` |
126
+ | `domain` | `/{version}/domains/{domain}` |
127
+ | `records` | `/{version}/domains/{domain}/records` |
128
+ | `record` | `/{version}/domains/{domain}/records/{record_id}` |
129
+
130
+ ```bash
131
+ export DIGITALOCEAN_ENDPOINTS__RECORDS="/{version}/domains/{domain}/records"
132
+ ```
133
+
134
+ Check that the token works:
135
+
136
+ ```python
137
+ do.account()
138
+ ```
139
+
140
+ ```python
141
+ Account(email='you@example.com', uuid='...', status='active', email_verified=True, droplet_limit=25, ...)
142
+ ```
143
+
144
+ ## Domains
145
+
146
+ DigitalOcean is not a registrar. A domain here is a zone you manage with DigitalOcean DNS: buy the name anywhere, then set its name servers to `ns1.digitalocean.com`, `ns2.digitalocean.com` and `ns3.digitalocean.com`.
147
+
148
+ ```python
149
+ do.domains.create("example.com")
150
+ do.domains.create("example.com", ip_address="203.0.113.10")
151
+
152
+ for domain in do.domains.iter():
153
+ print(domain.name, domain.ttl)
154
+
155
+ do.domains.list()
156
+ do.domains.get("example.com").zone_file
157
+ do.domains.exists("example.com")
158
+ do.domains.delete("example.com")
159
+ do.domains.delete("maybe-gone.com", missing_ok=True)
160
+ ```
161
+
162
+ When `ip_address` is given, DigitalOcean creates an `A` record for the apex right away.
163
+
164
+ ## DNS
165
+
166
+ Every DNS method takes the domain first. Record names are relative to the domain and the apex is `@`. You can also pass a full name like `www.example.com`, it is turned into `www` for you.
167
+
168
+ ```python
169
+ from digitalocean_sdk import DigitalOcean, DNSRecord
170
+
171
+ do = DigitalOcean()
172
+
173
+ do.dns.list("example.com")
174
+ do.dns.list("example.com", "A")
175
+ do.dns.list("example.com", "A", "www")
176
+ do.dns.list("example.com", name="www")
177
+ do.dns.exists("example.com", "A", "shop")
178
+ do.dns.get("example.com", 28448433)
179
+ ```
180
+
181
+ Filtering by type and name happens on DigitalOcean's side. Lists follow every page for you, and `iter()` does the same lazily. Each item is a `DNSRecord`:
182
+
183
+ ```python
184
+ DNSRecord(type='A', name='www', data='203.0.113.10', ttl=1800, priority=None, port=None, weight=None, flags=None, tag=None, id=28448433)
185
+ ```
186
+
187
+ ### Creating and changing records
188
+
189
+ DigitalOcean identifies records by id. `create` and `add` return the new records with their ids filled in.
190
+
191
+ ```python
192
+ record = do.dns.create("example.com", DNSRecord.a("api", "203.0.113.20"))
193
+
194
+ do.dns.add(
195
+ "example.com",
196
+ DNSRecord.txt("@", "google-site-verification=abc"),
197
+ DNSRecord.mx("@", "mail.example.com", priority=10),
198
+ DNSRecord.caa("@", "letsencrypt.org"),
199
+ DNSRecord.srv("_sip._udp", "sip.example.com", port=5060),
200
+ )
201
+
202
+ do.dns.update("example.com", record, data="203.0.113.21", ttl=300)
203
+ do.dns.update("example.com", 28448433, name="api-v2")
204
+
205
+ record.ttl = 600
206
+ do.dns.update("example.com", record)
207
+
208
+ do.dns.delete("example.com", record)
209
+ do.dns.delete("example.com", 28448433, missing_ok=True)
210
+ do.dns.delete_all("example.com", "A", "old")
211
+ ```
212
+
213
+ `update` with fields sends a `PATCH` and changes only those fields. `update` with just a record sends a `PUT` with the whole record.
214
+
215
+ Host names in `CNAME`, `MX`, `NS` and `SRV` records must be fully qualified with a trailing dot. The helpers add that dot for you, so `DNSRecord.cname("docs", "example.github.io")` is sent as `example.github.io.`. Use `@` to point at the apex itself.
216
+
217
+ ### Replacing a set of records
218
+
219
+ `replace` makes one type and name hold exactly the values you pass. Matching records are kept, missing ones are created and the rest are deleted. Pass `ttl` to change it on every record in the set.
220
+
221
+ ```python
222
+ do.dns.replace("example.com", "A", "www", ["203.0.113.10", "203.0.113.11"], ttl=600)
223
+ ```
224
+
225
+ ### Shortcuts
226
+
227
+ ```python
228
+ do.dns.point("example.com", "shop", "203.0.113.10")
229
+ do.dns.point("example.com", "shop", "2001:db8::10")
230
+ do.dns.alias("example.com", "docs", "example.github.io")
231
+ do.dns.set_txt("example.com", "_acme-challenge", "token-value", ttl=60)
232
+ do.dns.unpoint("example.com", "shop")
233
+ ```
234
+
235
+ `point` picks `A` or `AAAA` from the address you pass. `unpoint` deletes the matching records and returns how many it removed, so it is safe to call when nothing is there.
236
+
237
+ ### Example: one subdomain per customer
238
+
239
+ ```python
240
+ from digitalocean_sdk import DigitalOcean
241
+
242
+ SERVER_IP = "203.0.113.10"
243
+
244
+ def create_customer_site(slug: str) -> str:
245
+ with DigitalOcean() as do:
246
+ if do.dns.exists("example.com", "A", slug):
247
+ raise ValueError(f"{slug}.example.com is already taken")
248
+ do.dns.point("example.com", slug, SERVER_IP, ttl=300)
249
+ return f"{slug}.example.com"
250
+ ```
251
+
252
+ ## Async
253
+
254
+ `AsyncDigitalOcean` has the same methods. Await them.
255
+
256
+ ```python
257
+ import asyncio
258
+ from digitalocean_sdk import AsyncDigitalOcean
259
+
260
+ async def main():
261
+ async with AsyncDigitalOcean() as do:
262
+ records = await do.dns.list("example.com", "A")
263
+ await do.dns.point("example.com", "shop", "203.0.113.10")
264
+ async for domain in do.domains.iter():
265
+ print(domain.name)
266
+
267
+ asyncio.run(main())
268
+ ```
269
+
270
+ ## Other endpoints
271
+
272
+ The SDK focuses on Domains and DNS, but the same authenticated client can call any other DigitalOcean endpoint, with the same retries and errors:
273
+
274
+ ```python
275
+ droplets = do.request("GET", "/v2/droplets", params={"per_page": 50})
276
+ do.request("POST", "/v2/droplets/123/actions", json={"type": "reboot"})
277
+ ```
278
+
279
+ ## Errors
280
+
281
+ All exceptions inherit from `DigitalOceanError`.
282
+
283
+ | Exception | When |
284
+ | --- | --- |
285
+ | `ValidationError` | 400 or 422, the request body or parameters were rejected |
286
+ | `AuthenticationError` | 401, or no token was found |
287
+ | `PermissionDeniedError` | 403, often a token without the right scope |
288
+ | `NotFoundError` | 404 |
289
+ | `ConflictError` | 409 |
290
+ | `RateLimitError` | 429 that is still failing after retries. Has `.retry_after` |
291
+ | `ServerError` | 5xx |
292
+ | `APIConnectionError` / `APITimeoutError` | the request never got an answer |
293
+
294
+ Status errors carry `status_code`, DigitalOcean's error `code` (for example `not_found` or `unprocessable_entity`), the `request_id` to quote to support, and the raw `response`.
295
+
296
+ ```python
297
+ from digitalocean_sdk import ValidationError
298
+
299
+ try:
300
+ do.dns.create("example.com", DNSRecord.cname("@", "elsewhere.com"))
301
+ except ValidationError as exc:
302
+ print(exc.code, exc.message, exc.request_id)
303
+ ```
304
+
305
+ ## Retries and timeouts
306
+
307
+ DigitalOcean allows 5,000 requests per hour and 250 per minute. When it answers 429, the client waits until the `ratelimit-reset` time it sends and tries again, as long as that wait is under `max_retry_wait`. Server errors and dropped connections are retried with exponential backoff, but only for idempotent methods (`GET`, `PUT`, `DELETE`). A `POST` that creates a record is never sent twice.
308
+
309
+ ```python
310
+ do = DigitalOcean(timeout=10, max_retries=5, max_retry_wait=30)
311
+ ```
312
+
313
+ Set `max_retries=0` to turn retries off. To use proxies or a custom transport, pass your own `httpx.Client` or `httpx.AsyncClient` as `http_client=`.
314
+
315
+ ## Messages
316
+
317
+ Every error and validation message the SDK produces is stored in `digitalocean_sdk/messages.yaml`, so none of them are hardcoded. You can replace some or all of them, for example to translate them:
318
+
319
+ ```yaml
320
+ value:
321
+ empty_name: Record adı boş ola bilməz
322
+ error:
323
+ timeout: "Sorğu vaxtı bitdi: {error}"
324
+ ```
325
+
326
+ ```python
327
+ from digitalocean_sdk import messages
328
+
329
+ messages.use("messages.az.yaml")
330
+ messages.use({"value.empty_name": "Record adı boş ola bilməz"})
331
+ ```
332
+
333
+ Unknown keys raise `KeyError`, which catches typos early. Call `messages.reset()` to go back to the defaults.
334
+
335
+ ## Development
336
+
337
+ ```bash
338
+ python -m venv .venv
339
+ . .venv/bin/activate
340
+ pip install -e ".[dev]"
341
+ pytest
342
+ ruff check .
343
+ ```
344
+
345
+ The tests use `httpx.MockTransport`, so they never touch the real API.
346
+
347
+ ## Links
348
+
349
+ - Source code: [github.com/qiyascc/digitalocean-python-sdk](https://github.com/qiyascc/digitalocean-python-sdk)
350
+ - PyPI: [pypi.org/project/digitalocean-python-sdk](https://pypi.org/project/digitalocean-python-sdk/)
351
+ - Project page: [theqiyas.cc/projects/digitalocean-python-sdk](https://theqiyas.cc/projects/digitalocean-python-sdk/)
352
+ - Author: [Qiyas Sadıqov](https://theqiyas.cc), more projects at [theqiyas.cc](https://theqiyas.cc)
353
+
354
+ ## License
355
+
356
+ MIT