namedaemon 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,18 @@
1
+ name: test
2
+ on:
3
+ push:
4
+ branches: [dev, main]
5
+ pull_request:
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python }}
17
+ - run: pip install .
18
+ - run: python -m unittest discover -s tests -v
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ build/
4
+ dist/
5
+ .venv/
6
+ venv/
7
+ .env
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - First release: `NameDaemon` client and `namedaemon` CLI for the public NameDaemon REST layer.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Krasen Slavov
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,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: namedaemon
3
+ Version: 0.1.0
4
+ Summary: Client and CLI for NameDaemon: .com domain availability, brandability scores, name ideas, TLD costs and domain health.
5
+ Project-URL: Homepage, https://namedaemon.com
6
+ Project-URL: Repository, https://github.com/krasenslavov/namedaemon-pypi
7
+ Project-URL: Issues, https://github.com/krasenslavov/namedaemon-pypi/issues
8
+ Project-URL: Changelog, https://github.com/krasenslavov/namedaemon-pypi/blob/main/CHANGELOG.md
9
+ Author: Krasen Slavov
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: brand-name,dns,domain,domain-availability,domain-name,name-generator,namedaemon,rdap,tld,whois
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Internet :: Name Service (DNS)
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # namedaemon
24
+
25
+ Python client and command-line tool for [NameDaemon](https://namedaemon.com), a free set of .com domain tools: availability,
26
+ brandability scores, name ideas, extension costs, domain health and more. No API key, no dependencies.
27
+
28
+ ```
29
+ pip install namedaemon
30
+ ```
31
+
32
+ Needs Python 3.9 or newer.
33
+
34
+ ## Command line
35
+
36
+ ```
37
+ namedaemon rate example
38
+ namedaemon ideas nova 10
39
+ namedaemon check nova novax mybrand
40
+ namedaemon tld nova com io ai
41
+ namedaemon tco io
42
+ namedaemon health example
43
+ namedaemon --json appraise example
44
+ ```
45
+
46
+ `python -m namedaemon ...` works too. `--json` (before the command) prints the raw response.
47
+
48
+ ## Library
49
+
50
+ ```python
51
+ from namedaemon import NameDaemon
52
+
53
+ nd = NameDaemon()
54
+
55
+ r = nd.rate("example")
56
+ print(r["score"], r["verdict"]) # 95.5 Excellent
57
+
58
+ ideas = nd.ideas("nova", limit=10)["ideas"]
59
+ results = nd.check(["nova", "novax"])["results"]
60
+ cost = nd.tco("io", years=5)
61
+ health = nd.domain_health("example")
62
+ ```
63
+
64
+ | Method | What it returns |
65
+ |---|---|
66
+ | `rate(name)` | brandability score, verdict, breakdown, tips |
67
+ | `ideas(keyword, limit=None)` | two-word .com ideas built around a keyword |
68
+ | `check(names, verify=0)` | zone status of up to 50 names, optional live checks |
69
+ | `verify(names)` | live availability of up to 10 names |
70
+ | `tld_check(label, tlds)` | one label across up to 12 extensions |
71
+ | `tco(tld, **options)` | registration, renewal and total cost by registrar |
72
+ | `hacks(word=...)` | domain hacks such as `hack.er` |
73
+ | `names(seeds, vibe=..., count=...)` | brand-name generator |
74
+ | `domain_health(name)` | registration, DNS, email and blocklist checks |
75
+ | `appraise(name, deep=False)` | strength report (no price) |
76
+ | `handles(name)` | username availability on social platforms |
77
+ | `web3(name)`, `screen(name)`, `hosting()`, `trend(keyword)` | ENS/Solana names, name screening, hosting share, keyword trend |
78
+ | `get(path, **params)`, `post(path, body)` | any other public route under `/wp-json/nd/v1/` |
79
+
80
+ Every method returns the parsed JSON of the matching REST route. The package is typed (`py.typed`).
81
+
82
+ ### Errors and rate limits
83
+
84
+ The public API is free and shared, so it limits each visitor to roughly 20 requests a minute. Heavier routes count for more.
85
+ When the limit is hit the client raises `RateLimitError` (with `retry_after` in seconds, 60 by default); pass `retries=1` to wait
86
+ and retry automatically. Other failures raise `NameDaemonError` with `status`, `code` and the server's `message`.
87
+
88
+ ```python
89
+ from namedaemon import NameDaemon, RateLimitError
90
+
91
+ nd = NameDaemon(timeout=20, retries=1)
92
+ try:
93
+ nd.rate("example")
94
+ except RateLimitError as e:
95
+ print(e.retry_after)
96
+ ```
97
+
98
+ Please keep usage reasonable. For bulk work, see the paid plan at https://namedaemon.com.
99
+
100
+ ## Links
101
+
102
+ - Website and free tools: https://namedaemon.com
103
+ - Issues: https://github.com/krasenslavov/namedaemon-pypi/issues
104
+ - JavaScript version: `npm install namedaemon`
105
+
106
+ MIT license.
@@ -0,0 +1,84 @@
1
+ # namedaemon
2
+
3
+ Python client and command-line tool for [NameDaemon](https://namedaemon.com), a free set of .com domain tools: availability,
4
+ brandability scores, name ideas, extension costs, domain health and more. No API key, no dependencies.
5
+
6
+ ```
7
+ pip install namedaemon
8
+ ```
9
+
10
+ Needs Python 3.9 or newer.
11
+
12
+ ## Command line
13
+
14
+ ```
15
+ namedaemon rate example
16
+ namedaemon ideas nova 10
17
+ namedaemon check nova novax mybrand
18
+ namedaemon tld nova com io ai
19
+ namedaemon tco io
20
+ namedaemon health example
21
+ namedaemon --json appraise example
22
+ ```
23
+
24
+ `python -m namedaemon ...` works too. `--json` (before the command) prints the raw response.
25
+
26
+ ## Library
27
+
28
+ ```python
29
+ from namedaemon import NameDaemon
30
+
31
+ nd = NameDaemon()
32
+
33
+ r = nd.rate("example")
34
+ print(r["score"], r["verdict"]) # 95.5 Excellent
35
+
36
+ ideas = nd.ideas("nova", limit=10)["ideas"]
37
+ results = nd.check(["nova", "novax"])["results"]
38
+ cost = nd.tco("io", years=5)
39
+ health = nd.domain_health("example")
40
+ ```
41
+
42
+ | Method | What it returns |
43
+ |---|---|
44
+ | `rate(name)` | brandability score, verdict, breakdown, tips |
45
+ | `ideas(keyword, limit=None)` | two-word .com ideas built around a keyword |
46
+ | `check(names, verify=0)` | zone status of up to 50 names, optional live checks |
47
+ | `verify(names)` | live availability of up to 10 names |
48
+ | `tld_check(label, tlds)` | one label across up to 12 extensions |
49
+ | `tco(tld, **options)` | registration, renewal and total cost by registrar |
50
+ | `hacks(word=...)` | domain hacks such as `hack.er` |
51
+ | `names(seeds, vibe=..., count=...)` | brand-name generator |
52
+ | `domain_health(name)` | registration, DNS, email and blocklist checks |
53
+ | `appraise(name, deep=False)` | strength report (no price) |
54
+ | `handles(name)` | username availability on social platforms |
55
+ | `web3(name)`, `screen(name)`, `hosting()`, `trend(keyword)` | ENS/Solana names, name screening, hosting share, keyword trend |
56
+ | `get(path, **params)`, `post(path, body)` | any other public route under `/wp-json/nd/v1/` |
57
+
58
+ Every method returns the parsed JSON of the matching REST route. The package is typed (`py.typed`).
59
+
60
+ ### Errors and rate limits
61
+
62
+ The public API is free and shared, so it limits each visitor to roughly 20 requests a minute. Heavier routes count for more.
63
+ When the limit is hit the client raises `RateLimitError` (with `retry_after` in seconds, 60 by default); pass `retries=1` to wait
64
+ and retry automatically. Other failures raise `NameDaemonError` with `status`, `code` and the server's `message`.
65
+
66
+ ```python
67
+ from namedaemon import NameDaemon, RateLimitError
68
+
69
+ nd = NameDaemon(timeout=20, retries=1)
70
+ try:
71
+ nd.rate("example")
72
+ except RateLimitError as e:
73
+ print(e.retry_after)
74
+ ```
75
+
76
+ Please keep usage reasonable. For bulk work, see the paid plan at https://namedaemon.com.
77
+
78
+ ## Links
79
+
80
+ - Website and free tools: https://namedaemon.com
81
+ - Issues: https://github.com/krasenslavov/namedaemon-pypi/issues
82
+ - JavaScript version: `npm install namedaemon`
83
+
84
+ MIT license.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "namedaemon"
7
+ dynamic = ["version"]
8
+ description = "Client and CLI for NameDaemon: .com domain availability, brandability scores, name ideas, TLD costs and domain health."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Krasen Slavov" }]
14
+ keywords = ["domain", "domain-name", "domain-availability", "whois", "rdap", "dns", "tld", "brand-name", "name-generator", "namedaemon"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Internet :: Name Service (DNS)",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = []
25
+
26
+ [project.scripts]
27
+ namedaemon = "namedaemon.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://namedaemon.com"
31
+ Repository = "https://github.com/krasenslavov/namedaemon-pypi"
32
+ Issues = "https://github.com/krasenslavov/namedaemon-pypi/issues"
33
+ Changelog = "https://github.com/krasenslavov/namedaemon-pypi/blob/main/CHANGELOG.md"
34
+
35
+ [tool.hatch.version]
36
+ path = "src/namedaemon/__init__.py"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/namedaemon"]
@@ -0,0 +1,12 @@
1
+ """Client and CLI for NameDaemon (https://namedaemon.com)."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .client import ( # noqa: E402
6
+ DEFAULT_BASE_URL,
7
+ NameDaemon,
8
+ NameDaemonError,
9
+ RateLimitError,
10
+ )
11
+
12
+ __all__ = ["NameDaemon", "NameDaemonError", "RateLimitError", "DEFAULT_BASE_URL", "__version__"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,79 @@
1
+ """Command line: ``namedaemon rate example`` or ``python -m namedaemon rate example``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any, List, Optional
9
+
10
+ from . import __version__
11
+ from .client import NameDaemon, NameDaemonError, RateLimitError
12
+
13
+
14
+ def _pretty(cmd: str, r: Any) -> str:
15
+ if cmd == "rate":
16
+ line = f"{r['name']}: {r['score'] if r.get('score') is not None else 'no score'} {r.get('verdict') or ''}".strip()
17
+ tips = r.get("tips") or []
18
+ return line + ("\n- " + "\n- ".join(tips) if tips else "")
19
+ if cmd == "ideas":
20
+ return "\n".join(f"{i['domain']}.com {i['score']}" for i in r["ideas"])
21
+ if cmd in ("check", "verify", "tld"):
22
+ return "\n".join(
23
+ f"{x['domain']} {x['status']}" + (f" (drops {x['est_drop']})" if x.get("est_drop") else "")
24
+ for x in r["results"]
25
+ )
26
+ return json.dumps(r, indent=2)
27
+
28
+
29
+ def build_parser() -> argparse.ArgumentParser:
30
+ p = argparse.ArgumentParser(prog="namedaemon", description="NameDaemon .com domain tools (https://namedaemon.com)")
31
+ p.add_argument("--version", action="version", version=f"namedaemon {__version__}")
32
+ p.add_argument("--json", action="store_true", help="print the raw JSON response")
33
+ sub = p.add_subparsers(dest="cmd", required=True)
34
+ sub.add_parser("rate", help="brandability score for one name").add_argument("name")
35
+ ideas = sub.add_parser("ideas", help="two-word .com ideas around a keyword")
36
+ ideas.add_argument("keyword")
37
+ ideas.add_argument("limit", nargs="?", type=int)
38
+ sub.add_parser("check", help="status of up to 50 names").add_argument("names", nargs="+")
39
+ sub.add_parser("verify", help="live availability of up to 10 names").add_argument("names", nargs="+")
40
+ tld = sub.add_parser("tld", help="one label across several extensions")
41
+ tld.add_argument("label")
42
+ tld.add_argument("tlds", nargs="+")
43
+ sub.add_parser("tco", help="cost of ownership for an extension").add_argument("tld")
44
+ sub.add_parser("health", help="DNS, email and registration health").add_argument("name")
45
+ sub.add_parser("appraise", help="strength report (no price)").add_argument("name")
46
+ sub.add_parser("handles", help="username availability on social platforms").add_argument("name")
47
+ return p
48
+
49
+
50
+ def main(argv: Optional[List[str]] = None) -> int:
51
+ args = build_parser().parse_args(argv)
52
+ nd = NameDaemon()
53
+ try:
54
+ if args.cmd == "rate":
55
+ res = nd.rate(args.name)
56
+ elif args.cmd == "ideas":
57
+ res = nd.ideas(args.keyword, args.limit)
58
+ elif args.cmd == "check":
59
+ res = nd.check(args.names)
60
+ elif args.cmd == "verify":
61
+ res = nd.verify(args.names)
62
+ elif args.cmd == "tld":
63
+ res = nd.tld_check(args.label, args.tlds)
64
+ elif args.cmd == "tco":
65
+ res = nd.tco(args.tld)
66
+ elif args.cmd == "health":
67
+ res = nd.domain_health(args.name)
68
+ elif args.cmd == "appraise":
69
+ res = nd.appraise(args.name)
70
+ else:
71
+ res = nd.handles(args.name)
72
+ except RateLimitError as e:
73
+ print(f"Rate limit reached. Try again in about {e.retry_after} seconds.", file=sys.stderr)
74
+ return 1
75
+ except NameDaemonError as e:
76
+ print(f"Error{' ' + str(e.status) if e.status else ''}: {e.message}", file=sys.stderr)
77
+ return 1
78
+ print(json.dumps(res, indent=2) if args.json else _pretty(args.cmd, res))
79
+ return 0
@@ -0,0 +1,194 @@
1
+ """NameDaemon client for the public REST layer (https://namedaemon.com/wp-json/nd/v1).
2
+
3
+ Standard library only (urllib), so it installs with no dependencies.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import time
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ from typing import Any, Dict, Iterable, Optional, Sequence, Union
14
+
15
+ from . import __version__
16
+
17
+ DEFAULT_BASE_URL = "https://namedaemon.com/wp-json/nd/v1"
18
+
19
+ Json = Dict[str, Any]
20
+
21
+
22
+ class NameDaemonError(Exception):
23
+ """Any failed request. `status` is 0 for network errors."""
24
+
25
+ def __init__(self, message: str, status: int = 0, code: str = "", body: Any = None):
26
+ super().__init__(message)
27
+ self.message = message
28
+ self.status = status
29
+ self.code = code
30
+ self.body = body
31
+
32
+
33
+ class RateLimitError(NameDaemonError):
34
+ """HTTP 429: the shared limit was hit. `retry_after` is in seconds."""
35
+
36
+ def __init__(self, message: str, retry_after: int = 60, **kw: Any):
37
+ super().__init__(message, status=429, **kw)
38
+ self.retry_after = retry_after
39
+
40
+
41
+ def _clean(params: Optional[Dict[str, Any]]) -> Dict[str, str]:
42
+ out: Dict[str, str] = {}
43
+ for k, v in (params or {}).items():
44
+ if v is None or v == "":
45
+ continue
46
+ if isinstance(v, bool):
47
+ v = "true" if v else "false"
48
+ elif isinstance(v, (list, tuple)):
49
+ v = ",".join(str(x) for x in v)
50
+ out[k] = str(v)
51
+ return out
52
+
53
+
54
+ class NameDaemon:
55
+ """Client for the public NameDaemon API.
56
+
57
+ >>> nd = NameDaemon()
58
+ >>> nd.rate("example")["score"]
59
+ 95.5
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ base_url: str = DEFAULT_BASE_URL,
65
+ timeout: float = 20.0,
66
+ retries: int = 0,
67
+ user_agent: Optional[str] = None,
68
+ ) -> None:
69
+ self.base_url = base_url.rstrip("/")
70
+ self.timeout = timeout
71
+ self.retries = retries
72
+ self.user_agent = user_agent or f"namedaemon-py/{__version__}"
73
+
74
+ # -- transport ---------------------------------------------------------
75
+ def _request(self, method: str, path: str, params: Optional[Dict[str, Any]] = None, body: Any = None) -> Json:
76
+ url = f"{self.base_url}/{path.lstrip('/')}"
77
+ query = urllib.parse.urlencode(_clean(params))
78
+ if query:
79
+ url += "?" + query
80
+ headers = {"Accept": "application/json", "User-Agent": self.user_agent}
81
+ data = None
82
+ if body is not None:
83
+ data = json.dumps(body).encode("utf-8")
84
+ headers["Content-Type"] = "application/json"
85
+ attempt = 0
86
+ while True:
87
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
88
+ try:
89
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
90
+ raw = resp.read().decode("utf-8", "replace")
91
+ status = resp.status
92
+ hdrs = resp.headers
93
+ except urllib.error.HTTPError as e:
94
+ raw = e.read().decode("utf-8", "replace")
95
+ status = e.code
96
+ hdrs = e.headers
97
+ except (urllib.error.URLError, TimeoutError, OSError) as e:
98
+ raise NameDaemonError(f"Network error: {e}") from e
99
+
100
+ try:
101
+ parsed = json.loads(raw) if raw else None
102
+ except ValueError:
103
+ parsed = None
104
+
105
+ if 200 <= status < 300:
106
+ if parsed is None:
107
+ raise NameDaemonError("Response was not JSON", status=status)
108
+ return parsed
109
+
110
+ message = (parsed or {}).get("message") if isinstance(parsed, dict) else None
111
+ code = (parsed or {}).get("code", "") if isinstance(parsed, dict) else ""
112
+ message = message or f"HTTP {status}"
113
+ if status == 429:
114
+ try:
115
+ retry_after = int(hdrs.get("Retry-After", "")) if hdrs else 60
116
+ except (TypeError, ValueError):
117
+ retry_after = 60
118
+ if retry_after <= 0:
119
+ retry_after = 60
120
+ if attempt < self.retries:
121
+ attempt += 1
122
+ time.sleep(min(retry_after, 60))
123
+ continue
124
+ raise RateLimitError(message, retry_after=retry_after, code=code, body=parsed)
125
+ raise NameDaemonError(message, status=status, code=code, body=parsed)
126
+
127
+ def get(self, path: str, **params: Any) -> Json:
128
+ """Any public GET route, e.g. ``nd.get("trend", keyword="ai")``."""
129
+ return self._request("GET", path, params)
130
+
131
+ def post(self, path: str, body: Any) -> Json:
132
+ """Any public POST route with a JSON body."""
133
+ return self._request("POST", path, body=body)
134
+
135
+ # -- tools -------------------------------------------------------------
136
+ def rate(self, name: str) -> Json:
137
+ """Brandability score and breakdown for one name (4-24 letters)."""
138
+ return self.get("rate", name=name)
139
+
140
+ def ideas(self, q: str, limit: Optional[int] = None) -> Json:
141
+ """Two-word .com ideas built around a keyword (3-14 letters)."""
142
+ return self.get("ideas", q=q, limit=limit)
143
+
144
+ def verify(self, domains: Sequence[str]) -> Json:
145
+ """Live availability of up to 10 names."""
146
+ return self.post("verify", {"domains": list(domains)})
147
+
148
+ def check(self, domains: Sequence[str], verify: int = 0) -> Json:
149
+ """Zone status of up to 50 names, with optional live checks (0-10)."""
150
+ return self.post("check", {"domains": list(domains), "verify": verify})
151
+
152
+ def tld_check(self, label: str, tlds: Union[str, Iterable[str]]) -> Json:
153
+ """One label across up to 12 extensions."""
154
+ return self.get("tld-check", label=label, tlds=tlds if isinstance(tlds, str) else list(tlds))
155
+
156
+ def tco(self, tld: Optional[str] = None, **options: Any) -> Json:
157
+ """Cost of ownership for an extension, or a ranking with q/scope/sort/limit."""
158
+ return self.get("tco", tld=tld, **options)
159
+
160
+ def hacks(self, **options: Any) -> Json:
161
+ """Domain hacks (e.g. hack.er). Pass word=... or keyword=... and mode=..."""
162
+ return self.get("hacks", **options)
163
+
164
+ def names(self, seeds: Union[str, Iterable[str]], **options: Any) -> Json:
165
+ """Brand-name generator: seeds plus vibe, min_len, max_len, count, variation, hide_taken."""
166
+ return self.get("names", seeds=seeds if isinstance(seeds, str) else list(seeds), **options)
167
+
168
+ def domain_health(self, name: str) -> Json:
169
+ """Registration, DNS, email and blocklist checks for a .com label."""
170
+ return self.get("domain-health", name=name)
171
+
172
+ def appraise(self, name: str, deep: bool = False) -> Json:
173
+ """Strength report for a name (no price)."""
174
+ return self.get("appraise", name=name, deep=True if deep else None)
175
+
176
+ def handles(self, name: str) -> Json:
177
+ """Username availability on social platforms."""
178
+ return self.get("handles", name=name)
179
+
180
+ def web3(self, name: str) -> Json:
181
+ """ENS and Solana name availability."""
182
+ return self.get("web3", name=name)
183
+
184
+ def screen(self, name: str) -> Json:
185
+ """Name screening: unwanted meanings in other languages."""
186
+ return self.get("screen", name=name)
187
+
188
+ def hosting(self, mode: Optional[str] = None, q: Optional[str] = None) -> Json:
189
+ """Hosting provider ranking (mode: top, provider or domain)."""
190
+ return self.get("hosting", mode=mode, q=q)
191
+
192
+ def trend(self, keyword: str) -> Json:
193
+ """Daily size history for a keyword in the .com zone."""
194
+ return self.get("trend", keyword=keyword)
File without changes
@@ -0,0 +1,84 @@
1
+ import io
2
+ import json
3
+ import os
4
+ import unittest
5
+ import urllib.error
6
+ from unittest import mock
7
+
8
+ from namedaemon import NameDaemon, NameDaemonError, RateLimitError
9
+
10
+
11
+ class FakeResp:
12
+ def __init__(self, body, status=200, headers=None):
13
+ self._b = body if isinstance(body, str) else json.dumps(body)
14
+ self.status = status
15
+ self.headers = headers or {}
16
+
17
+ def read(self):
18
+ return self._b.encode()
19
+
20
+ def __enter__(self):
21
+ return self
22
+
23
+ def __exit__(self, *a):
24
+ return False
25
+
26
+
27
+ def http_error(status, body, headers=None):
28
+ return urllib.error.HTTPError("u", status, "x", headers or {}, io.BytesIO(json.dumps(body).encode()))
29
+
30
+
31
+ class ClientTests(unittest.TestCase):
32
+ @mock.patch("urllib.request.urlopen")
33
+ def test_rate_url_and_agent(self, uo):
34
+ uo.return_value = FakeResp({"name": "example", "score": 95.5})
35
+ r = NameDaemon().rate("example")
36
+ self.assertEqual(r["score"], 95.5)
37
+ req = uo.call_args[0][0]
38
+ self.assertEqual(req.full_url, "https://namedaemon.com/wp-json/nd/v1/rate?name=example")
39
+ self.assertTrue(req.get_header("User-agent").startswith("namedaemon-py/"))
40
+
41
+ @mock.patch("urllib.request.urlopen")
42
+ def test_lists_join_and_none_dropped(self, uo):
43
+ uo.return_value = FakeResp({"results": []})
44
+ NameDaemon().tld_check("nova", ["com", "io"])
45
+ self.assertIn("label=nova&tlds=com%2Cio", uo.call_args[0][0].full_url)
46
+
47
+ @mock.patch("urllib.request.urlopen")
48
+ def test_check_posts_json(self, uo):
49
+ uo.return_value = FakeResp({"results": []})
50
+ NameDaemon().check(["a", "b"], verify=2)
51
+ req = uo.call_args[0][0]
52
+ self.assertEqual(req.get_method(), "POST")
53
+ self.assertEqual(json.loads(req.data), {"domains": ["a", "b"], "verify": 2})
54
+
55
+ @mock.patch("urllib.request.urlopen")
56
+ def test_429(self, uo):
57
+ uo.side_effect = http_error(429, {"code": "ndt_rate_limited", "message": "Too many requests."}, {"Retry-After": "30"})
58
+ with self.assertRaises(RateLimitError) as cm:
59
+ NameDaemon().rate("example")
60
+ self.assertEqual(cm.exception.retry_after, 30)
61
+
62
+ @mock.patch("urllib.request.urlopen")
63
+ def test_400(self, uo):
64
+ uo.side_effect = http_error(400, {"code": "rest_invalid_param", "message": "Invalid parameter(s): name"})
65
+ with self.assertRaises(NameDaemonError) as cm:
66
+ NameDaemon().rate("x1")
67
+ self.assertEqual(cm.exception.status, 400)
68
+ self.assertIn("name", cm.exception.message)
69
+
70
+ @mock.patch("urllib.request.urlopen")
71
+ def test_non_json(self, uo):
72
+ uo.return_value = FakeResp("<html>challenge</html>")
73
+ with self.assertRaises(NameDaemonError):
74
+ NameDaemon().rate("example")
75
+
76
+ @unittest.skipUnless(os.environ.get("NAMEDAEMON_LIVE"), "set NAMEDAEMON_LIVE=1")
77
+ def test_live(self):
78
+ r = NameDaemon().rate("example")
79
+ self.assertEqual(r["name"], "example")
80
+ self.assertIsInstance(r["score"], (int, float))
81
+
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()