namecheap-python 1.4.0__py3-none-any.whl → 1.5.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.
namecheap/__init__.py CHANGED
@@ -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.4.0"
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",
namecheap/_api/domains.py CHANGED
@@ -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
- Returns:
428
- Dict mapping domain to pricing info
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 for efficient API calls
428
+ # Group domains by TLD
434
429
  tld_groups: dict[str, builtins.list[str]] = {}
435
430
  for domain in domains:
436
- ext = tldextract.extract(domain)
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
- try:
447
- logger.debug(f"Fetching pricing for TLD: {tld}")
448
- # Get pricing for this TLD
449
- result: Any = self._request(
450
- "namecheap.users.getPricing",
451
- {
452
- "ProductType": "DOMAIN",
453
- "ActionName": "REGISTER",
454
- "ProductName": tld,
455
- },
456
- path="UserGetPricingResult.ProductType",
457
- )
458
- assert isinstance(result, dict)
459
- logger.debug(f"Pricing API response for {tld}: {result}")
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
namecheap/_api/users.py CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
5
+ from typing import Any, Literal
6
6
 
7
- from namecheap.models import AccountBalance
7
+ from namecheap.models import AccountBalance, ProductPrice
8
8
 
9
9
  from .base import BaseAPI
10
10
 
@@ -30,3 +30,73 @@ class UsersAPI(BaseAPI):
30
30
 
31
31
  assert result, "API returned empty result for getBalances"
32
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
namecheap/models.py CHANGED
@@ -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
 
namecheap_cli/__main__.py CHANGED
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: namecheap-python
3
- Version: 1.4.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
@@ -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` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
528
- | `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
529
- | `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
530
- | `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
531
- | `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
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
 
@@ -1,17 +1,17 @@
1
- namecheap/__init__.py,sha256=d4na6bJ0UVqU2CUBwP5eROrinnD92qpFBNHn8l6IAxo,943
1
+ namecheap/__init__.py,sha256=hXo3ATiYSo40CbXPNlY9Q02wrU4Kfr3FDBdRMs5ZCcc,981
2
2
  namecheap/client.py,sha256=c-JxBygXjD62bcTDzD9jv8hzd9K-C4TzzSoQQxT7H6I,6897
3
3
  namecheap/errors.py,sha256=5bGbV1e4_jkK8YXZXbLF6GJCVUTKw1CtMl9-mz7ogZg,5010
4
4
  namecheap/logging.py,sha256=lMR1fr1dWWz3z2NFEY-vl8b52FmmhH76R2NjyifSdYA,3396
5
- namecheap/models.py,sha256=Rz6wc-6uQJPI-i1eALorRRjiTs9-XRhqGsEiD0jFVfg,17130
5
+ namecheap/models.py,sha256=KMy3tvd3LOeQDhCbipZ7qLZcHJs4aw6b-7Kk-xkie50,18102
6
6
  namecheap/_api/__init__.py,sha256=ymQxKCySphoeoo4s_J0tLziXttLNhOQ8AZbCzFcuAHs,36
7
7
  namecheap/_api/base.py,sha256=FoczO1Q860PaFUFv-S3IoIV2xaGVJAlchkWnmTI6dlw,6121
8
8
  namecheap/_api/dns.py,sha256=Hny5TsVWmmG-3rF6kb8JwboGFt2wKsd4-Z6T8GannBM,17213
9
- namecheap/_api/domains.py,sha256=fo9JBsKtPlqme_nkEw8KseQ93STnH2Nn744669rn7TA,21030
10
- namecheap/_api/users.py,sha256=CCXSZJiPkQiLHYRAlYKTBCDG3-JSPdNkNWWww71JXV0,795
9
+ namecheap/_api/domains.py,sha256=IcPcmiGpJLgqCOjNUCRP3dwt-Kbdc0nAINnEvU72DvU,15121
10
+ namecheap/_api/users.py,sha256=QHTYVQnPvSBgaXUVPR2H-4M20dF45qNYFiS_2z63lxk,3189
11
11
  namecheap/_api/whoisguard.py,sha256=R7jBiWlFqxu0EKwtayJEFHZwqub2ELd6pziaiQvxvd8,6207
12
12
  namecheap_cli/README.md,sha256=liduIiGr8DHXGTht5swrYnvtAlcdCMQOnSdCD61g4Vw,7337
13
13
  namecheap_cli/__init__.py,sha256=nGRHc_CkO4xKhSQdAVG-koEffP8VS0TvbfbZkg7Jg4k,108
14
- namecheap_cli/__main__.py,sha256=V4qNuZfsLYHmjQV9awjG4ZvBusiUf_lkQcFP9I1ulx8,49629
14
+ namecheap_cli/__main__.py,sha256=yWlZRDDk2a79kiyBZ-7Z49zK978afM0SwWlJO_iVRx8,52551
15
15
  namecheap_cli/completion.py,sha256=JTEMnceQli7TombjZkHh-IcZKW4RFRI8Yk5VynxPsEA,2777
16
16
  namecheap_dns_tui/README.md,sha256=It16ZiZh0haEeaENfF5HX0Ec4dBawdTYiAi-TiG9wi0,1690
17
17
  namecheap_dns_tui/__init__.py,sha256=-yL_1Ha41FlQcmjG-raUrZP9CjTJD3d0w2BW2X-twJg,106
@@ -20,8 +20,8 @@ namecheap_dns_tui/assets/screenshot1.png,sha256=OXO2P80ll5WRzLYgaakcNnzos8svlJoX
20
20
  namecheap_dns_tui/assets/screenshot2.png,sha256=5VN_qDMNhWEyrOqKw7vxl1h-TgmZQ_V9aph3Xmf_AFg,279194
21
21
  namecheap_dns_tui/assets/screenshot3.png,sha256=h39wSKxx1JCkgeAB7Q3_JlBcAtX1vsRFKtWtOwbBVso,220625
22
22
  namecheap_dns_tui/assets/screenshot4.png,sha256=J4nCOW16z3vaRiPbcMiiIRgV7q3XFbi_1N1ivD1Pa4Y,238068
23
- namecheap_python-1.4.0.dist-info/METADATA,sha256=mJMPACBtcu8-nOEIArjlR52KYdPub1ZdkOTavx5GvN0,20120
24
- namecheap_python-1.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
- namecheap_python-1.4.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
26
- namecheap_python-1.4.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
27
- namecheap_python-1.4.0.dist-info/RECORD,,
23
+ namecheap_python-1.5.0.dist-info/METADATA,sha256=LNO8PE7Zuivr5XH5_frOmTTdhD0j3gCg4IgTpEdR9Oc,20807
24
+ namecheap_python-1.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
+ namecheap_python-1.5.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
26
+ namecheap_python-1.5.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
27
+ namecheap_python-1.5.0.dist-info/RECORD,,