namecheap-python 1.3.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,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
  ]
namecheap/_api/domains.py CHANGED
@@ -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
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
@@ -0,0 +1,195 @@
1
+ """Domain privacy (WhoisGuard) API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from decimal import Decimal
6
+ from typing import Any, Literal
7
+
8
+ from namecheap.models import WhoisguardEntry
9
+
10
+ from .base import BaseAPI
11
+
12
+
13
+ class WhoisguardAPI(BaseAPI):
14
+ """Domain privacy (WhoisGuard) management.
15
+
16
+ The Namecheap API uses WhoisGuard IDs internally, but this class
17
+ provides domain-name-based convenience methods that resolve the ID
18
+ automatically via get_list().
19
+ """
20
+
21
+ def get_list(
22
+ self,
23
+ *,
24
+ list_type: Literal["ALL", "ALLOTED", "FREE", "DISCARD"] = "ALL",
25
+ page: int = 1,
26
+ page_size: int = 100,
27
+ ) -> list[WhoisguardEntry]:
28
+ """
29
+ Get all WhoisGuard subscriptions.
30
+
31
+ Args:
32
+ list_type: Filter type (ALL, ALLOTED, FREE, DISCARD)
33
+ page: Page number
34
+ page_size: Items per page (2-100)
35
+
36
+ Returns:
37
+ List of WhoisguardEntry subscriptions
38
+
39
+ Examples:
40
+ >>> entries = nc.whoisguard.get_list()
41
+ >>> for e in entries:
42
+ ... print(f"{e.domain} (ID={e.id}) status={e.status}")
43
+ """
44
+ result: Any = self._request(
45
+ "namecheap.whoisguard.getList",
46
+ {
47
+ "ListType": list_type,
48
+ "Page": page,
49
+ "PageSize": min(page_size, 100),
50
+ },
51
+ path="WhoisguardGetListResult",
52
+ )
53
+
54
+ if not result:
55
+ return []
56
+
57
+ entries = result.get("Whoisguard", [])
58
+ if isinstance(entries, dict):
59
+ entries = [entries]
60
+ assert isinstance(entries, list), f"Unexpected Whoisguard type: {type(entries)}"
61
+
62
+ return [WhoisguardEntry.model_validate(e) for e in entries]
63
+
64
+ def _resolve_id(self, domain: str) -> int:
65
+ """Resolve a domain name to its WhoisGuard ID."""
66
+ entries = self.get_list(list_type="ALLOTED")
67
+ for entry in entries:
68
+ if entry.domain.lower() == domain.lower():
69
+ return entry.id
70
+ raise ValueError(
71
+ f"No WhoisGuard subscription found for {domain}. "
72
+ f"Domain must have WhoisGuard allotted to enable/disable it."
73
+ )
74
+
75
+ def enable(self, domain: str, forwarded_to_email: str) -> bool:
76
+ """
77
+ Enable domain privacy for a domain.
78
+
79
+ Args:
80
+ domain: Domain name (resolved to WhoisGuard ID automatically)
81
+ forwarded_to_email: Email where privacy-masked emails get forwarded
82
+
83
+ Returns:
84
+ True if successful
85
+
86
+ Examples:
87
+ >>> nc.whoisguard.enable("example.com", "me@gmail.com")
88
+ """
89
+ wg_id = self._resolve_id(domain)
90
+
91
+ result: Any = self._request(
92
+ "namecheap.whoisguard.enable",
93
+ {
94
+ "WhoisguardID": wg_id,
95
+ "ForwardedToEmail": forwarded_to_email,
96
+ },
97
+ path="WhoisguardEnableResult",
98
+ )
99
+
100
+ assert result, f"API returned empty result for whoisguard.enable on {domain}"
101
+ return result.get("@IsSuccess", "false").lower() == "true"
102
+
103
+ def disable(self, domain: str) -> bool:
104
+ """
105
+ Disable domain privacy for a domain.
106
+
107
+ Args:
108
+ domain: Domain name (resolved to WhoisGuard ID automatically)
109
+
110
+ Returns:
111
+ True if successful
112
+
113
+ Examples:
114
+ >>> nc.whoisguard.disable("example.com")
115
+ """
116
+ wg_id = self._resolve_id(domain)
117
+
118
+ result: Any = self._request(
119
+ "namecheap.whoisguard.disable",
120
+ {"WhoisguardID": wg_id},
121
+ path="WhoisguardDisableResult",
122
+ )
123
+
124
+ assert result, f"API returned empty result for whoisguard.disable on {domain}"
125
+ return result.get("@IsSuccess", "false").lower() == "true"
126
+
127
+ def renew(self, domain: str, *, years: int = 1) -> dict[str, Any]:
128
+ """
129
+ Renew domain privacy for a domain.
130
+
131
+ Args:
132
+ domain: Domain name (resolved to WhoisGuard ID automatically)
133
+ years: Number of years to renew (1-9)
134
+
135
+ Returns:
136
+ Dict with OrderId, TransactionId, ChargedAmount
137
+
138
+ Examples:
139
+ >>> result = nc.whoisguard.renew("example.com", years=1)
140
+ >>> print(f"Charged: {result['charged_amount']}")
141
+ """
142
+ assert 1 <= years <= 9, "Years must be between 1 and 9"
143
+ wg_id = self._resolve_id(domain)
144
+
145
+ result: Any = self._request(
146
+ "namecheap.whoisguard.renew",
147
+ {
148
+ "WhoisguardID": wg_id,
149
+ "Years": years,
150
+ },
151
+ path="WhoisguardRenewResult",
152
+ )
153
+
154
+ assert result, f"API returned empty result for whoisguard.renew on {domain}"
155
+ return {
156
+ "whoisguard_id": int(result.get("@WhoisguardId", wg_id)),
157
+ "years": int(result.get("@Years", years)),
158
+ "is_renewed": result.get("@Renew", "false").lower() == "true",
159
+ "order_id": int(result.get("@OrderId", 0)),
160
+ "transaction_id": int(result.get("@TransactionId", 0)),
161
+ "charged_amount": Decimal(result.get("@ChargedAmount", "0")),
162
+ }
163
+
164
+ def change_email(self, domain: str) -> dict[str, str]:
165
+ """
166
+ Rotate the privacy forwarding email address for a domain.
167
+
168
+ Namecheap generates a new masked email and retires the old one.
169
+ No input email needed — the API handles the rotation.
170
+
171
+ Args:
172
+ domain: Domain name (resolved to WhoisGuard ID automatically)
173
+
174
+ Returns:
175
+ Dict with new_email and old_email
176
+
177
+ Examples:
178
+ >>> result = nc.whoisguard.change_email("example.com")
179
+ >>> print(f"New: {result['new_email']}")
180
+ """
181
+ wg_id = self._resolve_id(domain)
182
+
183
+ result: Any = self._request(
184
+ "namecheap.whoisguard.changeEmailAddress",
185
+ {"WhoisguardID": wg_id},
186
+ path="WhoisguardChangeEmailAddressResult",
187
+ )
188
+
189
+ assert result, (
190
+ f"API returned empty result for whoisguard.changeEmailAddress on {domain}"
191
+ )
192
+ return {
193
+ "new_email": result.get("@WGEmail", ""),
194
+ "old_email": result.get("@WGOldEmail", ""),
195
+ }
namecheap/client.py CHANGED
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
17
17
  from ._api.dns import DnsAPI
18
18
  from ._api.domains import DomainsAPI
19
19
  from ._api.users import UsersAPI
20
+ from ._api.whoisguard import WhoisguardAPI
20
21
 
21
22
 
22
23
  class Namecheap:
@@ -111,6 +112,13 @@ class Namecheap:
111
112
 
112
113
  return DnsAPI(self)
113
114
 
115
+ @cached_property
116
+ def whoisguard(self) -> WhoisguardAPI:
117
+ """Domain privacy (WhoisGuard) management."""
118
+ from ._api.whoisguard import WhoisguardAPI
119
+
120
+ return WhoisguardAPI(self)
121
+
114
122
  @cached_property
115
123
  def users(self) -> UsersAPI:
116
124
  """User account operations."""
namecheap/models.py CHANGED
@@ -347,6 +347,110 @@ class DomainContacts(BaseModel):
347
347
  model_config = ConfigDict(populate_by_name=True)
348
348
 
349
349
 
350
+ class Tld(BaseModel):
351
+ """TLD information from Namecheap's supported TLD list."""
352
+
353
+ name: str = Field(alias="@Name")
354
+ description: str = Field(alias="#text", default="")
355
+ type: str = Field(alias="@Type")
356
+ min_register_years: int = Field(alias="@MinRegisterYears")
357
+ max_register_years: int = Field(alias="@MaxRegisterYears")
358
+ min_renew_years: int = Field(alias="@MinRenewYears")
359
+ max_renew_years: int = Field(alias="@MaxRenewYears")
360
+ min_transfer_years: int = Field(alias="@MinTransferYears")
361
+ max_transfer_years: int = Field(alias="@MaxTransferYears")
362
+ is_api_registerable: bool = Field(alias="@IsApiRegisterable")
363
+ is_api_renewable: bool = Field(alias="@IsApiRenewable")
364
+ is_api_transferable: bool = Field(alias="@IsApiTransferable")
365
+ is_epp_required: bool = Field(alias="@IsEppRequired")
366
+ is_disable_mod_contact: bool = Field(alias="@IsDisableModContact")
367
+ is_disable_wg_allot: bool = Field(alias="@IsDisableWGAllot")
368
+ is_supports_idn: bool = Field(alias="@IsSupportsIDN")
369
+ category: str = Field(alias="@Category", default="")
370
+ sequence_number: int = Field(alias="@SequenceNumber", default=0)
371
+ non_real_time: bool = Field(alias="@NonRealTime", default=False)
372
+
373
+ model_config = ConfigDict(populate_by_name=True)
374
+
375
+ @field_validator(
376
+ "is_api_registerable",
377
+ "is_api_renewable",
378
+ "is_api_transferable",
379
+ "is_epp_required",
380
+ "is_disable_mod_contact",
381
+ "is_disable_wg_allot",
382
+ "is_supports_idn",
383
+ "non_real_time",
384
+ mode="before",
385
+ )
386
+ @classmethod
387
+ def parse_bool(cls, v: Any) -> bool:
388
+ if isinstance(v, bool):
389
+ return v
390
+ if isinstance(v, str):
391
+ return v.lower() == "true"
392
+ return False
393
+
394
+ @field_validator(
395
+ "min_register_years",
396
+ "max_register_years",
397
+ "min_renew_years",
398
+ "max_renew_years",
399
+ "min_transfer_years",
400
+ "max_transfer_years",
401
+ "sequence_number",
402
+ mode="before",
403
+ )
404
+ @classmethod
405
+ def parse_int(cls, v: Any) -> int:
406
+ return int(v) if v else 0
407
+
408
+
409
+ class WhoisguardEntry(BaseModel):
410
+ """A WhoisGuard/domain privacy subscription."""
411
+
412
+ id: int = Field(alias="@ID")
413
+ domain: str = Field(alias="@DomainName", default="")
414
+ created: str = Field(alias="@Created", default="")
415
+ expires: str = Field(alias="@Expires", default="")
416
+ status: str = Field(alias="@Status", default="")
417
+
418
+ model_config = ConfigDict(populate_by_name=True)
419
+
420
+ @field_validator("id", mode="before")
421
+ @classmethod
422
+ def parse_id(cls, v: Any) -> int:
423
+ return int(v) if v else 0
424
+
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
+
350
454
  class Config(BaseModel):
