hostinger-python-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,366 @@
1
+ Metadata-Version: 2.5
2
+ Name: hostinger-python-sdk
3
+ Version: 0.1.0
4
+ Summary: A typed Python client for the Hostinger Domains and DNS API, sync and async.
5
+ Project-URL: Homepage, https://theqiyas.cc/projects/hostinger-python-sdk/
6
+ Project-URL: Source, https://github.com/qiyascc/hostinger-python-sdk
7
+ Project-URL: Author, https://theqiyas.cc
8
+ Project-URL: Issues, https://github.com/qiyascc/hostinger-python-sdk/issues
9
+ Project-URL: Changelog, https://github.com/qiyascc/hostinger-python-sdk/blob/main/CHANGELOG.md
10
+ Author-email: qiyascc <qiyasturk@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,dns,domains,hostinger,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
+ # hostinger-python-sdk
44
+
45
+ A small, typed Python client for the Hostinger Domains and DNS API. It works with both sync and async code, waits and retries when Hostinger rate limits you, and returns plain dataclasses, not raw JSON.
46
+
47
+ ```python
48
+ from hostinger_sdk import Hostinger
49
+
50
+ with Hostinger("API_TOKEN") as hs:
51
+ hs.dns.point("example.com", "shop", "203.0.113.10")
52
+ ```
53
+
54
+ That call makes `shop.example.com` resolve to `203.0.113.10`, replacing any `A` records `shop` had before.
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install hostinger-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 `hostinger_sdk`, so it does not clash with other Hostinger packages.
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 token on the [API page of hPanel](https://hpanel.hostinger.com/profile/api). A token has the same permissions as the user who created it. Then pick whichever option suits you:
71
+
72
+ ```python
73
+ hs = Hostinger("API_TOKEN")
74
+ ```
75
+
76
+ ```bash
77
+ export HOSTINGER_API_TOKEN=...
78
+ ```
79
+
80
+ ```toml
81
+ api_token = "..."
82
+ timeout = 15
83
+ default_ttl = 3600
84
+ ```
85
+
86
+ With no arguments, the client looks for `hostinger.toml`, `hostinger.yaml`, `hostinger.yml` or `hostinger.json` in the working directory. To load a file from somewhere else, set `HOSTINGER_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:
87
+
88
+ ```bash
89
+ pip install "hostinger-python-sdk[yaml]"
90
+ ```
91
+
92
+ | Setting | Env variable | Default |
93
+ | --- | --- | --- |
94
+ | `api_token` | `HOSTINGER_API_TOKEN` | |
95
+ | `base_url` | `HOSTINGER_BASE_URL` | `https://developers.hostinger.com` |
96
+ | `api_version` | `HOSTINGER_API_VERSION` | `v1` |
97
+ | `endpoints.*` | `HOSTINGER_ENDPOINTS__<NAME>` | see below |
98
+ | `timeout` | `HOSTINGER_TIMEOUT` | `30` |
99
+ | `max_retries` | `HOSTINGER_MAX_RETRIES` | `3` |
100
+ | `max_retry_wait` | `HOSTINGER_MAX_RETRY_WAIT` | `60` |
101
+ | `backoff_base` | `HOSTINGER_BACKOFF_BASE` | `0.5` |
102
+ | `backoff_cap` | `HOSTINGER_BACKOFF_CAP` | `8` |
103
+ | `default_ttl` | `HOSTINGER_DEFAULT_TTL` | `14400` |
104
+ | `user_agent` | `HOSTINGER_USER_AGENT` | `hostinger-python-sdk/{version}` |
105
+
106
+ Any setting can also be passed straight to the client, for example `Hostinger(timeout=10, max_retries=5)`. If you build settings yourself, hand them over with `settings=`:
107
+
108
+ ```python
109
+ from hostinger_sdk import Hostinger, load_settings
110
+
111
+ settings = load_settings("/etc/myapp/hostinger.toml", default_ttl=300)
112
+ hs = Hostinger(settings=settings)
113
+ ```
114
+
115
+ ### Endpoints
116
+
117
+ 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.
118
+
119
+ | Name | Default |
120
+ | --- | --- |
121
+ | `zone` | `/api/dns/{version}/zones/{domain}` |
122
+ | `zone_reset` | `/api/dns/{version}/zones/{domain}/reset` |
123
+ | `zone_validate` | `/api/dns/{version}/zones/{domain}/validate` |
124
+ | `snapshots` | `/api/dns/{version}/snapshots/{domain}` |
125
+ | `snapshot` | `/api/dns/{version}/snapshots/{domain}/{snapshot_id}` |
126
+ | `snapshot_restore` | `/api/dns/{version}/snapshots/{domain}/{snapshot_id}/restore` |
127
+ | `domains` | `/api/domains/{version}/portfolio` |
128
+ | `domain` | `/api/domains/{version}/portfolio/{domain}` |
129
+ | `availability` | `/api/domains/{version}/availability` |
130
+ | `nameservers` | `/api/domains/{version}/portfolio/{domain}/nameservers` |
131
+ | `domain_lock` | `/api/domains/{version}/portfolio/{domain}/domain-lock` |
132
+ | `privacy` | `/api/domains/{version}/portfolio/{domain}/privacy-protection` |
133
+ | `forwardings` | `/api/domains/{version}/forwarding` |
134
+ | `forwarding` | `/api/domains/{version}/forwarding/{domain}` |
135
+
136
+ ```bash
137
+ export HOSTINGER_ENDPOINTS__ZONE="/api/dns/{version}/zones/{domain}"
138
+ ```
139
+
140
+ ## DNS
141
+
142
+ 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.
143
+
144
+ ```python
145
+ from hostinger_sdk import Hostinger, DNSRecord
146
+
147
+ hs = Hostinger()
148
+
149
+ hs.dns.list("example.com")
150
+ hs.dns.list("example.com", "A")
151
+ hs.dns.list("example.com", "A", "www")
152
+ hs.dns.list("example.com", name="www")
153
+ hs.dns.exists("example.com", "A", "shop")
154
+ ```
155
+
156
+ Hostinger stores records in sets that share a name, a type and a TTL. The SDK flattens them, so each item is one `DNSRecord`:
157
+
158
+ ```python
159
+ DNSRecord(type='A', name='www', data='203.0.113.10', ttl=14400, disabled=False)
160
+ ```
161
+
162
+ ### Creating and changing records
163
+
164
+ ```python
165
+ hs.dns.add(
166
+ "example.com",
167
+ DNSRecord.a("api", "203.0.113.20"),
168
+ DNSRecord.txt("@", "google-site-verification=abc"),
169
+ DNSRecord.mx("@", "mx1.example.net", priority=10),
170
+ DNSRecord.caa("@", "letsencrypt.org"),
171
+ DNSRecord.srv("_sip._udp", "sip.example.com", port=5060),
172
+ )
173
+
174
+ hs.dns.replace("example.com", "A", "www", ["203.0.113.10", "203.0.113.11"], ttl=600)
175
+
176
+ hs.dns.delete("example.com", "A", "old")
177
+ hs.dns.delete("example.com", "A", "www", data="203.0.113.11")
178
+ hs.dns.delete_many("example.com", [("A", "old"), ("TXT", "_old-verification")])
179
+ ```
180
+
181
+ `add` keeps what is there and adds the new values. `replace` swaps out every record with that type and name. `replace_many` does the same for several sets in one request. `delete` without `data` removes the whole set, with `data` it removes only that value and keeps the rest.
182
+
183
+ Record content follows zone file syntax, which is what Hostinger expects. The helpers build it for you: `DNSRecord.mx("@", "mx1.example.net", 10)` becomes `10 mx1.example.net.`, `DNSRecord.caa("@", "letsencrypt.org")` becomes `0 issue "letsencrypt.org"`, and host names in `CNAME`, `ALIAS` and `NS` records get their trailing dot.
184
+
185
+ ### Shortcuts
186
+
187
+ ```python
188
+ hs.dns.point("example.com", "shop", "203.0.113.10")
189
+ hs.dns.point("example.com", "shop", "2001:db8::10")
190
+ hs.dns.alias("example.com", "docs", "example.github.io")
191
+ hs.dns.set_txt("example.com", "_acme-challenge", "token-value", ttl=300)
192
+ hs.dns.unpoint("example.com", "shop")
193
+ ```
194
+
195
+ `point` picks `A` or `AAAA` from the address you pass.
196
+
197
+ ### Validate, reset and snapshots
198
+
199
+ ```python
200
+ hs.dns.validate("example.com", [DNSRecord.a("www", "203.0.113.10")])
201
+
202
+ hs.dns.reset("example.com")
203
+ hs.dns.reset("example.com", reset_email_records=False, keep_types=["MX", "TXT"])
204
+
205
+ for snapshot in hs.dns.snapshots("example.com"):
206
+ print(snapshot.id, snapshot.reason, snapshot.created_at)
207
+
208
+ old = hs.dns.snapshot("example.com", 5341)
209
+ old.records
210
+ hs.dns.restore("example.com", 5341)
211
+ ```
212
+
213
+ `validate` checks records without saving them and raises `ValidationError` if Hostinger rejects any of them. Hostinger keeps snapshots of the zone as it changes, so a bad edit can be rolled back.
214
+
215
+ ### Example: one subdomain per customer
216
+
217
+ ```python
218
+ from hostinger_sdk import Hostinger
219
+
220
+ SERVER_IP = "203.0.113.10"
221
+
222
+ def create_customer_site(slug: str) -> str:
223
+ with Hostinger() as hs:
224
+ if hs.dns.exists("example.com", "A", slug):
225
+ raise ValueError(f"{slug}.example.com is already taken")
226
+ hs.dns.point("example.com", slug, SERVER_IP, ttl=300)
227
+ return f"{slug}.example.com"
228
+ ```
229
+
230
+ ## Domains
231
+
232
+ ```python
233
+ for domain in hs.domains.list():
234
+ print(domain.domain, domain.status, domain.expires_at)
235
+
236
+ info = hs.domains.get("example.com")
237
+ info.name_servers, info.is_locked, info.is_privacy_protected
238
+
239
+ for result in hs.domains.available("my-new-idea", ["com", "net", "az"]):
240
+ print(result.domain, result.available)
241
+
242
+ hs.domains.available("my-new-idea", ["com"], with_alternatives=True)
243
+
244
+ hs.domains.set_nameservers("example.com", ["ns1.dns-parking.com", "ns2.dns-parking.com"])
245
+ hs.domains.lock("example.com")
246
+ hs.domains.unlock("example.com")
247
+ hs.domains.enable_privacy("example.com")
248
+ hs.domains.disable_privacy("example.com")
249
+ ```
250
+
251
+ ### Forwarding
252
+
253
+ ```python
254
+ hs.domains.forward("old-brand.com", "https://new-brand.com", redirect_type=301)
255
+ hs.domains.forwarding("old-brand.com")
256
+ hs.domains.stop_forwarding("old-brand.com", missing_ok=True)
257
+ ```
258
+
259
+ `forward` creates the forwarding if there is none and updates it otherwise. `forwarding` returns `None` when the domain is not forwarded.
260
+
261
+ ## Async
262
+
263
+ `AsyncHostinger` has the same methods. Await them.
264
+
265
+ ```python
266
+ import asyncio
267
+ from hostinger_sdk import AsyncHostinger
268
+
269
+ async def main():
270
+ async with AsyncHostinger() as hs:
271
+ records = await hs.dns.list("example.com", "A")
272
+ await hs.dns.point("example.com", "shop", "203.0.113.10")
273
+ for domain in await hs.domains.list():
274
+ print(domain.domain)
275
+
276
+ asyncio.run(main())
277
+ ```
278
+
279
+ ## Other endpoints
280
+
281
+ The SDK focuses on Domains and DNS, but the same authenticated client can call any other Hostinger endpoint, with the same retries and errors:
282
+
283
+ ```python
284
+ hs.request("GET", "/api/vps/v1/virtual-machines")
285
+ hs.request("GET", "/api/billing/v1/subscriptions")
286
+ ```
287
+
288
+ ## Errors
289
+
290
+ All exceptions inherit from `HostingerError`.
291
+
292
+ | Exception | When |
293
+ | --- | --- |
294
+ | `ValidationError` | 400 or 422, the request body or parameters were rejected |
295
+ | `AuthenticationError` | 401, or no token was found |
296
+ | `PermissionDeniedError` | 403 |
297
+ | `NotFoundError` | 404 |
298
+ | `ConflictError` | 409 |
299
+ | `RateLimitError` | 429 that is still failing after retries. Has `.retry_after` |
300
+ | `ServerError` | 5xx |
301
+ | `APIConnectionError` / `APITimeoutError` | the request never got an answer |
302
+
303
+ Status errors carry `status_code`, Hostinger's `correlation_id` to quote to support, a `fields` list that says which field failed and why, and the raw `response`.
304
+
305
+ ```python
306
+ from hostinger_sdk import ValidationError
307
+
308
+ try:
309
+ hs.dns.add("example.com", DNSRecord("A", "www", "not-an-ip"))
310
+ except ValidationError as exc:
311
+ for field in exc.fields:
312
+ print(field.field, field.messages)
313
+ ```
314
+
315
+ ## Retries and timeouts
316
+
317
+ When Hostinger answers 429, the client waits as long as the `Retry-After` header says and tries again, as long as that wait is under `max_retry_wait`. Hostinger may block an IP for a while if it keeps hitting the limit, so the client does not hammer it. Server errors and dropped connections are retried with exponential backoff, but only for idempotent methods (`GET`, `PUT`, `DELETE`).
318
+
319
+ ```python
320
+ hs = Hostinger(timeout=10, max_retries=5, max_retry_wait=30)
321
+ ```
322
+
323
+ 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=`.
324
+
325
+ ## Messages
326
+
327
+ Every error and validation message the SDK produces is stored in `hostinger_sdk/messages.yaml`, so none of them are hardcoded. You can replace some or all of them, for example to translate them:
328
+
329
+ ```yaml
330
+ value:
331
+ empty_name: Record adı boş ola bilməz
332
+ error:
333
+ timeout: "Sorğu vaxtı bitdi: {error}"
334
+ ```
335
+
336
+ ```python
337
+ from hostinger_sdk import messages
338
+
339
+ messages.use("messages.az.yaml")
340
+ messages.use({"value.empty_name": "Record adı boş ola bilməz"})
341
+ ```
342
+
343
+ Unknown keys raise `KeyError`, which catches typos early. Call `messages.reset()` to go back to the defaults.
344
+
345
+ ## Development
346
+
347
+ ```bash
348
+ python -m venv .venv
349
+ . .venv/bin/activate
350
+ pip install -e ".[dev]"
351
+ pytest
352
+ ruff check .
353
+ ```
354
+
355
+ The tests use `httpx.MockTransport`, so they never touch the real API.
356
+
357
+ ## Links
358
+
359
+ - Source code: [github.com/qiyascc/hostinger-python-sdk](https://github.com/qiyascc/hostinger-python-sdk)
360
+ - PyPI: [pypi.org/project/hostinger-python-sdk](https://pypi.org/project/hostinger-python-sdk/)
361
+ - Project page: [theqiyas.cc/projects/hostinger-python-sdk](https://theqiyas.cc/projects/hostinger-python-sdk/)
362
+ - Author: [Qiyas Sadıqov](https://theqiyas.cc), more projects at [theqiyas.cc](https://theqiyas.cc)
363
+
364
+ ## License
365
+
366
+ MIT
@@ -0,0 +1,17 @@
1
+ hostinger_sdk/__init__.py,sha256=31TyAtHs7b4zIykUV84lDvLDaHpATyKo4r9_uMm0h68,1038
2
+ hostinger_sdk/_base.py,sha256=MvXb7l_HJAybQKPGtFIqLEyU_GERsEc5LyJzvtWsUTg,8769
3
+ hostinger_sdk/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
4
+ hostinger_sdk/client.py,sha256=ohAS8qPKZIPaaXYkJ1VJ1R_SSWOtb7p9phAKw3_aLG8,575
5
+ hostinger_sdk/config.py,sha256=q0irN94VDN-htgl5YLZZG78tLxfvyMzb6lD1x9hNECs,3598
6
+ hostinger_sdk/errors.py,sha256=AcLz0txQ43PFY2ruRV6QRaFBwYcrRNYvYUSOIA5SsrM,4055
7
+ hostinger_sdk/messages.py,sha256=Us_YoaNd64bApY58c9gbphy22V8IZ9SKmJOepMGo5WU,1635
8
+ hostinger_sdk/messages.yaml,sha256=1PGMIQMc1FlMXhUw01_vAeeJWZJAytmmEWcq5AP77nY,1525
9
+ hostinger_sdk/models.py,sha256=LOjY1sAciFXejHqWkzSwCu_GChR0iQFlU9cqYTjuMAQ,8395
10
+ hostinger_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ hostinger_sdk/resources/__init__.py,sha256=R6kCIj55V8rtdoa3WVzptinalfsWbEDci_HTYV-8dAk,164
12
+ hostinger_sdk/resources/dns.py,sha256=3dCXyeTi9_ud0cqnFq9LiVTnyDLAueHL4K9RqF2zEKg,8567
13
+ hostinger_sdk/resources/domains.py,sha256=I8bq6hKGJlSbYhJtso-DBIrRqDt-YsGkr7RGGCsAE5o,4219
14
+ hostinger_python_sdk-0.1.0.dist-info/METADATA,sha256=HCWFbvtpr_Zx19FogaTV3TQ31xnswUEuK_yySJ_bbvI,13597
15
+ hostinger_python_sdk-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
16
+ hostinger_python_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=ENVL-_Eh4n1SumZuLuTjO2K0OOL_eXh4pkeme35b02I,1064
17
+ hostinger_python_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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,47 @@
1
+ from . import messages
2
+ from ._version import __version__
3
+ from .client import AsyncHostinger, Hostinger
4
+ from .config import Settings, load_settings
5
+ from .errors import (
6
+ APIConnectionError,
7
+ APIStatusError,
8
+ APITimeoutError,
9
+ AuthenticationError,
10
+ ConflictError,
11
+ FieldError,
12
+ HostingerError,
13
+ NotFoundError,
14
+ PermissionDeniedError,
15
+ RateLimitError,
16
+ ServerError,
17
+ ValidationError,
18
+ )
19
+ from .models import Availability, DNSRecord, Domain, DomainInfo, Forwarding, RecordType, Snapshot
20
+
21
+ __all__ = [
22
+ "__version__",
23
+ "Hostinger",
24
+ "AsyncHostinger",
25
+ "Settings",
26
+ "load_settings",
27
+ "messages",
28
+ "Availability",
29
+ "DNSRecord",
30
+ "Domain",
31
+ "DomainInfo",
32
+ "Forwarding",
33
+ "RecordType",
34
+ "Snapshot",
35
+ "HostingerError",
36
+ "APIConnectionError",
37
+ "APITimeoutError",
38
+ "APIStatusError",
39
+ "ValidationError",
40
+ "AuthenticationError",
41
+ "PermissionDeniedError",
42
+ "NotFoundError",
43
+ "ConflictError",
44
+ "RateLimitError",
45
+ "ServerError",
46
+ "FieldError",
47
+ ]