domainkits 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DomainKits
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,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: domainkits
3
+ Version: 0.1.0
4
+ Summary: Python client for the DomainKits REST API. Search expiring, newly registered, aged, active, deleted and marketplace domains, plus WHOIS, DNS, Certificate Transparency and trends.
5
+ Author-email: DomainKits <info@domainkits.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://domainkits.com/dev
8
+ Project-URL: Documentation, https://domainkits.com/dev/api-docs
9
+ Project-URL: Repository, https://github.com/ABTdomain/domainkits-sdk-py
10
+ Keywords: domain,domain-search,expired-domains,newly-registered-domains,whois,dns,certificate-transparency,domainkits
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ # domainkits
26
+
27
+ Python client for the [DomainKits](https://domainkits.com) REST API.
28
+
29
+ DomainKits is one API with a shared key across every endpoint. This package covers all of them, six domain search types, WHOIS, DNS, reverse nameserver, Certificate Transparency, safety, trends and bulk download, with automatic paging and structured quota errors. No dependencies.
30
+
31
+ ## Requirements
32
+
33
+ The REST API is for Premium and Platinum accounts; unauthenticated requests are rejected with 401. Keys start with `dk_` and come from [domainkits.com](https://domainkits.com/pricing).
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install domainkits
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ import os
45
+ from domainkits import DomainKits
46
+
47
+ dk = DomainKits(os.environ["DOMAINKITS_API_KEY"])
48
+
49
+ result = dk.expired.list(
50
+ keyword="clinic",
51
+ tld="com",
52
+ length="5-10",
53
+ no_number=True,
54
+ no_hyphen=True,
55
+ )
56
+
57
+ print(f"{result['total']} matches")
58
+ for d in result["data"]:
59
+ print(d["domain"], d["age"], d["status"])
60
+ ```
61
+
62
+ ### Paging
63
+
64
+ A single request returns at most 500 results. `paginate` walks the whole result set for you:
65
+
66
+ ```python
67
+ for domain in dk.expired.paginate(keyword="clinic", tld="com"):
68
+ print(domain["domain"])
69
+ ```
70
+
71
+ It stops when the result set is exhausted. Break out of the loop whenever you have enough; no further requests are made.
72
+
73
+ ### Export
74
+
75
+ `export` pulls up to 50,000 rows as CSV in one request:
76
+
77
+ ```python
78
+ csv = dk.expired.export(tld="com", status="pending_delete")
79
+ ```
80
+
81
+ This runs on a separate, much smaller quota, 10 per day and 100 per month on Premium, 3 and 9 during the trial. It is for occasional bulk pulls, not for a scheduled job. The export also returns fewer columns than paged mode: `registered_date` is the year only, and `age` is omitted.
82
+
83
+ 50,000 is a cap, not a promise of completeness. Browsing `.com` matched 4,847,613 expiring domains on 27 July 2026, so an unfiltered export returns the first 50,000. Narrow the query if you need the result set to fit.
84
+
85
+ ## Search types
86
+
87
+ | Attribute | What it searches |
88
+ |---|---|
89
+ | `dk.expired` | Domains in the deletion cycle: expired, redemption, pending delete |
90
+ | `dk.nrds` | Newly registered domains, last 60 days |
91
+ | `dk.aged` | Domains with 5 to 20+ years of registration history |
92
+ | `dk.active` | Currently registered domains |
93
+ | `dk.deleted` | Dropped domains (requires `keyword`) |
94
+ | `dk.market` | Domains listed for sale on marketplaces |
95
+
96
+ Each has `list`, `paginate` and `export`. Filters are passed as keyword arguments and match the REST parameter names: an expired result carries `status`, an NRD result carries `expiry_date`, a market result carries `marketplace`.
97
+
98
+ ## Other endpoints
99
+
100
+ ```python
101
+ dk.whois("example.com")
102
+ dk.dns("example.com")
103
+ dk.safety("example.com")
104
+ dk.ns_reverse("ns1.example.com", tld="com")
105
+ dk.tld_check("yourbrand")
106
+ dk.typosquat("example.com")
107
+ dk.ip_lookup("8.8.8.8")
108
+ dk.registrar("godaddy")
109
+ dk.status_guide("clientHold")
110
+ dk.monitor_changes(tld="com", reason="transfer")
111
+ dk.ct_subdomains("example.com")
112
+ dk.ct_certs(domain="example.com")
113
+ dk.ct_search("example")
114
+ dk.tld_trends("newly", tld="com")
115
+ dk.keyword_trends("hot")
116
+ dk.usage()
117
+ dk.search_status()
118
+ ```
119
+
120
+ ## Coverage
121
+
122
+ **gTLDs only** for the domain search endpoints. The index covers generic TLDs: `.com`, `.net`, `.org`, `.info`, `.biz`, `.xyz`, `.online`, `.site`, `.top`, `.club`, `.live`, `.app`, `.dev` and others. Country-code TLDs are not indexed: a query for `.de`, `.io`, `.co` or `.us` returns an empty result set, not an error.
123
+
124
+ `whois`, `dns`, `safety`, `ip_lookup` and the Certificate Transparency endpoints work on any domain, ccTLDs included.
125
+
126
+ **No PII.** Responses contain no personal data. WHOIS results are limited to registrar, dates, status codes and nameservers; registrant names, emails, addresses and phone numbers are not returned.
127
+
128
+ ## Errors
129
+
130
+ ```python
131
+ from domainkits import DomainKits, DomainKitsError, RateLimitError, AuthError
132
+
133
+ try:
134
+ dk.expired.export(tld="com")
135
+ except RateLimitError as error:
136
+ print(f"Quota exhausted. Retry in {error.retry_after_ms}ms")
137
+ print(error.rate_limit)
138
+ except AuthError:
139
+ print("Key rejected, check your plan tier")
140
+ except DomainKitsError as error:
141
+ print(error.status, error)
142
+ ```
143
+
144
+ Every error carries the `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` values as a parsed `rate_limit` object. `RateLimitError.retry_after_ms` tells you how long until the window resets.
145
+
146
+ 429 and 5xx responses are retried automatically, twice by default, waiting until the rate-limit window resets when that is under two minutes. Set `max_retries=0` to handle it yourself.
147
+
148
+ ## Options
149
+
150
+ ```python
151
+ dk = DomainKits(
152
+ api_key=os.environ["DOMAINKITS_API_KEY"],
153
+ timeout=60.0,
154
+ max_retries=2,
155
+ base_url="https://premium-api.domainkits.com/api/v1",
156
+ )
157
+ ```
158
+
159
+ ## Quotas
160
+
161
+ Call `usage()` for the live picture on your account; every endpoint reports its own per-minute, daily and monthly allowance alongside what you have already spent.
162
+
163
+ Daily quotas reset at 00:00 UTC, monthly quotas on the 1st. Current limits: [domainkits.com/dev/api-docs](https://domainkits.com/dev/api-docs).
164
+
165
+ ## Resources
166
+
167
+ - [DomainKits API reference](https://domainkits.com/dev/api-docs)
168
+ - [@domainkits/sdk](https://www.npmjs.com/package/@domainkits/sdk), the same API for TypeScript
169
+ - [n8n-nodes-domainkits](https://www.npmjs.com/package/n8n-nodes-domainkits), the same API for n8n
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,149 @@
1
+ # domainkits
2
+
3
+ Python client for the [DomainKits](https://domainkits.com) REST API.
4
+
5
+ DomainKits is one API with a shared key across every endpoint. This package covers all of them, six domain search types, WHOIS, DNS, reverse nameserver, Certificate Transparency, safety, trends and bulk download, with automatic paging and structured quota errors. No dependencies.
6
+
7
+ ## Requirements
8
+
9
+ The REST API is for Premium and Platinum accounts; unauthenticated requests are rejected with 401. Keys start with `dk_` and come from [domainkits.com](https://domainkits.com/pricing).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install domainkits
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ import os
21
+ from domainkits import DomainKits
22
+
23
+ dk = DomainKits(os.environ["DOMAINKITS_API_KEY"])
24
+
25
+ result = dk.expired.list(
26
+ keyword="clinic",
27
+ tld="com",
28
+ length="5-10",
29
+ no_number=True,
30
+ no_hyphen=True,
31
+ )
32
+
33
+ print(f"{result['total']} matches")
34
+ for d in result["data"]:
35
+ print(d["domain"], d["age"], d["status"])
36
+ ```
37
+
38
+ ### Paging
39
+
40
+ A single request returns at most 500 results. `paginate` walks the whole result set for you:
41
+
42
+ ```python
43
+ for domain in dk.expired.paginate(keyword="clinic", tld="com"):
44
+ print(domain["domain"])
45
+ ```
46
+
47
+ It stops when the result set is exhausted. Break out of the loop whenever you have enough; no further requests are made.
48
+
49
+ ### Export
50
+
51
+ `export` pulls up to 50,000 rows as CSV in one request:
52
+
53
+ ```python
54
+ csv = dk.expired.export(tld="com", status="pending_delete")
55
+ ```
56
+
57
+ This runs on a separate, much smaller quota, 10 per day and 100 per month on Premium, 3 and 9 during the trial. It is for occasional bulk pulls, not for a scheduled job. The export also returns fewer columns than paged mode: `registered_date` is the year only, and `age` is omitted.
58
+
59
+ 50,000 is a cap, not a promise of completeness. Browsing `.com` matched 4,847,613 expiring domains on 27 July 2026, so an unfiltered export returns the first 50,000. Narrow the query if you need the result set to fit.
60
+
61
+ ## Search types
62
+
63
+ | Attribute | What it searches |
64
+ |---|---|
65
+ | `dk.expired` | Domains in the deletion cycle: expired, redemption, pending delete |
66
+ | `dk.nrds` | Newly registered domains, last 60 days |
67
+ | `dk.aged` | Domains with 5 to 20+ years of registration history |
68
+ | `dk.active` | Currently registered domains |
69
+ | `dk.deleted` | Dropped domains (requires `keyword`) |
70
+ | `dk.market` | Domains listed for sale on marketplaces |
71
+
72
+ Each has `list`, `paginate` and `export`. Filters are passed as keyword arguments and match the REST parameter names: an expired result carries `status`, an NRD result carries `expiry_date`, a market result carries `marketplace`.
73
+
74
+ ## Other endpoints
75
+
76
+ ```python
77
+ dk.whois("example.com")
78
+ dk.dns("example.com")
79
+ dk.safety("example.com")
80
+ dk.ns_reverse("ns1.example.com", tld="com")
81
+ dk.tld_check("yourbrand")
82
+ dk.typosquat("example.com")
83
+ dk.ip_lookup("8.8.8.8")
84
+ dk.registrar("godaddy")
85
+ dk.status_guide("clientHold")
86
+ dk.monitor_changes(tld="com", reason="transfer")
87
+ dk.ct_subdomains("example.com")
88
+ dk.ct_certs(domain="example.com")
89
+ dk.ct_search("example")
90
+ dk.tld_trends("newly", tld="com")
91
+ dk.keyword_trends("hot")
92
+ dk.usage()
93
+ dk.search_status()
94
+ ```
95
+
96
+ ## Coverage
97
+
98
+ **gTLDs only** for the domain search endpoints. The index covers generic TLDs: `.com`, `.net`, `.org`, `.info`, `.biz`, `.xyz`, `.online`, `.site`, `.top`, `.club`, `.live`, `.app`, `.dev` and others. Country-code TLDs are not indexed: a query for `.de`, `.io`, `.co` or `.us` returns an empty result set, not an error.
99
+
100
+ `whois`, `dns`, `safety`, `ip_lookup` and the Certificate Transparency endpoints work on any domain, ccTLDs included.
101
+
102
+ **No PII.** Responses contain no personal data. WHOIS results are limited to registrar, dates, status codes and nameservers; registrant names, emails, addresses and phone numbers are not returned.
103
+
104
+ ## Errors
105
+
106
+ ```python
107
+ from domainkits import DomainKits, DomainKitsError, RateLimitError, AuthError
108
+
109
+ try:
110
+ dk.expired.export(tld="com")
111
+ except RateLimitError as error:
112
+ print(f"Quota exhausted. Retry in {error.retry_after_ms}ms")
113
+ print(error.rate_limit)
114
+ except AuthError:
115
+ print("Key rejected, check your plan tier")
116
+ except DomainKitsError as error:
117
+ print(error.status, error)
118
+ ```
119
+
120
+ Every error carries the `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` values as a parsed `rate_limit` object. `RateLimitError.retry_after_ms` tells you how long until the window resets.
121
+
122
+ 429 and 5xx responses are retried automatically, twice by default, waiting until the rate-limit window resets when that is under two minutes. Set `max_retries=0` to handle it yourself.
123
+
124
+ ## Options
125
+
126
+ ```python
127
+ dk = DomainKits(
128
+ api_key=os.environ["DOMAINKITS_API_KEY"],
129
+ timeout=60.0,
130
+ max_retries=2,
131
+ base_url="https://premium-api.domainkits.com/api/v1",
132
+ )
133
+ ```
134
+
135
+ ## Quotas
136
+
137
+ Call `usage()` for the live picture on your account; every endpoint reports its own per-minute, daily and monthly allowance alongside what you have already spent.
138
+
139
+ Daily quotas reset at 00:00 UTC, monthly quotas on the 1st. Current limits: [domainkits.com/dev/api-docs](https://domainkits.com/dev/api-docs).
140
+
141
+ ## Resources
142
+
143
+ - [DomainKits API reference](https://domainkits.com/dev/api-docs)
144
+ - [@domainkits/sdk](https://www.npmjs.com/package/@domainkits/sdk), the same API for TypeScript
145
+ - [n8n-nodes-domainkits](https://www.npmjs.com/package/n8n-nodes-domainkits), the same API for n8n
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterator
4
+
5
+ from .client import (
6
+ AuthError,
7
+ DomainKitsClient,
8
+ DomainKitsError,
9
+ RateLimit,
10
+ RateLimitError,
11
+ )
12
+
13
+ __version__ = "0.1.0"
14
+ __all__ = [
15
+ "DomainKits",
16
+ "DomainKitsError",
17
+ "RateLimitError",
18
+ "AuthError",
19
+ "RateLimit",
20
+ "SearchResource",
21
+ ]
22
+
23
+ MAX_LIMIT = 500
24
+
25
+
26
+ class SearchResource:
27
+ def __init__(self, client: DomainKitsClient, path: str):
28
+ self._client = client
29
+ self._path = path
30
+
31
+ def list(self, **params: Any) -> dict[str, Any]:
32
+ return self._client.request_list(self._path, params)
33
+
34
+ def paginate(self, page_size: int = MAX_LIMIT, **params: Any) -> Iterator[dict[str, Any]]:
35
+ offset = int(params.pop("offset", 0) or 0)
36
+ limit = min(page_size, MAX_LIMIT)
37
+ while True:
38
+ result = self._client.request_list(self._path, {**params, "limit": limit, "offset": offset})
39
+ data = result["data"]
40
+ yield from data
41
+ offset += len(data)
42
+ if not data or len(data) < limit or offset >= result["total"]:
43
+ return
44
+
45
+ def export(self, **params: Any) -> str:
46
+ _, _, body = self._client.request_raw(self._path, {**params, "export": "csv"})
47
+ return body.decode("utf-8")
48
+
49
+
50
+ class DomainKits:
51
+ def __init__(
52
+ self,
53
+ api_key: str,
54
+ base_url: str | None = None,
55
+ timeout: float = 60.0,
56
+ max_retries: int = 2,
57
+ ):
58
+ self._client = DomainKitsClient(api_key, base_url=base_url, timeout=timeout, max_retries=max_retries)
59
+
60
+ self.expired = SearchResource(self._client, "/search/expired")
61
+ self.nrds = SearchResource(self._client, "/search/nrds")
62
+ self.aged = SearchResource(self._client, "/search/aged")
63
+ self.active = SearchResource(self._client, "/search/active")
64
+ self.deleted = SearchResource(self._client, "/search/deleted")
65
+ self.market = SearchResource(self._client, "/search/market")
66
+
67
+ def usage(self) -> dict[str, Any]:
68
+ return self._client.request("/usage")
69
+
70
+ def search_status(self) -> dict[str, Any]:
71
+ return self._client.request("/search/status")
72
+
73
+ def health(self) -> dict[str, Any]:
74
+ return self._client.request_envelope("/health")
75
+
76
+ def whois(self, domain: str) -> dict[str, Any]:
77
+ return self._client.request("/whois", {"domain": domain})
78
+
79
+ def dns(self, domain: str) -> dict[str, Any]:
80
+ return self._client.request("/dns", {"domain": domain})
81
+
82
+ def safety(self, domain: str) -> dict[str, Any]:
83
+ return self._client.request("/safety", {"domain": domain})
84
+
85
+ def ip_lookup(self, query: str) -> dict[str, Any]:
86
+ return self._client.request("/ip-lookup", {"query": query})
87
+
88
+ def registrar(self, query: str) -> dict[str, Any]:
89
+ return self._client.request("/registrar", {"query": query})
90
+
91
+ def status_guide(self, query: str | None = None) -> dict[str, Any]:
92
+ return self._client.request("/status-guide", {"query": query})
93
+
94
+ def tld_check(self, prefix: str, **params: Any) -> dict[str, Any]:
95
+ return self._client.request_envelope("/tld-check", {"prefix": prefix, **params})
96
+
97
+ def ns_reverse(self, ns: str, **params: Any) -> dict[str, Any]:
98
+ return self._client.request_list("/ns-reverse", {"ns": ns, **params})
99
+
100
+ def typosquat(self, domain: str, **params: Any) -> dict[str, Any]:
101
+ return self._client.request_envelope("/typosquat", {"domain": domain, **params})
102
+
103
+ def monitor_changes(self, **params: Any) -> dict[str, Any]:
104
+ return self._client.request_list("/monitor/changes", params)
105
+
106
+ def ct_subdomains(self, domain: str, **params: Any) -> dict[str, Any]:
107
+ return self._client.request_list("/ct/subdomains", {"domain": domain, **params})
108
+
109
+ def ct_certs(self, **params: Any) -> dict[str, Any]:
110
+ return self._client.request_list("/ct/certs", params)
111
+
112
+ def ct_search(self, keyword: str, **params: Any) -> dict[str, Any]:
113
+ return self._client.request_list("/ct/search", {"keyword": keyword, **params})
114
+
115
+ def tld_trends(self, type: str, **params: Any) -> Any:
116
+ return self._client.request(f"/trends/tlds/{type}", params)
117
+
118
+ def keyword_trends(self, type: str, **params: Any) -> Any:
119
+ return self._client.request(f"/trends/keywords/{type}", params)
120
+
121
+ def nrds_download(self, **params: Any) -> bytes:
122
+ _, _, body = self._client.request_raw("/nrds/download", params)
123
+ return body
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timezone
10
+ from typing import Any
11
+
12
+ DEFAULT_BASE_URL = "https://premium-api.domainkits.com/api/v1"
13
+ DEFAULT_TIMEOUT = 60.0
14
+ DEFAULT_MAX_RETRIES = 2
15
+
16
+
17
+ @dataclass
18
+ class RateLimit:
19
+ limit: int | None
20
+ remaining: int | None
21
+ reset_at: datetime | None
22
+
23
+
24
+ class DomainKitsError(Exception):
25
+ def __init__(self, message: str, status: int, rate_limit: RateLimit):
26
+ super().__init__(message)
27
+ self.status = status
28
+ self.rate_limit = rate_limit
29
+
30
+
31
+ class RateLimitError(DomainKitsError):
32
+ def __init__(self, message: str, rate_limit: RateLimit):
33
+ super().__init__(message, 429, rate_limit)
34
+
35
+ @property
36
+ def retry_after_ms(self) -> int | None:
37
+ if not self.rate_limit.reset_at:
38
+ return None
39
+ delta = (self.rate_limit.reset_at - datetime.now(timezone.utc)).total_seconds() * 1000
40
+ return max(0, int(delta))
41
+
42
+
43
+ class AuthError(DomainKitsError):
44
+ pass
45
+
46
+
47
+ def _read_rate_limit(headers: Any) -> RateLimit:
48
+ def num(name: str) -> int | None:
49
+ raw = headers.get(name)
50
+ if raw is None:
51
+ return None
52
+ try:
53
+ return int(raw)
54
+ except ValueError:
55
+ return None
56
+
57
+ reset = num("x-ratelimit-reset")
58
+ return RateLimit(
59
+ limit=num("x-ratelimit-limit"),
60
+ remaining=num("x-ratelimit-remaining"),
61
+ reset_at=datetime.fromtimestamp(reset, tz=timezone.utc) if reset is not None else None,
62
+ )
63
+
64
+
65
+ def _extract_error(body: bytes) -> str | None:
66
+ try:
67
+ parsed = json.loads(body)
68
+ except (ValueError, UnicodeDecodeError):
69
+ return None
70
+ error = parsed.get("error") if isinstance(parsed, dict) else None
71
+ return error if isinstance(error, str) else None
72
+
73
+
74
+ def _backoff_seconds(error: RateLimitError, attempt: int) -> float | None:
75
+ until_ms = error.retry_after_ms
76
+ if until_ms is None:
77
+ return float(2**attempt)
78
+ if until_ms > 120_000:
79
+ return None
80
+ return until_ms / 1000 + 0.25
81
+
82
+
83
+ class DomainKitsClient:
84
+ def __init__(
85
+ self,
86
+ api_key: str,
87
+ base_url: str | None = None,
88
+ timeout: float = DEFAULT_TIMEOUT,
89
+ max_retries: int = DEFAULT_MAX_RETRIES,
90
+ ):
91
+ if not api_key:
92
+ raise ValueError("api_key is required. The DomainKits REST API rejects unauthenticated requests.")
93
+ self.api_key = api_key
94
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
95
+ self.timeout = timeout
96
+ self.max_retries = max_retries
97
+
98
+ def request_raw(self, path: str, params: dict[str, Any] | None = None) -> tuple[int, Any, bytes]:
99
+ query: dict[str, str] = {}
100
+ for key, value in (params or {}).items():
101
+ if value is None or value == "":
102
+ continue
103
+ if isinstance(value, bool):
104
+ query[key] = "true" if value else "false"
105
+ elif isinstance(value, (list, tuple)):
106
+ query[key] = ",".join(str(v) for v in value)
107
+ else:
108
+ query[key] = str(value)
109
+ url = self.base_url + path
110
+ if query:
111
+ url += "?" + urllib.parse.urlencode(query)
112
+
113
+ last_error: DomainKitsError | None = None
114
+ for attempt in range(self.max_retries + 1):
115
+ request = urllib.request.Request(
116
+ url,
117
+ headers={
118
+ "Authorization": f"Bearer {self.api_key}",
119
+ "Accept": "application/json",
120
+ "User-Agent": "domainkits-python/0.1.0",
121
+ },
122
+ )
123
+ try:
124
+ response = urllib.request.urlopen(request, timeout=self.timeout)
125
+ return response.status, response.headers, response.read()
126
+ except urllib.error.HTTPError as e:
127
+ body = e.read()
128
+ rate_limit = _read_rate_limit(e.headers)
129
+ message = _extract_error(body) or f"Request failed with status {e.code}"
130
+
131
+ if e.code in (401, 403):
132
+ raise AuthError(message, e.code, rate_limit) from None
133
+
134
+ if e.code == 429:
135
+ rate_error = RateLimitError(message, rate_limit)
136
+ last_error = rate_error
137
+ wait = _backoff_seconds(rate_error, attempt)
138
+ if attempt < self.max_retries and wait is not None:
139
+ time.sleep(wait)
140
+ continue
141
+ raise rate_error from None
142
+
143
+ if e.code >= 500 and attempt < self.max_retries:
144
+ last_error = DomainKitsError(message, e.code, rate_limit)
145
+ time.sleep(0.5 * (2**attempt))
146
+ continue
147
+
148
+ raise DomainKitsError(message, e.code, rate_limit) from None
149
+
150
+ raise last_error if last_error else DomainKitsError(
151
+ "Request failed after retries", 0, RateLimit(None, None, None)
152
+ )
153
+
154
+ def _parse(self, path: str, params: dict[str, Any] | None) -> tuple[Any, int, Any]:
155
+ status, headers, body = self.request_raw(path, params)
156
+ envelope = json.loads(body)
157
+ if isinstance(envelope, dict) and envelope.get("success") is False:
158
+ raise DomainKitsError(
159
+ envelope.get("error") or "DomainKits API returned an error",
160
+ status,
161
+ _read_rate_limit(headers),
162
+ )
163
+ return envelope, status, headers
164
+
165
+ def request(self, path: str, params: dict[str, Any] | None = None) -> Any:
166
+ envelope, _, _ = self._parse(path, params)
167
+ if isinstance(envelope, dict) and envelope.get("data") is not None:
168
+ return envelope["data"]
169
+ return envelope
170
+
171
+ def request_list(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
172
+ envelope, _, _ = self._parse(path, params)
173
+ data = envelope.get("data") if isinstance(envelope, dict) else None
174
+ if not isinstance(data, list):
175
+ data = []
176
+ total = envelope.get("total") if isinstance(envelope, dict) else None
177
+ return {"data": data, "total": total if isinstance(total, int) else len(data)}
178
+
179
+ def request_envelope(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
180
+ envelope, _, _ = self._parse(path, params)
181
+ return envelope
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: domainkits
3
+ Version: 0.1.0
4
+ Summary: Python client for the DomainKits REST API. Search expiring, newly registered, aged, active, deleted and marketplace domains, plus WHOIS, DNS, Certificate Transparency and trends.
5
+ Author-email: DomainKits <info@domainkits.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://domainkits.com/dev
8
+ Project-URL: Documentation, https://domainkits.com/dev/api-docs
9
+ Project-URL: Repository, https://github.com/ABTdomain/domainkits-sdk-py
10
+ Keywords: domain,domain-search,expired-domains,newly-registered-domains,whois,dns,certificate-transparency,domainkits
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ # domainkits
26
+
27
+ Python client for the [DomainKits](https://domainkits.com) REST API.
28
+
29
+ DomainKits is one API with a shared key across every endpoint. This package covers all of them, six domain search types, WHOIS, DNS, reverse nameserver, Certificate Transparency, safety, trends and bulk download, with automatic paging and structured quota errors. No dependencies.
30
+
31
+ ## Requirements
32
+
33
+ The REST API is for Premium and Platinum accounts; unauthenticated requests are rejected with 401. Keys start with `dk_` and come from [domainkits.com](https://domainkits.com/pricing).
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install domainkits
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ import os
45
+ from domainkits import DomainKits
46
+
47
+ dk = DomainKits(os.environ["DOMAINKITS_API_KEY"])
48
+
49
+ result = dk.expired.list(
50
+ keyword="clinic",
51
+ tld="com",
52
+ length="5-10",
53
+ no_number=True,
54
+ no_hyphen=True,
55
+ )
56
+
57
+ print(f"{result['total']} matches")
58
+ for d in result["data"]:
59
+ print(d["domain"], d["age"], d["status"])
60
+ ```
61
+
62
+ ### Paging
63
+
64
+ A single request returns at most 500 results. `paginate` walks the whole result set for you:
65
+
66
+ ```python
67
+ for domain in dk.expired.paginate(keyword="clinic", tld="com"):
68
+ print(domain["domain"])
69
+ ```
70
+
71
+ It stops when the result set is exhausted. Break out of the loop whenever you have enough; no further requests are made.
72
+
73
+ ### Export
74
+
75
+ `export` pulls up to 50,000 rows as CSV in one request:
76
+
77
+ ```python
78
+ csv = dk.expired.export(tld="com", status="pending_delete")
79
+ ```
80
+
81
+ This runs on a separate, much smaller quota, 10 per day and 100 per month on Premium, 3 and 9 during the trial. It is for occasional bulk pulls, not for a scheduled job. The export also returns fewer columns than paged mode: `registered_date` is the year only, and `age` is omitted.
82
+
83
+ 50,000 is a cap, not a promise of completeness. Browsing `.com` matched 4,847,613 expiring domains on 27 July 2026, so an unfiltered export returns the first 50,000. Narrow the query if you need the result set to fit.
84
+
85
+ ## Search types
86
+
87
+ | Attribute | What it searches |
88
+ |---|---|
89
+ | `dk.expired` | Domains in the deletion cycle: expired, redemption, pending delete |
90
+ | `dk.nrds` | Newly registered domains, last 60 days |
91
+ | `dk.aged` | Domains with 5 to 20+ years of registration history |
92
+ | `dk.active` | Currently registered domains |
93
+ | `dk.deleted` | Dropped domains (requires `keyword`) |
94
+ | `dk.market` | Domains listed for sale on marketplaces |
95
+
96
+ Each has `list`, `paginate` and `export`. Filters are passed as keyword arguments and match the REST parameter names: an expired result carries `status`, an NRD result carries `expiry_date`, a market result carries `marketplace`.
97
+
98
+ ## Other endpoints
99
+
100
+ ```python
101
+ dk.whois("example.com")
102
+ dk.dns("example.com")
103
+ dk.safety("example.com")
104
+ dk.ns_reverse("ns1.example.com", tld="com")
105
+ dk.tld_check("yourbrand")
106
+ dk.typosquat("example.com")
107
+ dk.ip_lookup("8.8.8.8")
108
+ dk.registrar("godaddy")
109
+ dk.status_guide("clientHold")
110
+ dk.monitor_changes(tld="com", reason="transfer")
111
+ dk.ct_subdomains("example.com")
112
+ dk.ct_certs(domain="example.com")
113
+ dk.ct_search("example")
114
+ dk.tld_trends("newly", tld="com")
115
+ dk.keyword_trends("hot")
116
+ dk.usage()
117
+ dk.search_status()
118
+ ```
119
+
120
+ ## Coverage
121
+
122
+ **gTLDs only** for the domain search endpoints. The index covers generic TLDs: `.com`, `.net`, `.org`, `.info`, `.biz`, `.xyz`, `.online`, `.site`, `.top`, `.club`, `.live`, `.app`, `.dev` and others. Country-code TLDs are not indexed: a query for `.de`, `.io`, `.co` or `.us` returns an empty result set, not an error.
123
+
124
+ `whois`, `dns`, `safety`, `ip_lookup` and the Certificate Transparency endpoints work on any domain, ccTLDs included.
125
+
126
+ **No PII.** Responses contain no personal data. WHOIS results are limited to registrar, dates, status codes and nameservers; registrant names, emails, addresses and phone numbers are not returned.
127
+
128
+ ## Errors
129
+
130
+ ```python
131
+ from domainkits import DomainKits, DomainKitsError, RateLimitError, AuthError
132
+
133
+ try:
134
+ dk.expired.export(tld="com")
135
+ except RateLimitError as error:
136
+ print(f"Quota exhausted. Retry in {error.retry_after_ms}ms")
137
+ print(error.rate_limit)
138
+ except AuthError:
139
+ print("Key rejected, check your plan tier")
140
+ except DomainKitsError as error:
141
+ print(error.status, error)
142
+ ```
143
+
144
+ Every error carries the `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` values as a parsed `rate_limit` object. `RateLimitError.retry_after_ms` tells you how long until the window resets.
145
+
146
+ 429 and 5xx responses are retried automatically, twice by default, waiting until the rate-limit window resets when that is under two minutes. Set `max_retries=0` to handle it yourself.
147
+
148
+ ## Options
149
+
150
+ ```python
151
+ dk = DomainKits(
152
+ api_key=os.environ["DOMAINKITS_API_KEY"],
153
+ timeout=60.0,
154
+ max_retries=2,
155
+ base_url="https://premium-api.domainkits.com/api/v1",
156
+ )
157
+ ```
158
+
159
+ ## Quotas
160
+
161
+ Call `usage()` for the live picture on your account; every endpoint reports its own per-minute, daily and monthly allowance alongside what you have already spent.
162
+
163
+ Daily quotas reset at 00:00 UTC, monthly quotas on the 1st. Current limits: [domainkits.com/dev/api-docs](https://domainkits.com/dev/api-docs).
164
+
165
+ ## Resources
166
+
167
+ - [DomainKits API reference](https://domainkits.com/dev/api-docs)
168
+ - [@domainkits/sdk](https://www.npmjs.com/package/@domainkits/sdk), the same API for TypeScript
169
+ - [n8n-nodes-domainkits](https://www.npmjs.com/package/n8n-nodes-domainkits), the same API for n8n
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ domainkits/__init__.py
5
+ domainkits/client.py
6
+ domainkits.egg-info/PKG-INFO
7
+ domainkits.egg-info/SOURCES.txt
8
+ domainkits.egg-info/dependency_links.txt
9
+ domainkits.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ domainkits
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "domainkits"
7
+ version = "0.1.0"
8
+ description = "Python client for the DomainKits REST API. Search expiring, newly registered, aged, active, deleted and marketplace domains, plus WHOIS, DNS, Certificate Transparency and trends."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ {name = "DomainKits", email = "info@domainkits.com"}
14
+ ]
15
+ keywords = ["domain", "domain-search", "expired-domains", "newly-registered-domains", "whois", "dns", "certificate-transparency", "domainkits"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Internet :: WWW/HTTP",
26
+ ]
27
+ requires-python = ">=3.9"
28
+ dependencies = []
29
+
30
+ [project.urls]
31
+ Homepage = "https://domainkits.com/dev"
32
+ Documentation = "https://domainkits.com/dev/api-docs"
33
+ Repository = "https://github.com/ABTdomain/domainkits-sdk-py"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["."]
37
+ include = ["domainkits*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+