351
455
  """Client configuration with validation."""
352
456
 
namecheap_cli/__main__.py CHANGED
@@ -1016,6 +1016,293 @@ def domain_contacts(config: Config, domain: str) -> None:
1016
1016
  sys.exit(1)
1017
1017
 
1018
1018
 
1019
+ @domain_group.command("tlds")
1020
+ @click.option("--registerable", is_flag=True, help="Only show API-registerable TLDs")
1021
+ @click.option(
1022
+ "--type",
1023
+ "-t",
1024
+ "tld_type",
1025
+ type=click.Choice(["GTLD", "CCTLD"], case_sensitive=False),
1026
+ help="Filter by TLD type",
1027
+ )
1028
+ @pass_config
1029
+ def domain_tlds(config: Config, registerable: bool, tld_type: str | None) -> None:
1030
+ """List all supported TLDs."""
1031
+ nc = config.init_client()
1032
+
1033
+ try:
1034
+ with Progress(
1035
+ SpinnerColumn(),
1036
+ TextColumn("[progress.description]{task.description}"),
1037
+ transient=True,
1038
+ ) as progress:
1039
+ progress.add_task("Loading TLD list...", total=None)
1040
+ tlds = nc.domains.get_tld_list()
1041
+
1042
+ if registerable:
1043
+ tlds = [t for t in tlds if t.is_api_registerable]
1044
+ if tld_type:
1045
+ tlds = [t for t in tlds if t.type.upper() == tld_type.upper()]
1046
+
1047
+ tlds.sort(key=lambda t: t.name)
1048
+
1049
+ if config.output_format == "table":
1050
+ table = Table(title=f"Supported TLDs ({len(tlds)} total)")
1051
+ table.add_column("TLD", style="cyan")
1052
+ table.add_column("Type", style="magenta")
1053
+ table.add_column("Description", style="dim")
1054
+ table.add_column("Register", justify="center")
1055
+ table.add_column("Renew", justify="center")
1056
+ table.add_column("Transfer", justify="center")
1057
+ table.add_column("Years", justify="center")
1058
+
1059
+ for t in tlds:
1060
+ table.add_row(
1061
+ f".{t.name}",
1062
+ t.type,
1063
+ t.description[:40] if t.description else "",
1064
+ "✓" if t.is_api_registerable else "✗",
1065
+ "✓" if t.is_api_renewable else "✗",
1066
+ "✓" if t.is_api_transferable else "✗",
1067
+ f"{t.min_register_years}-{t.max_register_years}",
1068
+ )
1069
+
1070
+ console.print(table)
1071
+ else:
1072
+ data = [
1073
+ {
1074
+ "tld": t.name,
1075
+ "type": t.type,
1076
+ "description": t.description,
1077
+ "api_registerable": t.is_api_registerable,
1078
+ "api_renewable": t.is_api_renewable,
1079
+ "api_transferable": t.is_api_transferable,
1080
+ }
1081
+ for t in tlds
1082
+ ]
1083
+ output_formatter(data, config.output_format)
1084
+
1085
+ except NamecheapError as e:
1086
+ console.print(f"[red]❌ Error: {e}[/red]")
1087
+ sys.exit(1)
1088
+
1089
+
1090
+ @cli.group("privacy")
1091
+ def privacy_group() -> None:
1092
+ """Domain privacy (WhoisGuard) management."""
1093
+ pass
1094
+
1095
+
1096
+ @privacy_group.command("list")
1097
+ @click.option(
1098
+ "--type",
1099
+ "-t",
1100
+ "list_type",
1101
+ type=click.Choice(["ALL", "ALLOTED", "FREE", "DISCARD"], case_sensitive=False),
1102
+ default="ALL",
1103
+ help="Filter by subscription type",
1104
+ )
1105
+ @pass_config
1106
+ def privacy_list(config: Config, list_type: str) -> None:
1107
+ """List WhoisGuard subscriptions."""
1108
+ nc = config.init_client()
1109
+
1110
+ try:
1111
+ with Progress(
1112
+ SpinnerColumn(),
1113
+ TextColumn("[progress.description]{task.description}"),
1114
+ transient=True,
1115
+ ) as progress:
1116
+ progress.add_task("Loading WhoisGuard subscriptions...", total=None)
1117
+ entries = nc.whoisguard.get_list(list_type=list_type.upper())
1118
+
1119
+ if config.output_format == "table":
1120
+ table = Table(title=f"WhoisGuard Subscriptions ({len(entries)} total)")
1121
+ table.add_column("ID", style="dim")
1122
+ table.add_column("Domain", style="cyan")
1123
+ table.add_column("Status", style="green")
1124
+ table.add_column("Created", style="yellow")
1125
+ table.add_column("Expires", style="yellow")
1126
+
1127
+ for e in entries:
1128
+ status_style = (
1129
+ "green" if e.status.lower() in ("enabled", "alloted") else "yellow"
1130
+ )
1131
+ table.add_row(
1132
+ str(e.id),
1133
+ e.domain or "[dim]unassigned[/dim]",
1134
+ f"[{status_style}]{e.status}[/{status_style}]",
1135
+ e.created,
1136
+ e.expires,
1137
+ )
1138
+
1139
+ console.print(table)
1140
+ else:
1141
+ output_formatter([e.model_dump() for e in entries], config.output_format)
1142
+
1143
+ except NamecheapError as e:
1144
+ console.print(f"[red]❌ Error: {e}[/red]")
1145
+ sys.exit(1)
1146
+
1147
+
1148
+ @privacy_group.command("enable")
1149
+ @click.argument("domain")
1150
+ @click.argument("email")
1151
+ @pass_config
1152
+ def privacy_enable(config: Config, domain: str, email: str) -> None:
1153
+ """Enable domain privacy. Requires forwarding email.
1154
+
1155
+ Example:
1156
+ namecheap-cli privacy enable example.com me@gmail.com
1157
+ """
1158
+ nc = config.init_client()
1159
+
1160
+ try:
1161
+ with Progress(
1162
+ SpinnerColumn(),
1163
+ TextColumn("[progress.description]{task.description}"),
1164
+ transient=True,
1165
+ ) as progress:
1166
+ progress.add_task(f"Enabling privacy for {domain}...", total=None)
1167
+ success = nc.whoisguard.enable(domain, email)
1168
+
1169
+ if success:
1170
+ console.print(f"[green]✅ Privacy enabled for {domain}[/green]")
1171
+ console.print(f"[dim]Forwarding email: {email}[/dim]")
1172
+ else:
1173
+ console.print("[red]❌ Failed to enable privacy[/red]")
1174
+ sys.exit(1)
1175
+
1176
+ except ValueError as e:
1177
+ console.print(f"[red]❌ {e}[/red]")
1178
+ sys.exit(1)
1179
+ except NamecheapError as e:
1180
+ console.print(f"[red]❌ Error: {e}[/red]")
1181
+ sys.exit(1)
1182
+
1183
+
1184
+ @privacy_group.command("disable")
1185
+ @click.argument("domain")
1186
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
1187
+ @pass_config
1188
+ def privacy_disable(config: Config, domain: str, yes: bool) -> None:
1189
+ """Disable domain privacy.
1190
+
1191
+ Example:
1192
+ namecheap-cli privacy disable example.com
1193
+ """
1194
+ nc = config.init_client()
1195
+
1196
+ try:
1197
+ if not yes and not config.quiet:
1198
+ console.print(
1199
+ f"\n[yellow]This will expose your WHOIS information for {domain}.[/yellow]"
1200
+ )
1201
+ if not Confirm.ask("Continue?", default=False):
1202
+ console.print("[yellow]Cancelled[/yellow]")
1203
+ return
1204
+
1205
+ with Progress(
1206
+ SpinnerColumn(),
1207
+ TextColumn("[progress.description]{task.description}"),
1208
+ transient=True,
1209
+ ) as progress:
1210
+ progress.add_task(f"Disabling privacy for {domain}...", total=None)
1211
+ success = nc.whoisguard.disable(domain)
1212
+
1213
+ if success:
1214
+ console.print(f"[green]✅ Privacy disabled for {domain}[/green]")
1215
+ else:
1216
+ console.print("[red]❌ Failed to disable privacy[/red]")
1217
+ sys.exit(1)
1218
+
1219
+ except ValueError as e:
1220
+ console.print(f"[red]❌ {e}[/red]")
1221
+ sys.exit(1)
1222
+ except NamecheapError as e:
1223
+ console.print(f"[red]❌ Error: {e}[/red]")
1224
+ sys.exit(1)
1225
+
1226
+
1227
+ @privacy_group.command("renew")
1228
+ @click.argument("domain")
1229
+ @click.option("--years", "-y", type=int, default=1, help="Years to renew (1-9)")
1230
+ @click.option("--yes", is_flag=True, help="Skip confirmation")
1231
+ @pass_config
1232
+ def privacy_renew(config: Config, domain: str, years: int, yes: bool) -> None:
1233
+ """Renew domain privacy subscription.
1234
+
1235
+ Example:
1236
+ namecheap-cli privacy renew example.com --years 2
1237
+ """
1238
+ nc = config.init_client()
1239
+
1240
+ try:
1241
+ if not yes and not config.quiet:
1242
+ console.print(
1243
+ f"\n[yellow]Renewing WhoisGuard for {domain} ({years} year{'s' if years > 1 else ''}).[/yellow]"
1244
+ )
1245
+ if not Confirm.ask("Continue?", default=True):
1246
+ console.print("[yellow]Cancelled[/yellow]")
1247
+ return
1248
+
1249
+ with Progress(
1250
+ SpinnerColumn(),
1251
+ TextColumn("[progress.description]{task.description}"),
1252
+ transient=True,
1253
+ ) as progress:
1254
+ progress.add_task(f"Renewing privacy for {domain}...", total=None)
1255
+ result = nc.whoisguard.renew(domain, years=years)
1256
+
1257
+ if result["is_renewed"]:
1258
+ console.print(f"[green]✅ Privacy renewed for {domain}[/green]")
1259
+ console.print(f"[dim]Charged: {result['charged_amount']}[/dim]")
1260
+ else:
1261
+ console.print("[red]❌ Failed to renew privacy[/red]")
1262
+ sys.exit(1)
1263
+
1264
+ except ValueError as e:
1265
+ console.print(f"[red]❌ {e}[/red]")
1266
+ sys.exit(1)
1267
+ except NamecheapError as e:
1268
+ console.print(f"[red]❌ Error: {e}[/red]")
1269
+ sys.exit(1)
1270
+
1271
+
1272
+ @privacy_group.command("change-email")
1273
+ @click.argument("domain")
1274
+ @pass_config
1275
+ def privacy_change_email(config: Config, domain: str) -> None:
1276
+ """Rotate the privacy forwarding email address.
1277
+
1278
+ Namecheap generates a new masked email automatically.
1279
+
1280
+ Example:
1281
+ namecheap-cli privacy change-email example.com
1282
+ """
1283
+ nc = config.init_client()
1284
+
1285
+ try:
1286
+ with Progress(
1287
+ SpinnerColumn(),
1288
+ TextColumn("[progress.description]{task.description}"),
1289
+ transient=True,
1290
+ ) as progress:
1291
+ progress.add_task(f"Rotating privacy email for {domain}...", total=None)
1292
+ result = nc.whoisguard.change_email(domain)
1293
+
1294
+ console.print(f"[green]✅ Privacy email rotated for {domain}[/green]")
1295
+ console.print(f" New: {result['new_email']}")
1296
+ console.print(f" Old: {result['old_email']}")
1297
+
1298
+ except ValueError as e:
1299
+ console.print(f"[red]❌ {e}[/red]")
1300
+ sys.exit(1)
1301
+ except NamecheapError as e:
1302
+ console.print(f"[red]❌ Error: {e}[/red]")
1303
+ sys.exit(1)
1304
+
1305
+
1019
1306
  @cli.group("account")
