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.
Files changed (39) hide show
  1. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/CONTRIBUTING.md +3 -1
  2. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/PKG-INFO +66 -10
  3. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/README.md +65 -9
  4. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/pyproject.toml +1 -1
  5. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/__init__.py +7 -1
  6. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/domains.py +56 -139
  7. namecheap_python-1.5.0/src/namecheap/_api/users.py +102 -0
  8. namecheap_python-1.5.0/src/namecheap/_api/whoisguard.py +195 -0
  9. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/client.py +8 -0
  10. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/models.py +104 -0
  11. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/__main__.py +368 -0
  12. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/uv.lock +1 -1
  13. namecheap_python-1.3.0/src/namecheap/_api/users.py +0 -32
  14. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.env.example +0 -0
  15. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.github/cliff.toml +0 -0
  16. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.github/workflows/release.yml +0 -0
  17. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.gitignore +0 -0
  18. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/.pre-commit-config.yaml +0 -0
  19. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/CLAUDE.md +0 -0
  20. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/CLI.md +0 -0
  21. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/LICENSE +0 -0
  22. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/MANIFEST.in +0 -0
  23. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/examples/README.md +0 -0
  24. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/examples/quickstart.py +0 -0
  25. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/__init__.py +0 -0
  26. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/base.py +0 -0
  27. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/_api/dns.py +0 -0
  28. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/errors.py +0 -0
  29. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap/logging.py +0 -0
  30. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/README.md +0 -0
  31. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/__init__.py +0 -0
  32. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_cli/completion.py +0 -0
  33. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/README.md +0 -0
  34. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/__init__.py +0 -0
  35. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/__main__.py +0 -0
  36. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot1.png +0 -0
  37. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot2.png +0 -0
  38. {namecheap_python-1.3.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot3.png +0 -0
  39. {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
- └── dns.py # namecheap.domains.dns.* endpoints + builder
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.0
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.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
483
- | `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
484
- | `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
485
- | `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
486
- | `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
487
- | `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
488
- | `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
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 the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
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.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
441
- | `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
442
- | `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
443
- | `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
444
- | `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
445
- | `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
446
- | `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
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 the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "namecheap-python"
3
- version = "1.3.0"
3
+ version = "1.5.0"
4
4
  description = "A friendly Python SDK for Namecheap API"
5
5
  authors = [{name = "Adrian Galilea Delgado", email = "adriangalilea@gmail.com"}]
6
6
  readme = "README.md"
@@ -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.3.0"
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 Contact, Domain, DomainCheck, DomainContacts, DomainInfo
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
- Args:
390
- domains: List of domain names
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 for efficient API calls
428
+ # Group domains by TLD
399
429
  tld_groups: dict[str, builtins.list[str]] = {}
400
430
  for domain in domains:
401
- ext = tldextract.extract(domain)
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
- try:
412
- logger.debug(f"Fetching pricing for TLD: {tld}")
413
- # Get pricing for this TLD
414
- result: Any = self._request(
415
- "namecheap.users.getPricing",
416
- {
417
- "ProductType": "DOMAIN",
418
- "ActionName": "REGISTER",
419
- "ProductName": tld,
420
- },
421
- path="UserGetPricingResult.ProductType",
422
- )
423
- assert isinstance(result, dict)
424
- logger.debug(f"Pricing API response for {tld}: {result}")
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