nameai 1.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.
- nameai-1.1.0/.gitignore +10 -0
- nameai-1.1.0/PKG-INFO +81 -0
- nameai-1.1.0/README.md +60 -0
- nameai-1.1.0/pyproject.toml +37 -0
- nameai-1.1.0/src/nameai/__init__.py +227 -0
- nameai-1.1.0/src/nameai/py.typed +0 -0
nameai-1.1.0/.gitignore
ADDED
nameai-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nameai
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Official Python SDK for the Name.ai public API: domain search/availability, WHOIS lookup, TLD registration pricing and registry requirements, marketplace listings with cursor pagination, and batched reads.
|
|
5
|
+
Project-URL: Homepage, https://name.ai
|
|
6
|
+
Project-URL: Documentation, https://name.ai/developers/api
|
|
7
|
+
Project-URL: API Specification, https://name.ai/openapi.json
|
|
8
|
+
Project-URL: Source, https://github.com/namekart/nameai_mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/namekart/nameai_mcp/issues
|
|
10
|
+
Author-email: "Name.ai" <support@name.ai>
|
|
11
|
+
License: MIT
|
|
12
|
+
Keywords: domain-search,domains,mcp,name.ai,nameai,tld,whois
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Internet :: Name Service (DNS)
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# nameai
|
|
23
|
+
|
|
24
|
+
Official Python SDK for the [Name.ai](https://name.ai/developers/api) public
|
|
25
|
+
API: domain search/availability, WHOIS, TLD pricing and registry requirements,
|
|
26
|
+
and marketplace listings. No dependencies — the whole client is `urllib` from
|
|
27
|
+
the standard library. Python ≥ 3.9.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install nameai
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from nameai import NameAI
|
|
35
|
+
|
|
36
|
+
nameai = NameAI()
|
|
37
|
+
|
|
38
|
+
nameai.tld_price("ai", "register") # {'tld': 'ai', 'op': 'register', 'priceCents': 18000}
|
|
39
|
+
nameai.search_domain("acme.ai")["results"] # availability across alternate TLDs
|
|
40
|
+
nameai.whois_lookup("example.com")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Paging
|
|
44
|
+
|
|
45
|
+
Follow the cursor rather than incrementing an offset — a cursor names the row
|
|
46
|
+
you stopped at, so a listing sold or added while you page cannot shift the
|
|
47
|
+
window under you:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
for listing in nameai.list_all_listings(tld="ai", sort="price_asc"):
|
|
51
|
+
print(listing["domain"])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Batching
|
|
55
|
+
|
|
56
|
+
Several lookups in one round trip. Each operation still costs the rate limit
|
|
57
|
+
what the individual call would have, and one failure does not fail the rest:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
response = nameai.batch([
|
|
61
|
+
{"id": "a", "op": "search_domain", "params": {"q": "acme.ai"}},
|
|
62
|
+
{"id": "b", "op": "tld_registration_price", "params": {"tld": "ai", "op": "register"}},
|
|
63
|
+
{"id": "c", "op": "whois_lookup", "params": {"domain": "example.com"}},
|
|
64
|
+
])
|
|
65
|
+
for result in response["results"]:
|
|
66
|
+
print(result["id"], result["status"])
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Errors and auth
|
|
70
|
+
|
|
71
|
+
A non-2xx raises `NameAIError` carrying the API's typed `status`, `code` and
|
|
72
|
+
`retry_after`, so you can branch on the code instead of parsing a message.
|
|
73
|
+
|
|
74
|
+
Every method works unauthenticated. Authentication buys exactly one thing —
|
|
75
|
+
real marketplace prices instead of masked ones. Get a token per
|
|
76
|
+
[name.ai/auth.md](https://name.ai/auth.md) and pass
|
|
77
|
+
`NameAI(access_token=...)`.
|
|
78
|
+
|
|
79
|
+
- API docs: https://name.ai/developers/api
|
|
80
|
+
- OpenAPI spec: https://name.ai/openapi.json
|
|
81
|
+
- MCP server (same capabilities, for agents): https://name.ai/developers/mcp
|
nameai-1.1.0/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# nameai
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Name.ai](https://name.ai/developers/api) public
|
|
4
|
+
API: domain search/availability, WHOIS, TLD pricing and registry requirements,
|
|
5
|
+
and marketplace listings. No dependencies — the whole client is `urllib` from
|
|
6
|
+
the standard library. Python ≥ 3.9.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install nameai
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from nameai import NameAI
|
|
14
|
+
|
|
15
|
+
nameai = NameAI()
|
|
16
|
+
|
|
17
|
+
nameai.tld_price("ai", "register") # {'tld': 'ai', 'op': 'register', 'priceCents': 18000}
|
|
18
|
+
nameai.search_domain("acme.ai")["results"] # availability across alternate TLDs
|
|
19
|
+
nameai.whois_lookup("example.com")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Paging
|
|
23
|
+
|
|
24
|
+
Follow the cursor rather than incrementing an offset — a cursor names the row
|
|
25
|
+
you stopped at, so a listing sold or added while you page cannot shift the
|
|
26
|
+
window under you:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
for listing in nameai.list_all_listings(tld="ai", sort="price_asc"):
|
|
30
|
+
print(listing["domain"])
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Batching
|
|
34
|
+
|
|
35
|
+
Several lookups in one round trip. Each operation still costs the rate limit
|
|
36
|
+
what the individual call would have, and one failure does not fail the rest:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
response = nameai.batch([
|
|
40
|
+
{"id": "a", "op": "search_domain", "params": {"q": "acme.ai"}},
|
|
41
|
+
{"id": "b", "op": "tld_registration_price", "params": {"tld": "ai", "op": "register"}},
|
|
42
|
+
{"id": "c", "op": "whois_lookup", "params": {"domain": "example.com"}},
|
|
43
|
+
])
|
|
44
|
+
for result in response["results"]:
|
|
45
|
+
print(result["id"], result["status"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Errors and auth
|
|
49
|
+
|
|
50
|
+
A non-2xx raises `NameAIError` carrying the API's typed `status`, `code` and
|
|
51
|
+
`retry_after`, so you can branch on the code instead of parsing a message.
|
|
52
|
+
|
|
53
|
+
Every method works unauthenticated. Authentication buys exactly one thing —
|
|
54
|
+
real marketplace prices instead of masked ones. Get a token per
|
|
55
|
+
[name.ai/auth.md](https://name.ai/auth.md) and pass
|
|
56
|
+
`NameAI(access_token=...)`.
|
|
57
|
+
|
|
58
|
+
- API docs: https://name.ai/developers/api
|
|
59
|
+
- OpenAPI spec: https://name.ai/openapi.json
|
|
60
|
+
- MCP server (same capabilities, for agents): https://name.ai/developers/mcp
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nameai"
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "Official Python SDK for the Name.ai public API: domain search/availability, WHOIS lookup, TLD registration pricing and registry requirements, marketplace listings with cursor pagination, and batched reads."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Name.ai", email = "support@name.ai" }]
|
|
9
|
+
keywords = ["nameai", "name.ai", "domains", "domain-search", "whois", "tld", "mcp"]
|
|
10
|
+
# No dependencies on purpose: the whole client is urllib from the standard
|
|
11
|
+
# library. An SDK for six read endpoints should not drag a transport stack into
|
|
12
|
+
# someone else's dependency tree.
|
|
13
|
+
dependencies = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Internet :: Name Service (DNS)",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# These are how a package is tied back to the product it belongs to — an agent
|
|
24
|
+
# (or a person) checking whether this is the official SDK reads them.
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://name.ai"
|
|
27
|
+
Documentation = "https://name.ai/developers/api"
|
|
28
|
+
"API Specification" = "https://name.ai/openapi.json"
|
|
29
|
+
Source = "https://github.com/namekart/nameai_mcp"
|
|
30
|
+
Issues = "https://github.com/namekart/nameai_mcp/issues"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/nameai"]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Official Name.ai SDK — a thin, dependency-free client for the public API.
|
|
2
|
+
|
|
3
|
+
Docs: https://name.ai/developers/api · Spec: https://name.ai/openapi.json
|
|
4
|
+
|
|
5
|
+
Every read works without an account. Authentication is optional
|
|
6
|
+
(https://name.ai/auth.md) and buys exactly one thing: real marketplace prices
|
|
7
|
+
instead of masked ones. Pass ``access_token`` to get them.
|
|
8
|
+
|
|
9
|
+
from nameai import NameAI
|
|
10
|
+
|
|
11
|
+
nameai = NameAI()
|
|
12
|
+
print(nameai.tld_price("ai", "register"))
|
|
13
|
+
for listing in nameai.list_all_listings(tld="ai"):
|
|
14
|
+
...
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.parse
|
|
22
|
+
import urllib.request
|
|
23
|
+
from typing import Any, Dict, Iterator, List, Optional, Sequence
|
|
24
|
+
|
|
25
|
+
__all__ = ["NameAI", "NameAIError", "__version__"]
|
|
26
|
+
|
|
27
|
+
__version__ = "1.1.0"
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = "https://name.ai"
|
|
30
|
+
_USER_AGENT = f"nameai-python/{__version__} (+https://name.ai/developers)"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NameAIError(RuntimeError):
|
|
34
|
+
"""A non-2xx response from the Name.ai API.
|
|
35
|
+
|
|
36
|
+
Carries the typed error the API returns (see ``components.schemas.Error``
|
|
37
|
+
in the spec) so a caller can branch on ``code`` rather than parse a string.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
message: str,
|
|
43
|
+
*,
|
|
44
|
+
status: Optional[int] = None,
|
|
45
|
+
code: Optional[str] = None,
|
|
46
|
+
retry_after: Optional[int] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__(message)
|
|
49
|
+
self.status = status
|
|
50
|
+
self.code = code
|
|
51
|
+
self.retry_after = retry_after
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NameAI:
|
|
55
|
+
"""Client for the Name.ai public REST API.
|
|
56
|
+
|
|
57
|
+
:param base_url: override for testing or a staging host.
|
|
58
|
+
:param access_token: OAuth 2.1 bearer token; optional, unlocks marketplace
|
|
59
|
+
prices. See https://name.ai/auth.md.
|
|
60
|
+
:param timeout: per-request timeout in seconds.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
66
|
+
access_token: Optional[str] = None,
|
|
67
|
+
timeout: float = 30.0,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.base_url = base_url.rstrip("/")
|
|
70
|
+
self.access_token = access_token
|
|
71
|
+
self.timeout = timeout
|
|
72
|
+
|
|
73
|
+
# ── transport ──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _headers(self, **extra: str) -> Dict[str, str]:
|
|
76
|
+
headers = {"accept": "application/json", "user-agent": _USER_AGENT}
|
|
77
|
+
if self.access_token:
|
|
78
|
+
headers["authorization"] = f"Bearer {self.access_token}"
|
|
79
|
+
headers.update(extra)
|
|
80
|
+
return headers
|
|
81
|
+
|
|
82
|
+
def _request(
|
|
83
|
+
self,
|
|
84
|
+
method: str,
|
|
85
|
+
path: str,
|
|
86
|
+
*,
|
|
87
|
+
params: Optional[Dict[str, Any]] = None,
|
|
88
|
+
body: Optional[Any] = None,
|
|
89
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
90
|
+
) -> str:
|
|
91
|
+
url = f"{self.base_url}{path}"
|
|
92
|
+
if params:
|
|
93
|
+
clean = {k: str(v) for k, v in params.items() if v is not None}
|
|
94
|
+
if clean:
|
|
95
|
+
url = f"{url}?{urllib.parse.urlencode(clean)}"
|
|
96
|
+
|
|
97
|
+
headers = self._headers(**(extra_headers or {}))
|
|
98
|
+
data = None
|
|
99
|
+
if body is not None:
|
|
100
|
+
data = json.dumps(body).encode("utf-8")
|
|
101
|
+
headers["content-type"] = "application/json"
|
|
102
|
+
|
|
103
|
+
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
104
|
+
try:
|
|
105
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
106
|
+
return response.read().decode("utf-8")
|
|
107
|
+
except urllib.error.HTTPError as exc: # non-2xx
|
|
108
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
109
|
+
message, code = f"Name.ai API returned HTTP {exc.code}", None
|
|
110
|
+
try:
|
|
111
|
+
detail = json.loads(raw).get("error") or {}
|
|
112
|
+
message = detail.get("message") or message
|
|
113
|
+
code = detail.get("code")
|
|
114
|
+
except (ValueError, AttributeError):
|
|
115
|
+
pass # error body was not the typed shape
|
|
116
|
+
retry_after = exc.headers.get("retry-after") if exc.headers else None
|
|
117
|
+
raise NameAIError(
|
|
118
|
+
message,
|
|
119
|
+
status=exc.code,
|
|
120
|
+
code=code,
|
|
121
|
+
retry_after=int(retry_after) if retry_after and retry_after.isdigit() else None,
|
|
122
|
+
) from None
|
|
123
|
+
|
|
124
|
+
def _json(self, *args: Any, **kwargs: Any) -> Any:
|
|
125
|
+
return json.loads(self._request(*args, **kwargs))
|
|
126
|
+
|
|
127
|
+
# ── endpoints ──────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
def search_domain(self, domain: str) -> Dict[str, Any]:
|
|
130
|
+
"""Check a domain's availability plus its alternate-TLD siblings.
|
|
131
|
+
|
|
132
|
+
The endpoint streams NDJSON so a browser can fill results in as they
|
|
133
|
+
land; there is nothing to stream into here, so the rows are collected.
|
|
134
|
+
|
|
135
|
+
:returns: ``{"query": str, "results": [row, ...]}``
|
|
136
|
+
"""
|
|
137
|
+
text = self._request("POST", "/api/domain/search", body={"q": domain})
|
|
138
|
+
results: List[Dict[str, Any]] = []
|
|
139
|
+
for line in text.splitlines():
|
|
140
|
+
if not line.strip():
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
event = json.loads(line)
|
|
144
|
+
except ValueError:
|
|
145
|
+
continue
|
|
146
|
+
if event.get("kind") == "row":
|
|
147
|
+
event.pop("kind", None)
|
|
148
|
+
results.append(event)
|
|
149
|
+
return {"query": domain, "results": results}
|
|
150
|
+
|
|
151
|
+
def whois_lookup(self, domain: str, *, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
|
152
|
+
"""WHOIS/RDAP details: registrar, registrant, dates, nameservers.
|
|
153
|
+
|
|
154
|
+
:param idempotency_key: makes a retry replay-safe.
|
|
155
|
+
"""
|
|
156
|
+
extra = {"idempotency-key": idempotency_key} if idempotency_key else None
|
|
157
|
+
return self._json("POST", "/api/tools/whois", body={"domain": domain}, extra_headers=extra)
|
|
158
|
+
|
|
159
|
+
def tld_price(self, tld: str, operation: str = "register") -> Dict[str, Any]:
|
|
160
|
+
"""Price for ``register``, ``renew``, ``transfer`` or ``restore`` on a TLD."""
|
|
161
|
+
return self._json(
|
|
162
|
+
"GET", "/api/pricing/tld", params={"tld": tld.lstrip("."), "op": operation}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def tld_requirements(self, tld: str) -> Dict[str, Any]:
|
|
166
|
+
"""Registry requirements: term range, organization and nameserver rules."""
|
|
167
|
+
return self._json("GET", f"/api/tlds/{urllib.parse.quote(tld.lstrip('.'))}/metadata")
|
|
168
|
+
|
|
169
|
+
def market_listings(
|
|
170
|
+
self,
|
|
171
|
+
*,
|
|
172
|
+
limit: Optional[int] = None,
|
|
173
|
+
offset: Optional[int] = None,
|
|
174
|
+
cursor: Optional[str] = None,
|
|
175
|
+
q: Optional[str] = None,
|
|
176
|
+
tld: Optional[str] = None,
|
|
177
|
+
max: Optional[float] = None, # noqa: A002 — matches the query parameter
|
|
178
|
+
sort: Optional[str] = None,
|
|
179
|
+
) -> Dict[str, Any]:
|
|
180
|
+
"""One page of marketplace listings.
|
|
181
|
+
|
|
182
|
+
Prefer ``cursor`` (from a previous ``page["next_cursor"]``) over
|
|
183
|
+
incrementing ``offset``: a cursor names the row you stopped at, so a
|
|
184
|
+
listing sold or added mid-walk cannot shift the window under you.
|
|
185
|
+
:meth:`list_all_listings` does that for you.
|
|
186
|
+
"""
|
|
187
|
+
return self._json(
|
|
188
|
+
"GET",
|
|
189
|
+
"/api/market/listings",
|
|
190
|
+
params={
|
|
191
|
+
"limit": limit,
|
|
192
|
+
"offset": offset,
|
|
193
|
+
"cursor": cursor,
|
|
194
|
+
"q": q,
|
|
195
|
+
"tld": tld,
|
|
196
|
+
"max": max,
|
|
197
|
+
"sort": sort,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def list_all_listings(self, **params: Any) -> Iterator[Dict[str, Any]]:
|
|
202
|
+
"""Every matching listing, walked by cursor, one page held at a time."""
|
|
203
|
+
params.pop("offset", None)
|
|
204
|
+
params.pop("cursor", None)
|
|
205
|
+
cursor: Optional[str] = None
|
|
206
|
+
while True:
|
|
207
|
+
page = self.market_listings(cursor=cursor, **params)
|
|
208
|
+
for item in page.get("items") or []:
|
|
209
|
+
yield item
|
|
210
|
+
cursor = (page.get("page") or {}).get("next_cursor")
|
|
211
|
+
if not cursor:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
def batch(self, operations: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
|
|
215
|
+
"""Several public reads in one request — prefer this over a loop.
|
|
216
|
+
|
|
217
|
+
One round trip instead of N, and each operation costs the rate limit
|
|
218
|
+
exactly what the individual call would have.
|
|
219
|
+
|
|
220
|
+
:param operations: up to 20 of ``{"id": ..., "op": ..., "params": {...}}``
|
|
221
|
+
where ``op`` is ``search_domain``, ``whois_lookup``,
|
|
222
|
+
``tld_registration_price`` or ``tld_requirements``.
|
|
223
|
+
:returns: ``{"results": [...], "count": int, "failed": int}`` — one
|
|
224
|
+
result per operation, in order, each with its own ``status``. One
|
|
225
|
+
failure does not fail the batch.
|
|
226
|
+
"""
|
|
227
|
+
return self._json("POST", "/api/batch", body={"operations": list(operations)})
|
|
File without changes
|