1020
1307
  def account_group() -> None:
1021
1308
  """Account management commands."""
@@ -1063,6 +1350,87 @@ def account_balance(config: Config) -> None:
1063
1350
  sys.exit(1)
1064
1351
 
1065
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
+
1066
1434
  @cli.group("config")
1067
1435
  def config_group() -> None:
1068
1436
  """Configuration management commands."""
@@ -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
 
@@ -1,16 +1,17 @@
1
- namecheap/__init__.py,sha256=sUX2-G_o6k1POjfJ9oV32Hi08zaPNL4WwfgrLr_2Qak,879
2
- namecheap/client.py,sha256=KesIaZVa9HpXvlp-hc3nr2x-sqzMPiHEa8NibiPq580,6644
1
+ namecheap/__init__.py,sha256=hXo3ATiYSo40CbXPNlY9Q02wrU4Kfr3FDBdRMs5ZCcc,981
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=0Fhni7yKd84bBopPURIvhwmOCYMzw8tlZwHWzRRZphY,14494
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=txikTYACM82-3IXg2Gqagh9fRZ8aIZiu05unNpy2Zqs,20039
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
+ namecheap/_api/whoisguard.py,sha256=R7jBiWlFqxu0EKwtayJEFHZwqub2ELd6pziaiQvxvd8,6207
11
12
  namecheap_cli/README.md,sha256=liduIiGr8DHXGTht5swrYnvtAlcdCMQOnSdCD61g4Vw,7337
