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.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
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