domainhack 0.2.0__py3-none-any.whl
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.
- domainhack/__init__.py +5 -0
- domainhack/__main__.py +5 -0
- domainhack/adapters/__init__.py +0 -0
- domainhack/adapters/_circuit.py +105 -0
- domainhack/adapters/_http.py +22 -0
- domainhack/adapters/_registration.py +128 -0
- domainhack/adapters/_stderr.py +16 -0
- domainhack/adapters/_text_sink.py +33 -0
- domainhack/adapters/_throttle.py +150 -0
- domainhack/adapters/brand_data.py +49 -0
- domainhack/adapters/brand_report_render.py +222 -0
- domainhack/adapters/cached_registrar.py +340 -0
- domainhack/adapters/composite_writer.py +27 -0
- domainhack/adapters/console_writer.py +77 -0
- domainhack/adapters/csv_writer.py +79 -0
- domainhack/adapters/dns_confirming_writer.py +72 -0
- domainhack/adapters/dns_resolver.py +120 -0
- domainhack/adapters/file_word_source.py +51 -0
- domainhack/adapters/iana_tlds.py +110 -0
- domainhack/adapters/json_writer.py +51 -0
- domainhack/adapters/pacing.py +46 -0
- domainhack/adapters/rdap_bootstrap.py +223 -0
- domainhack/adapters/rdap_registrar.py +328 -0
- domainhack/adapters/registrar_catalog.py +120 -0
- domainhack/adapters/registrar_router.py +72 -0
- domainhack/adapters/registry_sources.py +192 -0
- domainhack/adapters/tqdm_progress.py +101 -0
- domainhack/adapters/whois_registrar.py +386 -0
- domainhack/cli/__init__.py +0 -0
- domainhack/cli/app.py +800 -0
- domainhack/cli/name_cmd.py +222 -0
- domainhack/cli/tlds_cmd.py +96 -0
- domainhack/data/hack_words.json +20 -0
- domainhack/data/iana_tlds.txt +1438 -0
- domainhack/data/rdap_dns.json +1 -0
- domainhack/data/registry_sources.json +2367 -0
- domainhack/data/second_level.json +28 -0
- domainhack/data/tld_presets.json +7 -0
- domainhack/domain/__init__.py +0 -0
- domainhack/domain/entities.py +287 -0
- domainhack/domain/label_rules.py +96 -0
- domainhack/domain/parking.py +81 -0
- domainhack/ports/__init__.py +0 -0
- domainhack/ports/dns_lookup.py +11 -0
- domainhack/ports/known_tlds.py +21 -0
- domainhack/ports/progress.py +32 -0
- domainhack/ports/registrar.py +27 -0
- domainhack/ports/result_cache.py +19 -0
- domainhack/ports/result_writer.py +13 -0
- domainhack/ports/word_source.py +9 -0
- domainhack/py.typed +0 -0
- domainhack/usecases/__init__.py +0 -0
- domainhack/usecases/brand_candidates.py +195 -0
- domainhack/usecases/brand_report.py +271 -0
- domainhack/usecases/check_domains.py +287 -0
- domainhack/usecases/confirm_dns.py +64 -0
- domainhack/usecases/estimate_run.py +80 -0
- domainhack/usecases/filter_words.py +37 -0
- domainhack/usecases/generate_range.py +107 -0
- domainhack/usecases/lanes.py +193 -0
- domainhack/usecases/rank_candidates.py +106 -0
- domainhack-0.2.0.dist-info/METADATA +501 -0
- domainhack-0.2.0.dist-info/RECORD +67 -0
- domainhack-0.2.0.dist-info/WHEEL +5 -0
- domainhack-0.2.0.dist-info/entry_points.txt +2 -0
- domainhack-0.2.0.dist-info/licenses/LICENSE +21 -0
- domainhack-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""``domainhack name NAME``: find a domain for a brand name, in one command.
|
|
2
|
+
|
|
3
|
+
Checks the exact name under each requested TLD, domain hacks (the name split
|
|
4
|
+
at a TLD it ends with, or the name plus a word that ends in one) and, with
|
|
5
|
+
``--variants``, brand variants; then prints a report with a recommendation.
|
|
6
|
+
Checking reuses ``CheckDomainsUseCase`` and the ``check`` wiring in
|
|
7
|
+
``cli.app`` (registrar catalog and router, parallel lanes, cache, circuit
|
|
8
|
+
breaker, progress bar).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from domainhack.adapters.brand_data import DEFAULT_PRESET, load_hack_words, load_tld_presets
|
|
20
|
+
from domainhack.adapters.brand_report_render import RENDERERS
|
|
21
|
+
from domainhack.adapters.cached_registrar import CachedRegistrarClient
|
|
22
|
+
from domainhack.adapters.dns_resolver import DnsPythonLookup
|
|
23
|
+
from domainhack.adapters.iana_tlds import IanaTldList
|
|
24
|
+
from domainhack.adapters.registrar_catalog import supported_tlds
|
|
25
|
+
from domainhack.adapters.registry_sources import describe_unsupported, unsupported_message
|
|
26
|
+
from domainhack.cli import app
|
|
27
|
+
from domainhack.domain.entities import DomainCheckResult
|
|
28
|
+
from domainhack.ports.dns_lookup import DnsLookup
|
|
29
|
+
from domainhack.ports.known_tlds import KnownTlds
|
|
30
|
+
from domainhack.ports.result_writer import ResultWriter
|
|
31
|
+
from domainhack.usecases.brand_candidates import BrandCandidates, resolve_tld_spec
|
|
32
|
+
from domainhack.usecases.brand_report import build_brand_report
|
|
33
|
+
from domainhack.usecases.check_domains import CheckDomainsUseCase
|
|
34
|
+
from domainhack.usecases.confirm_dns import ConfirmWithDns
|
|
35
|
+
|
|
36
|
+
REPORT_FORMATS = tuple(RENDERERS)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class NameServices:
|
|
41
|
+
"""What ``name`` needs besides the registrar catalog (tests pass fakes).
|
|
42
|
+
|
|
43
|
+
``dns`` is optional: without it, results are not confirmed against DNS.
|
|
44
|
+
``supported_tlds`` lists the catalog's TLDs for the ``all-supported`` preset.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
known_tlds: KnownTlds
|
|
48
|
+
dns: DnsLookup | None = None
|
|
49
|
+
supported_tlds: Callable[[], Iterable[str]] = supported_tlds
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _default_services() -> NameServices:
|
|
53
|
+
"""Production wiring for ``name``: the known-TLD list and the DNS lookup."""
|
|
54
|
+
return NameServices(known_tlds=IanaTldList(), dns=DnsPythonLookup())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _confirm_with_dns(
|
|
58
|
+
lookup: DnsLookup, results: Sequence[DomainCheckResult]
|
|
59
|
+
) -> list[DomainCheckResult]:
|
|
60
|
+
return ConfirmWithDns(lookup).apply(results)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _Collector(ResultWriter):
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
self.results: list[DomainCheckResult] = []
|
|
66
|
+
|
|
67
|
+
def write_result(self, result: DomainCheckResult) -> None:
|
|
68
|
+
self.results.append(result)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _brand_name(value: str) -> str:
|
|
72
|
+
name = value.strip().lower()
|
|
73
|
+
if not name or any(c.isspace() for c in name):
|
|
74
|
+
raise argparse.ArgumentTypeError(f"not a single name: {value!r}")
|
|
75
|
+
if "." in name:
|
|
76
|
+
raise argparse.ArgumentTypeError(
|
|
77
|
+
f"give the name without a TLD (e.g. {name.split('.')[0]!r}), got {value!r}"
|
|
78
|
+
)
|
|
79
|
+
return name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def register(sub: argparse._SubParsersAction[argparse.ArgumentParser], *, epilog: str) -> None:
|
|
83
|
+
"""Add the ``name`` subcommand to the CLI's subparsers."""
|
|
84
|
+
presets = ", ".join(load_tld_presets())
|
|
85
|
+
parser = sub.add_parser(
|
|
86
|
+
"name",
|
|
87
|
+
help="Find a domain for a brand name (exact, hacks, variants) and recommend one",
|
|
88
|
+
description="Check NAME under a set of TLDs, look for domain hacks that spell it "
|
|
89
|
+
"(sumanda -> sumandastud.io) and, with --variants, brand variants "
|
|
90
|
+
"(getsumanda.com); then print a report with a recommendation.",
|
|
91
|
+
epilog=epilog,
|
|
92
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument("name", type=_brand_name, help="The name, without a TLD (e.g. sumanda)")
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--tlds",
|
|
97
|
+
default=DEFAULT_PRESET,
|
|
98
|
+
metavar="PRESET|TLD[,...]",
|
|
99
|
+
help=f"Presets ({presets}) and TLDs, mixed: startup,com.ar,la (default: {DEFAULT_PRESET})",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--variants",
|
|
103
|
+
action="store_true",
|
|
104
|
+
help="Also check get-/use-/try-/hola-/my- NAME and NAME -hq/-app/-labs/-studio, "
|
|
105
|
+
"under .com and the first 3 other TLDs",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--format",
|
|
109
|
+
choices=REPORT_FORMATS,
|
|
110
|
+
default="text",
|
|
111
|
+
help="Report format on stdout (default: text)",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--output",
|
|
115
|
+
type=Path,
|
|
116
|
+
default=None,
|
|
117
|
+
help="Also save the raw results to FILE (.csv, .json or .jsonl)",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--confirm-dns",
|
|
121
|
+
action=argparse.BooleanOptionalAction,
|
|
122
|
+
default=None,
|
|
123
|
+
help="Confirm results against public DNS (default: on when a DNS resolver is available)",
|
|
124
|
+
)
|
|
125
|
+
app.add_run_options(parser)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_name(
|
|
129
|
+
args: argparse.Namespace,
|
|
130
|
+
*,
|
|
131
|
+
catalog: app.RegistrarCatalog | None = None,
|
|
132
|
+
services: NameServices | None = None,
|
|
133
|
+
) -> int:
|
|
134
|
+
services = services if services is not None else _default_services()
|
|
135
|
+
notes: list[str] = []
|
|
136
|
+
selection = resolve_tld_spec(args.tlds, load_tld_presets(), services.supported_tlds)
|
|
137
|
+
for suffix in selection.rejected:
|
|
138
|
+
notes.append(f".{suffix} skipped: not a TLD this version can check")
|
|
139
|
+
app._stderr(f"warning: .{suffix} is not a TLD this version can check; skipping")
|
|
140
|
+
if not selection.tlds:
|
|
141
|
+
app._stderr(f"error: no valid TLD in --tlds {args.tlds!r}")
|
|
142
|
+
return app.EXIT_USAGE
|
|
143
|
+
output: Path | None = args.output
|
|
144
|
+
fmt = app._resolve_output_format(output, None) if output is not None else None
|
|
145
|
+
|
|
146
|
+
confirm = args.confirm_dns if args.confirm_dns is not None else services.dns is not None
|
|
147
|
+
if confirm and services.dns is None:
|
|
148
|
+
app._stderr("warning: no DNS resolver available; results are not confirmed in DNS")
|
|
149
|
+
confirm = False
|
|
150
|
+
|
|
151
|
+
generator = BrandCandidates(
|
|
152
|
+
args.name,
|
|
153
|
+
selection.tlds,
|
|
154
|
+
services.known_tlds,
|
|
155
|
+
variants=args.variants,
|
|
156
|
+
hack_words=load_hack_words(),
|
|
157
|
+
)
|
|
158
|
+
candidates = generator.execute()
|
|
159
|
+
if not candidates:
|
|
160
|
+
app._stderr(f"error: {args.name!r} is not a valid domain name label")
|
|
161
|
+
return app.EXIT_USAGE
|
|
162
|
+
if generator.skipped:
|
|
163
|
+
notes.append(f"skipped {generator.skipped} invalid candidates")
|
|
164
|
+
for suffix in generator.unusable_suffixes:
|
|
165
|
+
notes.append(f"hack under .{suffix} skipped: not a TLD this version can check")
|
|
166
|
+
|
|
167
|
+
router = app._build_router(args, catalog)
|
|
168
|
+
try:
|
|
169
|
+
unsupported = {c.domain.tld for c in candidates if not router.supports(c.domain.tld)}
|
|
170
|
+
domains = [c.domain for c in candidates if c.domain.tld not in unsupported]
|
|
171
|
+
requested = [t.suffix for t in selection.tlds if t in unsupported]
|
|
172
|
+
for message in describe_unsupported(requested):
|
|
173
|
+
app._stderr(f"warning: {message}; skipping")
|
|
174
|
+
if not domains:
|
|
175
|
+
raise app.NoSupportedTLDError(
|
|
176
|
+
f"no registrar supports any candidate TLD for {args.name!r}"
|
|
177
|
+
)
|
|
178
|
+
file_writer = app.build_file_writer(output, fmt) if output and fmt else None
|
|
179
|
+
except BaseException:
|
|
180
|
+
router.close()
|
|
181
|
+
raise
|
|
182
|
+
|
|
183
|
+
collector = _Collector()
|
|
184
|
+
with app._build_registrar(args, router) as registrar:
|
|
185
|
+
cache = registrar if isinstance(registrar, CachedRegistrarClient) else None
|
|
186
|
+
use_case = CheckDomainsUseCase(
|
|
187
|
+
router,
|
|
188
|
+
collector,
|
|
189
|
+
app._build_progress(args),
|
|
190
|
+
cache=cache,
|
|
191
|
+
parallel=args.parallel,
|
|
192
|
+
lane_key=app._lane_key(args, router),
|
|
193
|
+
)
|
|
194
|
+
summary = use_case.execute(domains, total=len(domains))
|
|
195
|
+
|
|
196
|
+
order = {c.domain.fqdn: i for i, c in enumerate(candidates)}
|
|
197
|
+
results = sorted(collector.results, key=lambda r: order[r.domain.fqdn])
|
|
198
|
+
try:
|
|
199
|
+
if confirm and services.dns is not None and not summary.interrupted:
|
|
200
|
+
results = _confirm_with_dns(services.dns, results)
|
|
201
|
+
finally:
|
|
202
|
+
if file_writer is not None:
|
|
203
|
+
for result in results:
|
|
204
|
+
file_writer.write_result(result)
|
|
205
|
+
file_writer.flush()
|
|
206
|
+
|
|
207
|
+
if summary.interrupted:
|
|
208
|
+
notes.append("run interrupted: some names were not checked")
|
|
209
|
+
report = build_brand_report(
|
|
210
|
+
generator.name,
|
|
211
|
+
candidates,
|
|
212
|
+
results,
|
|
213
|
+
tlds=selection.tlds,
|
|
214
|
+
suffix_hack_found=generator.suffix_hack_found,
|
|
215
|
+
variants_requested=args.variants,
|
|
216
|
+
unsupported=unsupported,
|
|
217
|
+
notes=notes,
|
|
218
|
+
unsupported_reasons={t: unsupported_message(t.suffix) for t in unsupported},
|
|
219
|
+
)
|
|
220
|
+
sys.stdout.write(RENDERERS[args.format](report))
|
|
221
|
+
app._warn_skipped(generator.skipped)
|
|
222
|
+
return app._report(summary)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""``domainhack tlds``: which TLDs this version can check, how, and why not.
|
|
2
|
+
|
|
3
|
+
Offline: it reads the bundled IANA TLD list, RDAP bootstrap snapshot and
|
|
4
|
+
registry data, so the answer is the same on every machine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import dataclasses
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
from domainhack.adapters.iana_tlds import IanaTldList
|
|
17
|
+
from domainhack.adapters.rdap_bootstrap import RdapBootstrap
|
|
18
|
+
from domainhack.adapters.registrar_catalog import Backend, describe_backend
|
|
19
|
+
|
|
20
|
+
EXIT_OK = 0
|
|
21
|
+
EXIT_USAGE = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(sub: argparse._SubParsersAction[argparse.ArgumentParser], *, epilog: str) -> None:
|
|
25
|
+
"""Add the ``tlds`` subcommand to the CLI's subparsers."""
|
|
26
|
+
parser = sub.add_parser(
|
|
27
|
+
"tlds",
|
|
28
|
+
help="Show which TLDs can be checked, and how (RDAP or WHOIS) or why not",
|
|
29
|
+
description="List TLDs with the backend used to check them. Without arguments: "
|
|
30
|
+
"a coverage summary plus every country-code TLD. Offline.",
|
|
31
|
+
epilog=epilog,
|
|
32
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"only", nargs="*", metavar="TLD", help="Only these TLDs (e.g. cl es com.ar)"
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--unsupported", action="store_true", help="Only TLDs that cannot be checked, with why"
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--all", action="store_true", help="Include generic TLDs (com, app...), not only ccTLDs"
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("--json", action="store_true", help="Machine-readable output")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _offline_bootstrap() -> RdapBootstrap:
|
|
47
|
+
def no_fetch() -> bytes:
|
|
48
|
+
raise OSError("offline")
|
|
49
|
+
|
|
50
|
+
return RdapBootstrap(fetcher=no_fetch, warn=lambda _message: None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_tlds(args: argparse.Namespace, bootstrap: RdapBootstrap | None = None) -> int:
|
|
54
|
+
rdap = bootstrap if bootstrap is not None else _offline_bootstrap()
|
|
55
|
+
known = IanaTldList()
|
|
56
|
+
if args.only:
|
|
57
|
+
suffixes = [s.lower().lstrip(".") for s in args.only]
|
|
58
|
+
unknown = [s for s in suffixes if not known.is_known(s)]
|
|
59
|
+
if unknown:
|
|
60
|
+
print(f"domainhack tlds: error: not a known TLD: {', '.join(unknown)}", file=sys.stderr)
|
|
61
|
+
return EXIT_USAGE
|
|
62
|
+
else:
|
|
63
|
+
suffixes = sorted(
|
|
64
|
+
t
|
|
65
|
+
for t in known.top_level_domains
|
|
66
|
+
if not t.startswith("xn--") and (args.all or len(t) == 2)
|
|
67
|
+
)
|
|
68
|
+
every = [describe_backend(s, rdap) for s in suffixes]
|
|
69
|
+
shown = [b for b in every if b.kind == "unsupported"] if args.unsupported else every
|
|
70
|
+
if args.json:
|
|
71
|
+
print(json.dumps([dataclasses.asdict(b) for b in shown], indent=2))
|
|
72
|
+
sys.stdout.flush() # surface a closed pipe (| head) to main's handler
|
|
73
|
+
return EXIT_OK
|
|
74
|
+
if not args.only:
|
|
75
|
+
print(_summary(every))
|
|
76
|
+
print()
|
|
77
|
+
_print_table(shown)
|
|
78
|
+
sys.stdout.flush() # surface a closed pipe (| head) to main's handler
|
|
79
|
+
return EXIT_OK
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _summary(backends: Sequence[Backend]) -> str:
|
|
83
|
+
counts = Counter(b.kind for b in backends)
|
|
84
|
+
supported = len(backends) - counts["unsupported"]
|
|
85
|
+
parts = [
|
|
86
|
+
f"{counts[k]} {k}" for k in ("rdap", "whois", "whois-fallback", "unsupported") if counts[k]
|
|
87
|
+
]
|
|
88
|
+
return f"{supported} of {len(backends)} TLDs can be checked ({', '.join(parts)})"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _print_table(backends: Sequence[Backend]) -> None:
|
|
92
|
+
width_tld = max((len(b.tld) for b in backends), default=3) + 1
|
|
93
|
+
width_kind = max((len(b.kind) for b in backends), default=4)
|
|
94
|
+
width_host = max((len(b.host) for b in backends), default=4)
|
|
95
|
+
for b in backends:
|
|
96
|
+
print(f" .{b.tld:<{width_tld}} {b.kind:<{width_kind}} {b.host:<{width_host}} {b.detail}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Words appended to a name to form a domain hack: name + word must end in a known TLD (sumanda + studio -> sumandastud.io). A word whose ending is not a known TLD is simply never used. Each word maps to the short meaning shown in the report.",
|
|
3
|
+
"studio": "a creative or production studio",
|
|
4
|
+
"radio": "a radio show or podcast",
|
|
5
|
+
"audio": "audio, music or sound",
|
|
6
|
+
"folio": "a portfolio",
|
|
7
|
+
"portfolio": "a portfolio of work",
|
|
8
|
+
"demo": "a demo or showcase",
|
|
9
|
+
"labs": "a lab or experiments",
|
|
10
|
+
"online": "an online presence",
|
|
11
|
+
"games": "games",
|
|
12
|
+
"apps": "apps",
|
|
13
|
+
"tech": "technology",
|
|
14
|
+
"design": "design",
|
|
15
|
+
"team": "the team",
|
|
16
|
+
"store": "a store or shop",
|
|
17
|
+
"house": "a house, home or venue",
|
|
18
|
+
"news": "news or a newsletter",
|
|
19
|
+
"zone": "a zone or community space"
|
|
20
|
+
}
|