namecheap-python 1.3.0__tar.gz → 1.5.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.
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/CONTRIBUTING.md +3 -1
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/PKG-INFO +66 -10
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/README.md +65 -9
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/pyproject.toml +1 -1
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/__init__.py +7 -1
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/domains.py +56 -139
- namecheap_python-1.5.0/src/namecheap/_api/users.py +102 -0
- namecheap_python-1.5.0/src/namecheap/_api/whoisguard.py +195 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/client.py +8 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/models.py +104 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/__main__.py +368 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/uv.lock +1 -1
- namecheap_python-1.3.0/src/namecheap/_api/users.py +0 -32
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.env.example +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.github/cliff.toml +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.github/workflows/release.yml +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.gitignore +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.pre-commit-config.yaml +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/CLAUDE.md +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/CLI.md +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/LICENSE +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/MANIFEST.in +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/examples/README.md +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/examples/quickstart.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/__init__.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/base.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/dns.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/errors.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/logging.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/README.md +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/__init__.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/completion.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/README.md +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/__init__.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/__main__.py +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot1.png +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot2.png +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot3.png +0 -0
- {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot4.png +0 -0
|
@@ -54,7 +54,9 @@ src/
|
|
|
54
54
|
│ └── _api/ # API implementations
|
|
55
55
|
│ ├── base.py # BaseAPI with _request() — all API calls go through here
|
|
56
56
|
│ ├── domains.py # namecheap.domains.* endpoints
|
|
57
|
-
│
|
|
57
|
+
│ ├── dns.py # namecheap.domains.dns.* endpoints + builder
|
|
58
|
+
│ ├── users.py # namecheap.users.* endpoints
|
|
59
|
+
│ └── whoisguard.py # namecheap.whoisguard.* endpoints (domain privacy)
|
|
58
60
|
├── namecheap_cli/ # CLI (click)
|
|
59
61
|
│ ├── __main__.py # All commands in one file
|
|
60
62
|
│ └── completion.py # Shell completions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: namecheap-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: A friendly Python SDK for Namecheap API
|
|
5
5
|
Project-URL: Homepage, https://github.com/adriangalilea/namecheap-python
|
|
6
6
|
Project-URL: Repository, https://github.com/adriangalilea/namecheap-python
|
|
@@ -377,6 +377,18 @@ print(f"{bal.available_balance} {bal.currency}") # '4932.96 USD'
|
|
|
377
377
|
print(bal.funds_required_for_auto_renew) # Decimal('20.16')
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
### Pricing
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
# Get registration pricing for a specific TLD
|
|
384
|
+
pricing = nc.users.get_pricing("DOMAIN", action="REGISTER", product_name="com")
|
|
385
|
+
for p in pricing["REGISTER"]["com"]:
|
|
386
|
+
print(f"{p.duration} year: ${p.your_price} (regular: ${p.regular_price})")
|
|
387
|
+
|
|
388
|
+
# Get all domain pricing (large response — cache it)
|
|
389
|
+
all_pricing = nc.users.get_pricing("DOMAIN")
|
|
390
|
+
```
|
|
391
|
+
|
|
380
392
|
### Email Forwarding
|
|
381
393
|
|
|
382
394
|
```python
|
|
@@ -400,6 +412,41 @@ print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
|
|
|
400
412
|
print(contacts.registrant.email)
|
|
401
413
|
```
|
|
402
414
|
|
|
415
|
+
### TLD List
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
tlds = nc.domains.get_tld_list()
|
|
419
|
+
print(f"{len(tlds)} TLDs supported")
|
|
420
|
+
|
|
421
|
+
# Filter to API-registerable TLDs
|
|
422
|
+
registerable = [t for t in tlds if t.is_api_registerable]
|
|
423
|
+
for t in registerable[:5]:
|
|
424
|
+
print(f".{t.name} ({t.type}) — {t.min_register_years}-{t.max_register_years} years")
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Domain Privacy (WhoisGuard)
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
# List all WhoisGuard subscriptions
|
|
431
|
+
entries = nc.whoisguard.get_list()
|
|
432
|
+
for e in entries:
|
|
433
|
+
print(f"{e.domain} (ID={e.id}) status={e.status}")
|
|
434
|
+
|
|
435
|
+
# Enable privacy (resolves WhoisGuard ID from domain name automatically)
|
|
436
|
+
nc.whoisguard.enable("example.com", "me@gmail.com")
|
|
437
|
+
|
|
438
|
+
# Disable privacy
|
|
439
|
+
nc.whoisguard.disable("example.com")
|
|
440
|
+
|
|
441
|
+
# Renew privacy
|
|
442
|
+
result = nc.whoisguard.renew("example.com", years=1)
|
|
443
|
+
print(f"Charged: {result['charged_amount']}")
|
|
444
|
+
|
|
445
|
+
# Rotate the masked forwarding email
|
|
446
|
+
result = nc.whoisguard.change_email("example.com")
|
|
447
|
+
print(f"New: {result['new_email']}")
|
|
448
|
+
```
|
|
449
|
+
|
|
403
450
|
### Domain Management
|
|
404
451
|
|
|
405
452
|
```python
|
|
@@ -461,6 +508,15 @@ except NamecheapError as e:
|
|
|
461
508
|
|
|
462
509
|
This section documents undocumented or unusual Namecheap API behaviors we've discovered:
|
|
463
510
|
|
|
511
|
+
### No WHOIS lookups or Marketplace data
|
|
512
|
+
|
|
513
|
+
The Namecheap API only operates on domains **in your account**. There is no API for:
|
|
514
|
+
- WHOIS lookups on arbitrary domains
|
|
515
|
+
- Checking if a domain is listed on [Namecheap Marketplace](https://www.namecheap.com/domains/marketplace/)
|
|
516
|
+
- Aftermarket pricing or availability
|
|
517
|
+
|
|
518
|
+
`domains.check()` tells you if a domain is **unregistered**, not if it's for sale by its owner.
|
|
519
|
+
|
|
464
520
|
### TTL "Automatic" = 1799 seconds
|
|
465
521
|
|
|
466
522
|
The Namecheap web interface displays TTL as **"Automatic"** when the value is exactly **1799 seconds**, but shows **"30 min"** when it's **1800 seconds**. This behavior is completely undocumented in their official API documentation.
|
|
@@ -477,15 +533,15 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
|
477
533
|
|
|
478
534
|
| API | Status | Methods |
|
|
479
535
|
|-----|--------|---------|
|
|
480
|
-
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
536
|
+
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
481
537
|
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
|
|
482
|
-
| `namecheap.
|
|
483
|
-
| `namecheap.
|
|
484
|
-
| `namecheap.users.address.*` | 🚧 Planned |
|
|
485
|
-
| `namecheap.ssl.*` | 🚧 Planned |
|
|
486
|
-
| `namecheap.domains.transfer.*` | 🚧 Planned |
|
|
487
|
-
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records —
|
|
488
|
-
| `namecheap.
|
|
538
|
+
| `namecheap.whoisguard.*` | ✅ Done | `getList`, `enable`, `disable`, `renew`, `changeEmailAddress` |
|
|
539
|
+
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing`. Remaining methods are account management (`changePassword`, `update`, `create`, `login`, `resetPassword`) — only useful if building a reseller platform |
|
|
540
|
+
| `namecheap.users.address.*` | 🚧 Planned | Saved address book for `domains.register()` — store contacts once, reuse by ID instead of passing full contact info every time |
|
|
541
|
+
| `namecheap.ssl.*` | 🚧 Planned | Full SSL certificate lifecycle — purchase, activate with CSR, renew, revoke, reissue. Complex multi-step workflows with approval emails |
|
|
542
|
+
| `namecheap.domains.transfer.*` | 🚧 Planned | Transfer domains into Namecheap programmatically — initiate, track status, retry |
|
|
543
|
+
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records — only needed if you run your own nameservers and need to register them with the registry |
|
|
544
|
+
| `namecheap.domains.*` | 🚧 Planned | `reactivate` — restore expired domains within the redemption grace period |
|
|
489
545
|
|
|
490
546
|
## 🛠️ Development
|
|
491
547
|
|
|
@@ -497,7 +553,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
497
553
|
|
|
498
554
|
## 🤝 Contributing
|
|
499
555
|
|
|
500
|
-
Contributions are welcome! Please feel free to submit a Pull Request. See
|
|
556
|
+
Contributions are welcome! Please feel free to submit a Pull Request. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
501
557
|
|
|
502
558
|
### Contributors
|
|
503
559
|
|
|
@@ -335,6 +335,18 @@ print(f"{bal.available_balance} {bal.currency}") # '4932.96 USD'
|
|
|
335
335
|
print(bal.funds_required_for_auto_renew) # Decimal('20.16')
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
+
### Pricing
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
# Get registration pricing for a specific TLD
|
|
342
|
+
pricing = nc.users.get_pricing("DOMAIN", action="REGISTER", product_name="com")
|
|
343
|
+
for p in pricing["REGISTER"]["com"]:
|
|
344
|
+
print(f"{p.duration} year: ${p.your_price} (regular: ${p.regular_price})")
|
|
345
|
+
|
|
346
|
+
# Get all domain pricing (large response — cache it)
|
|
347
|
+
all_pricing = nc.users.get_pricing("DOMAIN")
|
|
348
|
+
```
|
|
349
|
+
|
|
338
350
|
### Email Forwarding
|
|
339
351
|
|
|
340
352
|
```python
|
|
@@ -358,6 +370,41 @@ print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
|
|
|
358
370
|
print(contacts.registrant.email)
|
|
359
371
|
```
|
|
360
372
|
|
|
373
|
+
### TLD List
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
tlds = nc.domains.get_tld_list()
|
|
377
|
+
print(f"{len(tlds)} TLDs supported")
|
|
378
|
+
|
|
379
|
+
# Filter to API-registerable TLDs
|
|
380
|
+
registerable = [t for t in tlds if t.is_api_registerable]
|
|
381
|
+
for t in registerable[:5]:
|
|
382
|
+
print(f".{t.name} ({t.type}) — {t.min_register_years}-{t.max_register_years} years")
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Domain Privacy (WhoisGuard)
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
# List all WhoisGuard subscriptions
|
|
389
|
+
entries = nc.whoisguard.get_list()
|
|
390
|
+
for e in entries:
|
|
391
|
+
print(f"{e.domain} (ID={e.id}) status={e.status}")
|
|
392
|
+
|
|
393
|
+
# Enable privacy (resolves WhoisGuard ID from domain name automatically)
|
|
394
|
+
nc.whoisguard.enable("example.com", "me@gmail.com")
|
|
395
|
+
|
|
396
|
+
# Disable privacy
|
|
397
|
+
nc.whoisguard.disable("example.com")
|
|
398
|
+
|
|
399
|
+
# Renew privacy
|
|
400
|
+
result = nc.whoisguard.renew("example.com", years=1)
|
|
401
|
+
print(f"Charged: {result['charged_amount']}")
|
|
402
|
+
|
|
403
|
+
# Rotate the masked forwarding email
|
|
404
|
+
result = nc.whoisguard.change_email("example.com")
|
|
405
|
+
print(f"New: {result['new_email']}")
|
|
406
|
+
```
|
|
407
|
+
|
|
361
408
|
### Domain Management
|
|
362
409
|
|
|
363
410
|
```python
|
|
@@ -419,6 +466,15 @@ except NamecheapError as e:
|
|
|
419
466
|
|
|
420
467
|
This section documents undocumented or unusual Namecheap API behaviors we've discovered:
|
|
421
468
|
|
|
469
|
+
### No WHOIS lookups or Marketplace data
|
|
470
|
+
|
|
471
|
+
The Namecheap API only operates on domains **in your account**. There is no API for:
|
|
472
|
+
- WHOIS lookups on arbitrary domains
|
|
473
|
+
- Checking if a domain is listed on [Namecheap Marketplace](https://www.namecheap.com/domains/marketplace/)
|
|
474
|
+
- Aftermarket pricing or availability
|
|
475
|
+
|
|
476
|
+
`domains.check()` tells you if a domain is **unregistered**, not if it's for sale by its owner.
|
|
477
|
+
|
|
422
478
|
### TTL "Automatic" = 1799 seconds
|
|
423
479
|
|
|
424
480
|
The Namecheap web interface displays TTL as **"Automatic"** when the value is exactly **1799 seconds**, but shows **"30 min"** when it's **1800 seconds**. This behavior is completely undocumented in their official API documentation.
|
|
@@ -435,15 +491,15 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
|
435
491
|
|
|
436
492
|
| API | Status | Methods |
|
|
437
493
|
|-----|--------|---------|
|
|
438
|
-
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
494
|
+
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
439
495
|
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
|
|
440
|
-
| `namecheap.
|
|
441
|
-
| `namecheap.
|
|
442
|
-
| `namecheap.users.address.*` | 🚧 Planned |
|
|
443
|
-
| `namecheap.ssl.*` | 🚧 Planned |
|
|
444
|
-
| `namecheap.domains.transfer.*` | 🚧 Planned |
|
|
445
|
-
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records —
|
|
446
|
-
| `namecheap.
|
|
496
|
+
| `namecheap.whoisguard.*` | ✅ Done | `getList`, `enable`, `disable`, `renew`, `changeEmailAddress` |
|
|
497
|
+
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing`. Remaining methods are account management (`changePassword`, `update`, `create`, `login`, `resetPassword`) — only useful if building a reseller platform |
|
|
498
|
+
| `namecheap.users.address.*` | 🚧 Planned | Saved address book for `domains.register()` — store contacts once, reuse by ID instead of passing full contact info every time |
|
|
499
|
+
| `namecheap.ssl.*` | 🚧 Planned | Full SSL certificate lifecycle — purchase, activate with CSR, renew, revoke, reissue. Complex multi-step workflows with approval emails |
|
|
500
|
+
| `namecheap.domains.transfer.*` | 🚧 Planned | Transfer domains into Namecheap programmatically — initiate, track status, retry |
|
|
501
|
+
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records — only needed if you run your own nameservers and need to register them with the registry |
|
|
502
|
+
| `namecheap.domains.*` | 🚧 Planned | `reactivate` — restore expired domains within the redemption grace period |
|
|
447
503
|
|
|
448
504
|
## 🛠️ Development
|
|
449
505
|
|
|
@@ -455,7 +511,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
455
511
|
|
|
456
512
|
## 🤝 Contributing
|
|
457
513
|
|
|
458
|
-
Contributions are welcome! Please feel free to submit a Pull Request. See
|
|
514
|
+
Contributions are welcome! Please feel free to submit a Pull Request. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
459
515
|
|
|
460
516
|
### Contributors
|
|
461
517
|
|
|
@@ -22,9 +22,12 @@ from .models import (
|
|
|
22
22
|
DomainInfo,
|
|
23
23
|
EmailForward,
|
|
24
24
|
Nameservers,
|
|
25
|
+
ProductPrice,
|
|
26
|
+
Tld,
|
|
27
|
+
WhoisguardEntry,
|
|
25
28
|
)
|
|
26
29
|
|
|
27
|
-
__version__ = "1.
|
|
30
|
+
__version__ = "1.5.0"
|
|
28
31
|
__all__ = [
|
|
29
32
|
"AccountBalance",
|
|
30
33
|
"ConfigurationError",
|
|
@@ -38,5 +41,8 @@ __all__ = [
|
|
|
38
41
|
"Namecheap",
|
|
39
42
|
"NamecheapError",
|
|
40
43
|
"Nameservers",
|
|
44
|
+
"ProductPrice",
|
|
45
|
+
"Tld",
|
|
41
46
|
"ValidationError",
|
|
47
|
+
"WhoisguardEntry",
|
|
42
48
|
]
|
|
@@ -9,7 +9,14 @@ from typing import Any
|
|
|
9
9
|
import tldextract
|
|
10
10
|
|
|
11
11
|
from namecheap.logging import logger
|
|
12
|
-
from namecheap.models import
|
|
12
|
+
from namecheap.models import (
|
|
13
|
+
Contact,
|
|
14
|
+
Domain,
|
|
15
|
+
DomainCheck,
|
|
16
|
+
DomainContacts,
|
|
17
|
+
DomainInfo,
|
|
18
|
+
Tld,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
from .base import BaseAPI
|
|
15
22
|
|
|
@@ -202,6 +209,34 @@ class DomainsAPI(BaseAPI):
|
|
|
202
209
|
aux_billing=parse_contact(result.get("AuxBilling", {})),
|
|
203
210
|
)
|
|
204
211
|
|
|
212
|
+
def get_tld_list(self) -> builtins.list[Tld]:
|
|
213
|
+
"""
|
|
214
|
+
Get list of all TLDs supported by Namecheap.
|
|
215
|
+
|
|
216
|
+
NOTE: Cache this response — it rarely changes and the API docs recommend it.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of Tld objects with registration/renewal constraints and capabilities
|
|
220
|
+
|
|
221
|
+
Examples:
|
|
222
|
+
>>> tlds = nc.domains.get_tld_list()
|
|
223
|
+
>>> registerable = [t for t in tlds if t.is_api_registerable]
|
|
224
|
+
>>> print(f"{len(registerable)} TLDs available for API registration")
|
|
225
|
+
"""
|
|
226
|
+
result: Any = self._request(
|
|
227
|
+
"namecheap.domains.getTldList",
|
|
228
|
+
path="Tlds",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert result, "API returned empty result for getTldList"
|
|
232
|
+
|
|
233
|
+
tlds = result.get("Tld", [])
|
|
234
|
+
if isinstance(tlds, dict):
|
|
235
|
+
tlds = [tlds]
|
|
236
|
+
assert isinstance(tlds, list), f"Unexpected Tld type: {type(tlds)}"
|
|
237
|
+
|
|
238
|
+
return [Tld.model_validate(t) for t in tlds]
|
|
239
|
+
|
|
205
240
|
def register(
|
|
206
241
|
self,
|
|
207
242
|
domain: str,
|
|
@@ -383,153 +418,35 @@ class DomainsAPI(BaseAPI):
|
|
|
383
418
|
def _get_pricing(
|
|
384
419
|
self, domains: builtins.list[str]
|
|
385
420
|
) -> dict[str, dict[str, Decimal | None]]:
|
|
386
|
-
"""
|
|
387
|
-
Get pricing information for domains.
|
|
421
|
+
"""Get 1-year registration pricing for a list of domains.
|
|
388
422
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Returns:
|
|
393
|
-
Dict mapping domain to pricing info
|
|
423
|
+
Groups by TLD, fetches pricing via users.get_pricing(), and maps
|
|
424
|
+
back to individual domain names.
|
|
394
425
|
"""
|
|
395
|
-
pricing = {}
|
|
396
|
-
logger.debug(f"Getting pricing for domains: {domains}")
|
|
426
|
+
pricing: dict[str, dict[str, Decimal | None]] = {}
|
|
397
427
|
|
|
398
|
-
# Group domains by TLD
|
|
428
|
+
# Group domains by TLD
|
|
399
429
|
tld_groups: dict[str, builtins.list[str]] = {}
|
|
400
430
|
for domain in domains:
|
|
401
|
-
|
|
402
|
-
tld = ext.suffix
|
|
431
|
+
tld = tldextract.extract(domain).suffix
|
|
403
432
|
if tld not in tld_groups:
|
|
404
433
|
tld_groups[tld] = []
|
|
405
434
|
tld_groups[tld].append(domain)
|
|
406
435
|
|
|
407
|
-
logger.debug(f"TLD groups: {tld_groups}")
|
|
408
|
-
|
|
409
|
-
# Fetch pricing for each TLD group
|
|
410
436
|
for tld, domain_list in tld_groups.items():
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
logger.debug(f"Response type: {type(result)}")
|
|
426
|
-
logger.debug(
|
|
427
|
-
f"Response keys: "
|
|
428
|
-
f"{list(result.keys()) if isinstance(result, dict) else 'Not a dict'}"
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
# Extract pricing info
|
|
432
|
-
if isinstance(result, dict):
|
|
433
|
-
logger.debug(f"Parsing pricing response for {tld}")
|
|
434
|
-
|
|
435
|
-
# Get ProductCategory (could be a list or single dict)
|
|
436
|
-
categories = result.get("ProductCategory", {})
|
|
437
|
-
if not isinstance(categories, list):
|
|
438
|
-
categories = [categories] if categories else []
|
|
439
|
-
|
|
440
|
-
logger.debug(f"Found {len(categories)} categories")
|
|
441
|
-
|
|
442
|
-
# Look for REGISTER category
|
|
443
|
-
for category in categories:
|
|
444
|
-
if not isinstance(category, dict):
|
|
445
|
-
continue
|
|
446
|
-
|
|
447
|
-
# Use normalized name for consistent access
|
|
448
|
-
category_name = category.get("@Name", "")
|
|
449
|
-
category_name_normalized = category.get(
|
|
450
|
-
"@Name_normalized", category_name.lower()
|
|
451
|
-
)
|
|
452
|
-
logger.debug(
|
|
453
|
-
f"Checking category: {category_name} "
|
|
454
|
-
f"(normalized: {category_name_normalized})"
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
if category_name_normalized == "register":
|
|
458
|
-
# Get products in this category
|
|
459
|
-
products = category.get("Product", {})
|
|
460
|
-
if not isinstance(products, list):
|
|
461
|
-
products = [products] if products else []
|
|
462
|
-
|
|
463
|
-
logger.debug(
|
|
464
|
-
f"Found {len(products)} products in REGISTER category"
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
# Find the product matching our TLD
|
|
468
|
-
for product in products:
|
|
469
|
-
if not isinstance(product, dict):
|
|
470
|
-
continue
|
|
471
|
-
|
|
472
|
-
product_name = product.get("@Name", "")
|
|
473
|
-
logger.debug(
|
|
474
|
-
f"Checking product: {product_name} vs {tld}"
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
if product_name.lower() == tld.lower():
|
|
478
|
-
# Get price list
|
|
479
|
-
price_info = product.get("Price", [])
|
|
480
|
-
if not isinstance(price_info, list):
|
|
481
|
-
price_info = [price_info] if price_info else []
|
|
482
|
-
|
|
483
|
-
logger.debug(
|
|
484
|
-
f"Found {len(price_info)} price entries for {tld}"
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
# Find 1 year price
|
|
488
|
-
for price in price_info:
|
|
489
|
-
if not isinstance(price, dict):
|
|
490
|
-
continue
|
|
491
|
-
|
|
492
|
-
duration = price.get("@Duration", "")
|
|
493
|
-
if duration == "1":
|
|
494
|
-
regular_price = price.get("@RegularPrice")
|
|
495
|
-
your_price = price.get("@YourPrice")
|
|
496
|
-
retail_price = price.get("@RetailPrice")
|
|
497
|
-
|
|
498
|
-
# Get additional cost
|
|
499
|
-
# (normalization handles their typo)
|
|
500
|
-
price.get("@YourAdditionalCost", "0")
|
|
501
|
-
|
|
502
|
-
logger.debug(
|
|
503
|
-
f"Found prices for {tld}: "
|
|
504
|
-
f"regular={regular_price}, "
|
|
505
|
-
f"your={your_price}, "
|
|
506
|
-
f"retail={retail_price}"
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
# Apply to all domains with this TLD
|
|
510
|
-
for domain in domain_list:
|
|
511
|
-
pricing[domain] = {
|
|
512
|
-
"regular_price": Decimal(
|
|
513
|
-
regular_price
|
|
514
|
-
)
|
|
515
|
-
if regular_price
|
|
516
|
-
else None,
|
|
517
|
-
"your_price": Decimal(your_price)
|
|
518
|
-
if your_price
|
|
519
|
-
else None,
|
|
520
|
-
"retail_price": Decimal(
|
|
521
|
-
retail_price
|
|
522
|
-
)
|
|
523
|
-
if retail_price
|
|
524
|
-
else None,
|
|
525
|
-
}
|
|
526
|
-
break
|
|
527
|
-
break
|
|
528
|
-
break
|
|
529
|
-
|
|
530
|
-
except Exception as e:
|
|
531
|
-
# If pricing fails, continue without it
|
|
532
|
-
logger.error(f"Failed to get pricing for TLD {tld}: {e}")
|
|
533
|
-
logger.debug("Full error:", exc_info=True)
|
|
437
|
+
result = self.client.users.get_pricing(
|
|
438
|
+
"DOMAIN", action="REGISTER", product_name=tld
|
|
439
|
+
)
|
|
440
|
+
prices = result.get("REGISTER", {}).get(tld, [])
|
|
441
|
+
|
|
442
|
+
for p in prices:
|
|
443
|
+
if p.duration == 1:
|
|
444
|
+
for domain in domain_list:
|
|
445
|
+
pricing[domain] = {
|
|
446
|
+
"regular_price": p.regular_price,
|
|
447
|
+
"your_price": p.your_price,
|
|
448
|
+
"retail_price": None,
|
|
449
|
+
}
|
|
450
|
+
break
|
|
534
451
|
|
|
535
452
|
return pricing
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Users API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from namecheap.models import AccountBalance, ProductPrice
|
|
8
|
+
|
|
9
|
+
from .base import BaseAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UsersAPI(BaseAPI):
|
|
13
|
+
"""User account operations."""
|
|
14
|
+
|
|
15
|
+
def get_balances(self) -> AccountBalance:
|
|
16
|
+
"""
|
|
17
|
+
Get account balance information.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
AccountBalance with available balance, earned amount, etc.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> bal = nc.users.get_balances()
|
|
24
|
+
>>> print(f"{bal.available_balance} {bal.currency}")
|
|
25
|
+
"""
|
|
26
|
+
result: Any = self._request(
|
|
27
|
+
"namecheap.users.getBalances",
|
|
28
|
+
path="UserGetBalancesResult",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert result, "API returned empty result for getBalances"
|
|
32
|
+
return AccountBalance.model_validate(result)
|
|
33
|
+
|
|
34
|
+
def get_pricing(
|
|
35
|
+
self,
|
|
36
|
+
product_type: Literal["DOMAIN", "SSLCERTIFICATE"] = "DOMAIN",
|
|
37
|
+
*,
|
|
38
|
+
action: str | None = None,
|
|
39
|
+
product_name: str | None = None,
|
|
40
|
+
) -> dict[str, dict[str, list[ProductPrice]]]:
|
|
41
|
+
"""
|
|
42
|
+
Get pricing for products.
|
|
43
|
+
|
|
44
|
+
Returns a nested dict: {action: {product: [prices]}}.
|
|
45
|
+
For example: {"REGISTER": {"com": [ProductPrice(duration=1, ...), ...]}}
|
|
46
|
+
|
|
47
|
+
NOTE: Cache this response — Namecheap recommends it.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
product_type: "DOMAIN" or "SSLCERTIFICATE"
|
|
51
|
+
action: Filter by action (REGISTER, RENEW, TRANSFER, REACTIVATE)
|
|
52
|
+
product_name: Filter by product/TLD name (e.g., "com")
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Nested dict of action -> product -> list of ProductPrice
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
>>> pricing = nc.users.get_pricing("DOMAIN", action="REGISTER", product_name="com")
|
|
59
|
+
>>> prices = pricing["REGISTER"]["com"]
|
|
60
|
+
>>> print(f"1-year .com: ${prices[0].your_price}")
|
|
61
|
+
"""
|
|
62
|
+
params: dict[str, Any] = {"ProductType": product_type}
|
|
63
|
+
if action:
|
|
64
|
+
params["ActionName"] = action
|
|
65
|
+
if product_name:
|
|
66
|
+
params["ProductName"] = product_name
|
|
67
|
+
|
|
68
|
+
result: Any = self._request(
|
|
69
|
+
"namecheap.users.getPricing",
|
|
70
|
+
params,
|
|
71
|
+
path="UserGetPricingResult.ProductType",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert result, "API returned empty result for getPricing"
|
|
75
|
+
|
|
76
|
+
pricing: dict[str, dict[str, list[ProductPrice]]] = {}
|
|
77
|
+
|
|
78
|
+
categories = result.get("ProductCategory", [])
|
|
79
|
+
if isinstance(categories, dict):
|
|
80
|
+
categories = [categories]
|
|
81
|
+
|
|
82
|
+
for category in categories:
|
|
83
|
+
action_name = category.get("@Name", "").upper()
|
|
84
|
+
|
|
85
|
+
products = category.get("Product", [])
|
|
86
|
+
if isinstance(products, dict):
|
|
87
|
+
products = [products]
|
|
88
|
+
|
|
89
|
+
for product in products:
|
|
90
|
+
name = product.get("@Name", "").lower()
|
|
91
|
+
|
|
92
|
+
prices = product.get("Price", [])
|
|
93
|
+
if isinstance(prices, dict):
|
|
94
|
+
prices = [prices]
|
|
95
|
+
|
|
96
|
+
parsed = [ProductPrice.model_validate(p) for p in prices]
|
|
97
|
+
|
|
98
|
+
if action_name not in pricing:
|
|
99
|
+
pricing[action_name] = {}
|
|
100
|
+
pricing[action_name][name] = parsed
|
|
101
|
+
|
|
102
|
+
return pricing
|