hostinger-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.
- hostinger_python_sdk-0.1.0/.gitignore +14 -0
- hostinger_python_sdk-0.1.0/CHANGELOG.md +18 -0
- hostinger_python_sdk-0.1.0/LICENSE +21 -0
- hostinger_python_sdk-0.1.0/PKG-INFO +366 -0
- hostinger_python_sdk-0.1.0/README.md +324 -0
- hostinger_python_sdk-0.1.0/examples/hostinger.toml +4 -0
- hostinger_python_sdk-0.1.0/examples/subdomain.py +28 -0
- hostinger_python_sdk-0.1.0/pyproject.toml +65 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/__init__.py +47 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/_base.py +246 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/_version.py +1 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/client.py +20 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/config.py +75 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/errors.py +151 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/messages.py +63 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/messages.yaml +28 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/models.py +252 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/py.typed +0 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/resources/__init__.py +4 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/resources/dns.py +196 -0
- hostinger_python_sdk-0.1.0/src/hostinger_sdk/resources/domains.py +97 -0
- hostinger_python_sdk-0.1.0/tests/conftest.py +61 -0
- hostinger_python_sdk-0.1.0/tests/test_async.py +27 -0
- hostinger_python_sdk-0.1.0/tests/test_client.py +98 -0
- hostinger_python_sdk-0.1.0/tests/test_dns.py +126 -0
- hostinger_python_sdk-0.1.0/tests/test_domains.py +74 -0
- hostinger_python_sdk-0.1.0/tests/test_messages.py +21 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Sync `Hostinger` and async `AsyncHostinger` clients
|
|
8
|
+
- DNS: list, exists, add, replace, replace_many, delete (whole set or one value), delete_many
|
|
9
|
+
- DNS shortcuts: point, unpoint, alias, set_txt
|
|
10
|
+
- DNS validate, reset, snapshots, snapshot and restore
|
|
11
|
+
- Domains: list, get, available, set_nameservers, lock, unlock, enable_privacy, disable_privacy
|
|
12
|
+
- Domain forwarding: forwarding, forward, stop_forwarding
|
|
13
|
+
- Record sets are flattened into one `DNSRecord` per value and grouped again on write
|
|
14
|
+
- `request()` for any other Hostinger endpoint
|
|
15
|
+
- Typed errors with field details and the correlation id, automatic retries for rate limits and server errors
|
|
16
|
+
- Configuration through goodconf: code, `HOSTINGER_*` environment variables or a TOML, YAML or JSON file
|
|
17
|
+
- Every user facing message lives in `messages.yaml` and can be overridden at runtime
|
|
18
|
+
- 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,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
|