12
13
  namecheap_cli/__init__.py,sha256=nGRHc_CkO4xKhSQdAVG-koEffP8VS0TvbfbZkg7Jg4k,108
13
- namecheap_cli/__main__.py,sha256=0vHk5aAhEzPKQ-UQOQdYqsolCF13DG3k9_z3pUbZXwk,40020
14
+ namecheap_cli/__main__.py,sha256=yWlZRDDk2a79kiyBZ-7Z49zK978afM0SwWlJO_iVRx8,52551
14
15
  namecheap_cli/completion.py,sha256=JTEMnceQli7TombjZkHh-IcZKW4RFRI8Yk5VynxPsEA,2777
15
16
  namecheap_dns_tui/README.md,sha256=It16ZiZh0haEeaENfF5HX0Ec4dBawdTYiAi-TiG9wi0,1690
16
17
  namecheap_dns_tui/__init__.py,sha256=-yL_1Ha41FlQcmjG-raUrZP9CjTJD3d0w2BW2X-twJg,106
@@ -19,8 +20,8 @@ namecheap_dns_tui/assets/screenshot1.png,sha256=OXO2P80ll5WRzLYgaakcNnzos8svlJoX
19
20
  namecheap_dns_tui/assets/screenshot2.png,sha256=5VN_qDMNhWEyrOqKw7vxl1h-TgmZQ_V9aph3Xmf_AFg,279194
20
21
  namecheap_dns_tui/assets/screenshot3.png,sha256=h39wSKxx1JCkgeAB7Q3_JlBcAtX1vsRFKtWtOwbBVso,220625
21
22
  namecheap_dns_tui/assets/screenshot4.png,sha256=J4nCOW16z3vaRiPbcMiiIRgV7q3XFbi_1N1ivD1Pa4Y,238068
22
- namecheap_python-1.3.0.dist-info/METADATA,sha256=phjrSgjok2JzRDjOQsEit1eb9Z7WF7Da5beLiKSEbGA,18802
23
- namecheap_python-1.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- namecheap_python-1.3.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
25
- namecheap_python-1.3.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
26
- namecheap_python-1.3.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,,