namecheap-python 1.4.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.4.0 → namecheap_python-1.5.0}/PKG-INFO +19 -7
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/README.md +18 -6
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/pyproject.toml +1 -1
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/__init__.py +3 -1
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/_api/domains.py +20 -138
- namecheap_python-1.5.0/src/namecheap/_api/users.py +102 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/models.py +28 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_cli/__main__.py +81 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/uv.lock +1 -1
- namecheap_python-1.4.0/src/namecheap/_api/users.py +0 -32
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/.env.example +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/.github/cliff.toml +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/.github/workflows/release.yml +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/.gitignore +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/.pre-commit-config.yaml +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/CLAUDE.md +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/CLI.md +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/CONTRIBUTING.md +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/LICENSE +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/MANIFEST.in +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/examples/README.md +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/examples/quickstart.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/_api/__init__.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/_api/base.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/_api/dns.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/_api/whoisguard.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/client.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/errors.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap/logging.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_cli/README.md +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_cli/__init__.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_cli/completion.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/README.md +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/__init__.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/__main__.py +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot1.png +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot2.png +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot3.png +0 -0
- {namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot4.png +0 -0
|
@@ -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
|
|
@@ -524,12 +536,12 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
|
524
536
|
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
525
537
|
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
|
|
526
538
|
| `namecheap.whoisguard.*` | ✅ Done | `getList`, `enable`, `disable`, `renew`, `changeEmailAddress` |
|
|
527
|
-
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing
|
|
528
|
-
| `namecheap.users.address.*` | 🚧 Planned |
|
|
529
|
-
| `namecheap.ssl.*` | 🚧 Planned |
|
|
530
|
-
| `namecheap.domains.transfer.*` | 🚧 Planned |
|
|
531
|
-
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records —
|
|
532
|
-
| `namecheap.domains.*` | 🚧 Planned | `reactivate` |
|
|
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 |
|
|
533
545
|
|
|
534
546
|
## 🛠️ Development
|
|
535
547
|
|
|
@@ -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
|
|
@@ -482,12 +494,12 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
|
482
494
|
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
483
495
|
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
|
|
484
496
|
| `namecheap.whoisguard.*` | ✅ Done | `getList`, `enable`, `disable`, `renew`, `changeEmailAddress` |
|
|
485
|
-
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing
|
|
486
|
-
| `namecheap.users.address.*` | 🚧 Planned |
|
|
487
|
-
| `namecheap.ssl.*` | 🚧 Planned |
|
|
488
|
-
| `namecheap.domains.transfer.*` | 🚧 Planned |
|
|
489
|
-
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records —
|
|
490
|
-
| `namecheap.domains.*` | 🚧 Planned | `reactivate` |
|
|
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 |
|
|
491
503
|
|
|
492
504
|
## 🛠️ Development
|
|
493
505
|
|
|
@@ -22,11 +22,12 @@ from .models import (
|
|
|
22
22
|
DomainInfo,
|
|
23
23
|
EmailForward,
|
|
24
24
|
Nameservers,
|
|
25
|
+
ProductPrice,
|
|
25
26
|
Tld,
|
|
26
27
|
WhoisguardEntry,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
|
-
__version__ = "1.
|
|
30
|
+
__version__ = "1.5.0"
|
|
30
31
|
__all__ = [
|
|
31
32
|
"AccountBalance",
|
|
32
33
|
"ConfigurationError",
|
|
@@ -40,6 +41,7 @@ __all__ = [
|
|
|
40
41
|
"Namecheap",
|
|
41
42
|
"NamecheapError",
|
|
42
43
|
"Nameservers",
|
|
44
|
+
"ProductPrice",
|
|
43
45
|
"Tld",
|
|
44
46
|
"ValidationError",
|
|
45
47
|
"WhoisguardEntry",
|
|
@@ -418,153 +418,35 @@ class DomainsAPI(BaseAPI):
|
|
|
418
418
|
def _get_pricing(
|
|
419
419
|
self, domains: builtins.list[str]
|
|
420
420
|
) -> dict[str, dict[str, Decimal | None]]:
|
|
421
|
-
"""
|
|
422
|
-
Get pricing information for domains.
|
|
423
|
-
|
|
424
|
-
Args:
|
|
425
|
-
domains: List of domain names
|
|
421
|
+
"""Get 1-year registration pricing for a list of domains.
|
|
426
422
|
|
|
427
|
-
|
|
428
|
-
|
|
423
|
+
Groups by TLD, fetches pricing via users.get_pricing(), and maps
|
|
424
|
+
back to individual domain names.
|
|
429
425
|
"""
|
|
430
|
-
pricing = {}
|
|
431
|
-
logger.debug(f"Getting pricing for domains: {domains}")
|
|
426
|
+
pricing: dict[str, dict[str, Decimal | None]] = {}
|
|
432
427
|
|
|
433
|
-
# Group domains by TLD
|
|
428
|
+
# Group domains by TLD
|
|
434
429
|
tld_groups: dict[str, builtins.list[str]] = {}
|
|
435
430
|
for domain in domains:
|
|
436
|
-
|
|
437
|
-
tld = ext.suffix
|
|
431
|
+
tld = tldextract.extract(domain).suffix
|
|
438
432
|
if tld not in tld_groups:
|
|
439
433
|
tld_groups[tld] = []
|
|
440
434
|
tld_groups[tld].append(domain)
|
|
441
435
|
|
|
442
|
-
logger.debug(f"TLD groups: {tld_groups}")
|
|
443
|
-
|
|
444
|
-
# Fetch pricing for each TLD group
|
|
445
436
|
for tld, domain_list in tld_groups.items():
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
logger.debug(f"Response type: {type(result)}")
|
|
461
|
-
logger.debug(
|
|
462
|
-
f"Response keys: "
|
|
463
|
-
f"{list(result.keys()) if isinstance(result, dict) else 'Not a dict'}"
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
# Extract pricing info
|
|
467
|
-
if isinstance(result, dict):
|
|
468
|
-
logger.debug(f"Parsing pricing response for {tld}")
|
|
469
|
-
|
|
470
|
-
# Get ProductCategory (could be a list or single dict)
|
|
471
|
-
categories = result.get("ProductCategory", {})
|
|
472
|
-
if not isinstance(categories, list):
|
|
473
|
-
categories = [categories] if categories else []
|
|
474
|
-
|
|
475
|
-
logger.debug(f"Found {len(categories)} categories")
|
|
476
|
-
|
|
477
|
-
# Look for REGISTER category
|
|
478
|
-
for category in categories:
|
|
479
|
-
if not isinstance(category, dict):
|
|
480
|
-
continue
|
|
481
|
-
|
|
482
|
-
# Use normalized name for consistent access
|
|
483
|
-
category_name = category.get("@Name", "")
|
|
484
|
-
category_name_normalized = category.get(
|
|
485
|
-
"@Name_normalized", category_name.lower()
|
|
486
|
-
)
|
|
487
|
-
logger.debug(
|
|
488
|
-
f"Checking category: {category_name} "
|
|
489
|
-
f"(normalized: {category_name_normalized})"
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
if category_name_normalized == "register":
|
|
493
|
-
# Get products in this category
|
|
494
|
-
products = category.get("Product", {})
|
|
495
|
-
if not isinstance(products, list):
|
|
496
|
-
products = [products] if products else []
|
|
497
|
-
|
|
498
|
-
logger.debug(
|
|
499
|
-
f"Found {len(products)} products in REGISTER category"
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
# Find the product matching our TLD
|
|
503
|
-
for product in products:
|
|
504
|
-
if not isinstance(product, dict):
|
|
505
|
-
continue
|
|
506
|
-
|
|
507
|
-
product_name = product.get("@Name", "")
|
|
508
|
-
logger.debug(
|
|
509
|
-
f"Checking product: {product_name} vs {tld}"
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
if product_name.lower() == tld.lower():
|
|
513
|
-
# Get price list
|
|
514
|
-
price_info = product.get("Price", [])
|
|
515
|
-
if not isinstance(price_info, list):
|
|
516
|
-
price_info = [price_info] if price_info else []
|
|
517
|
-
|
|
518
|
-
logger.debug(
|
|
519
|
-
f"Found {len(price_info)} price entries for {tld}"
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
# Find 1 year price
|
|
523
|
-
for price in price_info:
|
|
524
|
-
if not isinstance(price, dict):
|
|
525
|
-
continue
|
|
526
|
-
|
|
527
|
-
duration = price.get("@Duration", "")
|
|
528
|
-
if duration == "1":
|
|
529
|
-
regular_price = price.get("@RegularPrice")
|
|
530
|
-
your_price = price.get("@YourPrice")
|
|
531
|
-
retail_price = price.get("@RetailPrice")
|
|
532
|
-
|
|
533
|
-
# Get additional cost
|
|
534
|
-
# (normalization handles their typo)
|
|
535
|
-
price.get("@YourAdditionalCost", "0")
|
|
536
|
-
|
|
537
|
-
logger.debug(
|
|
538
|
-
f"Found prices for {tld}: "
|
|
539
|
-
f"regular={regular_price}, "
|
|
540
|
-
f"your={your_price}, "
|
|
541
|
-
f"retail={retail_price}"
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
# Apply to all domains with this TLD
|
|
545
|
-
for domain in domain_list:
|
|
546
|
-
pricing[domain] = {
|
|
547
|
-
"regular_price": Decimal(
|
|
548
|
-
regular_price
|
|
549
|
-
)
|
|
550
|
-
if regular_price
|
|
551
|
-
else None,
|
|
552
|
-
"your_price": Decimal(your_price)
|
|
553
|
-
if your_price
|
|
554
|
-
else None,
|
|
555
|
-
"retail_price": Decimal(
|
|
556
|
-
retail_price
|
|
557
|
-
)
|
|
558
|
-
if retail_price
|
|
559
|
-
else None,
|
|
560
|
-
}
|
|
561
|
-
break
|
|
562
|
-
break
|
|
563
|
-
break
|
|
564
|
-
|
|
565
|
-
except Exception as e:
|
|
566
|
-
# If pricing fails, continue without it
|
|
567
|
-
logger.error(f"Failed to get pricing for TLD {tld}: {e}")
|
|
568
|
-
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
|
|
569
451
|
|
|
570
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
|
|
@@ -423,6 +423,34 @@ class WhoisguardEntry(BaseModel):
|
|
|
423
423
|
return int(v) if v else 0
|
|
424
424
|
|
|
425
425
|
|
|
426
|
+
class ProductPrice(BaseModel):
|
|
427
|
+
"""A single price entry for a product at a specific duration."""
|
|
428
|
+
|
|
429
|
+
duration: int = Field(alias="@Duration")
|
|
430
|
+
duration_type: str = Field(alias="@DurationType", default="YEAR")
|
|
431
|
+
price: Decimal = Field(alias="@Price")
|
|
432
|
+
regular_price: Decimal = Field(alias="@RegularPrice")
|
|
433
|
+
your_price: Decimal = Field(alias="@YourPrice")
|
|
434
|
+
coupon_price: Decimal | None = Field(alias="@CouponPrice", default=None)
|
|
435
|
+
currency: str = Field(alias="@Currency", default="USD")
|
|
436
|
+
|
|
437
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
438
|
+
|
|
439
|
+
@field_validator(
|
|
440
|
+
"price", "regular_price", "your_price", "coupon_price", mode="before"
|
|
441
|
+
)
|
|
442
|
+
@classmethod
|
|
443
|
+
def parse_decimal(cls, v: Any) -> Decimal | None:
|
|
444
|
+
if v is None or v == "":
|
|
445
|
+
return None
|
|
446
|
+
return Decimal(str(v))
|
|
447
|
+
|
|
448
|
+
@field_validator("duration", mode="before")
|
|
449
|
+
@classmethod
|
|
450
|
+
def parse_duration(cls, v: Any) -> int:
|
|
451
|
+
return int(v) if v else 1
|
|
452
|
+
|
|
453
|
+
|
|
426
454
|
class Config(BaseModel):
|
|
427
455
|
"""Client configuration with validation."""
|
|
428
456
|
|
|
@@ -1350,6 +1350,87 @@ def account_balance(config: Config) -> None:
|
|
|
1350
1350
|
sys.exit(1)
|
|
1351
1351
|
|
|
1352
1352
|
|
|
1353
|
+
@account_group.command("pricing")
|
|
1354
|
+
@click.argument("tld", required=False)
|
|
1355
|
+
@click.option(
|
|
1356
|
+
"--action",
|
|
1357
|
+
"-a",
|
|
1358
|
+
type=click.Choice(
|
|
1359
|
+
["REGISTER", "RENEW", "TRANSFER", "REACTIVATE"], case_sensitive=False
|
|
1360
|
+
),
|
|
1361
|
+
default="REGISTER",
|
|
1362
|
+
help="Pricing action",
|
|
1363
|
+
)
|
|
1364
|
+
@pass_config
|
|
1365
|
+
def account_pricing(config: Config, tld: str | None, action: str) -> None:
|
|
1366
|
+
"""Get domain pricing. Optionally filter by TLD.
|
|
1367
|
+
|
|
1368
|
+
Examples:
|
|
1369
|
+
namecheap-cli account pricing com
|
|
1370
|
+
namecheap-cli account pricing --action RENEW
|
|
1371
|
+
namecheap-cli account pricing io --action REGISTER
|
|
1372
|
+
"""
|
|
1373
|
+
nc = config.init_client()
|
|
1374
|
+
|
|
1375
|
+
try:
|
|
1376
|
+
with Progress(
|
|
1377
|
+
SpinnerColumn(),
|
|
1378
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1379
|
+
transient=True,
|
|
1380
|
+
) as progress:
|
|
1381
|
+
progress.add_task("Getting pricing...", total=None)
|
|
1382
|
+
pricing = nc.users.get_pricing(
|
|
1383
|
+
"DOMAIN",
|
|
1384
|
+
action=action.upper(),
|
|
1385
|
+
product_name=tld,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
action_products = pricing.get(action.upper(), {})
|
|
1389
|
+
|
|
1390
|
+
if config.output_format == "table":
|
|
1391
|
+
if not action_products:
|
|
1392
|
+
console.print("[yellow]No pricing data found[/yellow]")
|
|
1393
|
+
return
|
|
1394
|
+
|
|
1395
|
+
table = Table(title=f"Domain Pricing — {action.upper()}")
|
|
1396
|
+
table.add_column("TLD", style="cyan")
|
|
1397
|
+
table.add_column("Duration", justify="center")
|
|
1398
|
+
table.add_column("Price", style="green", justify="right")
|
|
1399
|
+
table.add_column("Regular", style="dim", justify="right")
|
|
1400
|
+
table.add_column("Your Price", style="yellow", justify="right")
|
|
1401
|
+
|
|
1402
|
+
for product_name, prices in sorted(action_products.items()):
|
|
1403
|
+
for p in prices:
|
|
1404
|
+
table.add_row(
|
|
1405
|
+
f".{product_name}",
|
|
1406
|
+
f"{p.duration} {'year' if p.duration == 1 else 'years'}",
|
|
1407
|
+
f"${p.price}" if p.price else "-",
|
|
1408
|
+
f"${p.regular_price}" if p.regular_price else "-",
|
|
1409
|
+
f"${p.your_price}" if p.your_price else "-",
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
console.print(table)
|
|
1413
|
+
else:
|
|
1414
|
+
data = []
|
|
1415
|
+
for product_name, prices in action_products.items():
|
|
1416
|
+
for p in prices:
|
|
1417
|
+
data.append(
|
|
1418
|
+
{
|
|
1419
|
+
"tld": product_name,
|
|
1420
|
+
"duration": p.duration,
|
|
1421
|
+
"price": str(p.price),
|
|
1422
|
+
"regular_price": str(p.regular_price),
|
|
1423
|
+
"your_price": str(p.your_price),
|
|
1424
|
+
"currency": p.currency,
|
|
1425
|
+
}
|
|
1426
|
+
)
|
|
1427
|
+
output_formatter(data, config.output_format)
|
|
1428
|
+
|
|
1429
|
+
except NamecheapError as e:
|
|
1430
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1431
|
+
sys.exit(1)
|
|
1432
|
+
|
|
1433
|
+
|
|
1353
1434
|
@cli.group("config")
|
|
1354
1435
|
def config_group() -> None:
|
|
1355
1436
|
"""Configuration management commands."""
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Users API."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from namecheap.models import AccountBalance
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot1.png
RENAMED
|
File without changes
|
{namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot2.png
RENAMED
|
File without changes
|
{namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot3.png
RENAMED
|
File without changes
|
{namecheap_python-1.4.0 → namecheap_python-1.5.0}/src/namecheap_dns_tui/assets/screenshot4.png
RENAMED
|
File without changes
|