namecheap-python 1.0.0__py3-none-any.whl → 1.0.2__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/_api/base.py CHANGED
@@ -56,7 +56,9 @@ def normalize_xml_response(data: dict[str, Any]) -> dict[str, Any]:
56
56
  # Fix known typos from Namecheap
57
57
  if key == "@YourAdditonalCost": # Their typo (missing 'i' in Additional)
58
58
  normalized["@YourAdditionalCost"] = value # Correct spelling
59
- logger.debug("Fixed Namecheap typo: @YourAdditonalCost -> @YourAdditionalCost")
59
+ logger.debug(
60
+ "Fixed Namecheap typo: @YourAdditonalCost -> @YourAdditionalCost"
61
+ )
60
62
 
61
63
  # Debug canary: Alert if they have both versions (means they're fixing it)
62
64
  if "@YourAdditionalCost" in data and "@YourAdditonalCost" in data:
@@ -70,7 +72,8 @@ def normalize_xml_response(data: dict[str, Any]) -> dict[str, Any]:
70
72
  normalized[key] = normalize_xml_response(value)
71
73
  elif isinstance(value, list):
72
74
  normalized[key] = [
73
- normalize_xml_response(item) if isinstance(item, dict) else item for item in value
75
+ normalize_xml_response(item) if isinstance(item, dict) else item
76
+ for item in value
74
77
  ]
75
78
 
76
79
  return normalized
namecheap/_api/dns.py CHANGED
@@ -35,7 +35,9 @@ class DNSRecordBuilder:
35
35
  Self for chaining
36
36
  """
37
37
  self._records.append(
38
- DNSRecord.model_validate({"@Name": name, "@Type": "A", "@Address": ip, "@TTL": ttl})
38
+ DNSRecord.model_validate(
39
+ {"@Name": name, "@Type": "A", "@Address": ip, "@TTL": ttl}
40
+ )
39
41
  )
40
42
  return self
41
43
 
@@ -94,7 +96,13 @@ class DNSRecordBuilder:
94
96
  """
95
97
  self._records.append(
96
98
  DNSRecord.model_validate(
97
- {"@Name": name, "@Type": "MX", "@Address": server, "@TTL": ttl, "@MXPref": priority}
99
+ {
100
+ "@Name": name,
101
+ "@Type": "MX",
102
+ "@Address": server,
103
+ "@TTL": ttl,
104
+ "@MXPref": priority,
105
+ }
98
106
  )
99
107
  )
100
108
  return self
namecheap/_api/domains.py CHANGED
@@ -17,7 +17,9 @@ from .base import BaseAPI
17
17
  class DomainsAPI(BaseAPI):
18
18
  """Domain management operations."""
19
19
 
20
- def check(self, *domains: str, include_pricing: bool = False) -> builtins.list[DomainCheck]:
20
+ def check(
21
+ self, *domains: str, include_pricing: bool = False
22
+ ) -> builtins.list[DomainCheck]:
21
23
  """
22
24
  Check domain availability.
23
25
 
@@ -158,7 +160,9 @@ class DomainsAPI(BaseAPI):
158
160
 
159
161
  # Add contact info if provided
160
162
  if contact:
161
- contact_data = contact.model_dump() if isinstance(contact, Contact) else contact
163
+ contact_data = (
164
+ contact.model_dump() if isinstance(contact, Contact) else contact
165
+ )
162
166
  # Add contact fields for all types (Registrant, Tech, Admin, AuxBilling)
163
167
  for contact_type in ["Registrant", "Tech", "Admin", "AuxBilling"]:
164
168
  for field, value in contact_data.items():
@@ -284,7 +288,9 @@ class DomainsAPI(BaseAPI):
284
288
  assert isinstance(result, dict)
285
289
  return bool(result)
286
290
 
287
- def _get_pricing(self, domains: builtins.list[str]) -> dict[str, dict[str, Decimal | None]]:
291
+ def _get_pricing(
292
+ self, domains: builtins.list[str]
293
+ ) -> dict[str, dict[str, Decimal | None]]:
288
294
  """
289
295
  Get pricing information for domains.
290
296
 
@@ -362,7 +368,9 @@ class DomainsAPI(BaseAPI):
362
368
  if not isinstance(products, list):
363
369
  products = [products] if products else []
364
370
 
365
- logger.debug(f"Found {len(products)} products in REGISTER category")
371
+ logger.debug(
372
+ f"Found {len(products)} products in REGISTER category"
373
+ )
366
374
 
367
375
  # Find the product matching our TLD
368
376
  for product in products:
@@ -370,7 +378,9 @@ class DomainsAPI(BaseAPI):
370
378
  continue
371
379
 
372
380
  product_name = product.get("@Name", "")
373
- logger.debug(f"Checking product: {product_name} vs {tld}")
381
+ logger.debug(
382
+ f"Checking product: {product_name} vs {tld}"
383
+ )
374
384
 
375
385
  if product_name.lower() == tld.lower():
376
386
  # Get price list
@@ -378,7 +388,9 @@ class DomainsAPI(BaseAPI):
378
388
  if not isinstance(price_info, list):
379
389
  price_info = [price_info] if price_info else []
380
390
 
381
- logger.debug(f"Found {len(price_info)} price entries for {tld}")
391
+ logger.debug(
392
+ f"Found {len(price_info)} price entries for {tld}"
393
+ )
382
394
 
383
395
  # Find 1 year price
384
396
  for price in price_info:
@@ -405,13 +417,17 @@ class DomainsAPI(BaseAPI):
405
417
  # Apply to all domains with this TLD
406
418
  for domain in domain_list:
407
419
  pricing[domain] = {
408
- "regular_price": Decimal(regular_price)
420
+ "regular_price": Decimal(
421
+ regular_price
422
+ )
409
423
  if regular_price
410
424
  else None,
411
425
  "your_price": Decimal(your_price)
412
426
  if your_price
413
427
  else None,
414
- "retail_price": Decimal(retail_price)
428
+ "retail_price": Decimal(
429
+ retail_price
430
+ )
415
431
  if retail_price
416
432
  else None,
417
433
  }
namecheap/logging.py CHANGED
@@ -74,13 +74,21 @@ class ErrorDisplay:
74
74
 
75
75
  if hasattr(error, "_ip_help") and error._ip_help is not None:
76
76
  console.print("\n[yellow]🔍 IP Configuration Issue[/yellow]")
77
- console.print(f" Your current IP: [cyan]{error._ip_help['actual_ip']}[/cyan]")
78
- console.print(f" Configured IP: [cyan]{error._ip_help['configured_ip']}[/cyan]")
77
+ console.print(
78
+ f" Your current IP: [cyan]{error._ip_help['actual_ip']}[/cyan]"
79
+ )
80
+ console.print(
81
+ f" Configured IP: [cyan]{error._ip_help['configured_ip']}[/cyan]"
82
+ )
79
83
  console.print("\n[yellow]💡 To fix this:[/yellow]")
80
- console.print(" 1. Log in to [link=https://www.namecheap.com]Namecheap[/link]")
84
+ console.print(
85
+ " 1. Log in to [link=https://www.namecheap.com]Namecheap[/link]"
86
+ )
81
87
  console.print(" 2. Go to Profile → Tools → API Access")
82
88
  actual_ip = error._ip_help["actual_ip"]
83
- console.print(f" 3. Add this IP to whitelist: [cyan]{actual_ip}[/cyan]")
89
+ console.print(
90
+ f" 3. Add this IP to whitelist: [cyan]{actual_ip}[/cyan]"
91
+ )
84
92
  console.print(
85
93
  f" 4. Update your .env file: [cyan]NAMECHEAP_CLIENT_IP={actual_ip}[/cyan]"
86
94
  )
namecheap/models.py CHANGED
@@ -89,7 +89,9 @@ class DomainCheck(XMLModel):
89
89
  """
90
90
 
91
91
  domain: str = Field(alias="@Domain", description="Domain name checked")
92
- available: bool = Field(alias="@Available", description="Whether domain is available")
92
+ available: bool = Field(
93
+ alias="@Available", description="Whether domain is available"
94
+ )
93
95
  premium: bool = Field(
94
96
  default=False,
95
97
  alias="@IsPremiumName",
@@ -182,8 +184,8 @@ class DNSRecord(XMLModel):
182
184
  """A DNS record."""
183
185
 
184
186
  name: str = Field(alias="@Name", default="@")
185
- type: Literal["A", "AAAA", "CNAME", "MX", "NS", "TXT", "URL", "URL301", "FRAME"] = Field(
186
- alias="@Type"
187
+ type: Literal["A", "AAAA", "CNAME", "MX", "NS", "TXT", "URL", "URL301", "FRAME"] = (
188
+ Field(alias="@Type")
187
189
  )
188
190
  value: str = Field(alias="@Address")
189
191
  ttl: int = Field(alias="@TTL", default=1800)
namecheap_cli/__main__.py CHANGED
@@ -23,8 +23,26 @@ from .completion import get_completion_script
23
23
 
24
24
  console = Console()
25
25
 
26
+
26
27
  # Configuration
27
- CONFIG_DIR = Path.home() / ".namecheap"
28
+ def get_config_dir() -> Path:
29
+ """Get config directory, using XDG on Unix-like systems."""
30
+ import os
31
+ import sys
32
+
33
+ if sys.platform == "win32":
34
+ # Windows: use platformdirs for proper Windows paths
35
+ from platformdirs import user_config_dir
36
+
37
+ return Path(user_config_dir("namecheap"))
38
+ # Linux/macOS: use XDG
39
+ xdg_config = os.environ.get("XDG_CONFIG_HOME")
40
+ if xdg_config:
41
+ return Path(xdg_config) / "namecheap"
42
+ return Path.home() / ".config" / "namecheap"
43
+
44
+
45
+ CONFIG_DIR = get_config_dir()
28
46
  CONFIG_FILE = CONFIG_DIR / "config.yaml"
29
47
 
30
48
 
@@ -54,9 +72,33 @@ class Config:
54
72
  if self.client:
55
73
  return self.client
56
74
 
75
+ # Check if config file exists
76
+ if not CONFIG_FILE.exists():
77
+ console.print("[red]❌ Configuration not found![/red]")
78
+ console.print(
79
+ "\nPlease run [bold cyan]namecheap-cli config init[/bold cyan] to set up your configuration."
80
+ )
81
+ console.print(
82
+ f"\nThis will create a config file at: [dim]{CONFIG_FILE}[/dim]"
83
+ )
84
+ sys.exit(1)
85
+
57
86
  config = self.load_config()
58
87
  profile_config = config.get("profiles", {}).get(self.profile, {})
59
88
 
89
+ # Check if profile exists
90
+ if not profile_config:
91
+ console.print(
92
+ f"[red]❌ Profile '{self.profile}' not found in configuration![/red]"
93
+ )
94
+ console.print(
95
+ f"\nAvailable profiles: {', '.join(config.get('profiles', {}).keys()) or 'none'}"
96
+ )
97
+ console.print(
98
+ "\nRun [bold cyan]namecheap-cli config init[/bold cyan] to create a new profile."
99
+ )
100
+ sys.exit(1)
101
+
60
102
  # Override sandbox if specified
61
103
  if self.sandbox is not None:
62
104
  profile_config["sandbox"] = self.sandbox
@@ -65,7 +107,21 @@ class Config:
65
107
  self.client = Namecheap(**profile_config)
66
108
  return self.client
67
109
  except Exception as e:
68
- console.print(f"[red]❌ Error initializing client: {e}[/red]")
110
+ # Check for common configuration errors
111
+ error_msg = str(e)
112
+ if (
113
+ "Parameter APIUser is missing" in error_msg
114
+ or "Parameter APIKey is missing" in error_msg
115
+ ):
116
+ console.print("[red]❌ Invalid or incomplete configuration![/red]")
117
+ console.print(
118
+ "\nYour configuration appears to be missing required fields."
119
+ )
120
+ console.print(
121
+ "Please run [bold cyan]namecheap-cli config init[/bold cyan] to reconfigure."
122
+ )
123
+ else:
124
+ console.print(f"[red]❌ Error initializing client: {e}[/red]")
69
125
  sys.exit(1)
70
126
 
71
127
 
@@ -93,7 +149,9 @@ def output_formatter(data: Any, format: str, headers: list[str] | None = None) -
93
149
 
94
150
 
95
151
  @click.group()
96
- @click.option("--config", "config_path", type=click.Path(exists=True), help="Config file path")
152
+ @click.option(
153
+ "--config", "config_path", type=click.Path(exists=True), help="Config file path"
154
+ )
97
155
  @click.option("--profile", default="default", help="Config profile to use")
98
156
  @click.option("--sandbox", is_flag=True, help="Use sandbox API")
99
157
  @click.option(
@@ -108,7 +166,9 @@ def output_formatter(data: Any, format: str, headers: list[str] | None = None) -
108
166
  @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
109
167
  @click.version_option()
110
168
  @pass_config
111
- def cli(config: Config, config_path, profile, sandbox, output, no_color, quiet, verbose) -> None:
169
+ def cli(
170
+ config: Config, config_path, profile, sandbox, output, no_color, quiet, verbose
171
+ ) -> None:
112
172
  """Namecheap CLI - Manage domains and DNS records."""
113
173
  config.output_format = output
114
174
  config.no_color = no_color
@@ -129,10 +189,14 @@ def domain_group() -> None:
129
189
 
130
190
  @domain_group.command("list")
131
191
  @click.option("--status", type=click.Choice(["active", "expired", "locked"]))
132
- @click.option("--sort", type=click.Choice(["name", "expires", "created"]), default="name")
192
+ @click.option(
193
+ "--sort", type=click.Choice(["name", "expires", "created"]), default="name"
194
+ )
133
195
  @click.option("--expiring-in", type=int, help="Show domains expiring within N days")
134
196
  @pass_config
135
- def domain_list(config: Config, status: str | None, sort: str, expiring_in: int | None) -> None:
197
+ def domain_list(
198
+ config: Config, status: str | None, sort: str, expiring_in: int | None
199
+ ) -> None:
136
200
  """List all domains."""
137
201
  nc = config.init_client()
138
202
 
@@ -213,9 +277,8 @@ def domain_list(config: Config, status: str | None, sort: str, expiring_in: int
213
277
  @domain_group.command("check")
214
278
  @click.argument("domains", nargs=-1, required=False)
215
279
  @click.option("--file", "-f", type=click.File("r"), help="File with domains to check")
216
- @click.option("--pricing", "-p", is_flag=True, help="Include pricing information")
217
280
  @pass_config
218
- def domain_check(config: Config, domains: tuple[str, ...], file, pricing: bool) -> None:
281
+ def domain_check(config: Config, domains: tuple[str, ...], file) -> None:
219
282
  """Check domain availability."""
220
283
  nc = config.init_client()
221
284
 
@@ -235,39 +298,30 @@ def domain_check(config: Config, domains: tuple[str, ...], file, pricing: bool)
235
298
  transient=True,
236
299
  ) as progress:
237
300
  progress.add_task(f"Checking {len(domain_list)} domains...", total=None)
238
- results = nc.domains.check(*domain_list, include_pricing=pricing)
301
+ results = nc.domains.check(*domain_list, include_pricing=True)
239
302
 
240
303
  # Output
241
304
  if config.output_format == "table":
242
305
  table = Table(title="Domain Availability")
243
306
  table.add_column("Domain", style="cyan")
244
307
  table.add_column("Available", style="green")
245
- if pricing:
246
- table.add_column("Price", style="yellow")
247
- table.add_column("Total", style="yellow")
308
+ table.add_column("Price (USD/year)", style="yellow")
248
309
 
249
310
  for result in results:
250
311
  available_text = "✅ Available" if result.available else "❌ Taken"
251
312
  available_style = "green" if result.available else "red"
252
313
 
314
+ if result.available and result.price:
315
+ price_text = f"${result.price:.2f}"
316
+ else:
317
+ price_text = "-"
318
+
253
319
  row = [
254
320
  result.domain,
255
321
  f"[{available_style}]{available_text}[/{available_style}]",
322
+ price_text,
256
323
  ]
257
324
 
258
- if pricing and result.available:
259
- if result.price:
260
- price_text = f"${result.price:.2f}"
261
- total_text = (
262
- f"${result.total_price:.2f}" if result.total_price else price_text
263
- )
264
- else:
265
- price_text = "N/A"
266
- total_text = "N/A"
267
- row.extend([price_text, total_text])
268
- elif pricing:
269
- row.extend(["-", "-"])
270
-
271
325
  table.add_row(*row)
272
326
 
273
327
  console.print(table)
@@ -293,16 +347,11 @@ def domain_check(config: Config, domains: tuple[str, ...], file, pricing: bool)
293
347
  "domain": result.domain,
294
348
  "available": result.available,
295
349
  }
296
- if pricing and result.available and result.price:
350
+ if result.available and result.price:
297
351
  item["price"] = float(result.price)
298
- item["total_price"] = (
299
- float(result.total_price) if result.total_price else float(result.price)
300
- )
301
352
  data.append(item)
302
353
 
303
- headers = ["domain", "available"]
304
- if pricing:
305
- headers.extend(["price", "total_price"])
354
+ headers = ["domain", "available", "price"]
306
355
  output_formatter(data, config.output_format, headers)
307
356
 
308
357
  except NamecheapError as e:
@@ -331,12 +380,18 @@ def domain_info(config: Config, domain: str) -> None:
331
380
  f"[bold]Status:[/bold] "
332
381
  f"{'Active' if not domain_obj.is_expired else '[red]Expired[/red]'}"
333
382
  )
334
- console.print(f"[bold]Created:[/bold] {domain_obj.created.strftime('%Y-%m-%d')}")
335
- console.print(f"[bold]Expires:[/bold] {domain_obj.expires.strftime('%Y-%m-%d')}")
383
+ console.print(
384
+ f"[bold]Created:[/bold] {domain_obj.created.strftime('%Y-%m-%d')}"
385
+ )
386
+ console.print(
387
+ f"[bold]Expires:[/bold] {domain_obj.expires.strftime('%Y-%m-%d')}"
388
+ )
336
389
  console.print(
337
390
  f"[bold]Auto-Renew:[/bold] {'✓ Enabled' if domain_obj.auto_renew else '✗ Disabled'}"
338
391
  )
339
- console.print(f"[bold]Locked:[/bold] {'🔒 Yes' if domain_obj.is_locked else '🔓 No'}")
392
+ console.print(
393
+ f"[bold]Locked:[/bold] {'🔒 Yes' if domain_obj.is_locked else '🔓 No'}"
394
+ )
340
395
  console.print(
341
396
  f"[bold]WHOIS Guard:[/bold] "
342
397
  f"{'✓ Enabled' if domain_obj.whois_guard else '✗ Disabled'}"
@@ -345,7 +400,9 @@ def domain_info(config: Config, domain: str) -> None:
345
400
  # Calculate days until expiration
346
401
  days_left = (domain_obj.expires - datetime.now()).days
347
402
  if days_left < 30:
348
- console.print(f"\n⚠️ [yellow]Domain expires in {days_left} days![/yellow]")
403
+ console.print(
404
+ f"\n⚠️ [yellow]Domain expires in {days_left} days![/yellow]"
405
+ )
349
406
  elif days_left < 60:
350
407
  console.print(f"\n📅 Domain expires in {days_left} days")
351
408
 
@@ -478,7 +535,9 @@ def dns_add(
478
535
 
479
536
  try:
480
537
  # Create the new record
481
- new_record = DNSRecord(name=name, type=record_type, value=value, ttl=ttl, priority=priority)
538
+ new_record = DNSRecord(
539
+ name=name, type=record_type, value=value, ttl=ttl, priority=priority
540
+ )
482
541
 
483
542
  # Get existing records
484
543
  if not config.quiet:
@@ -549,7 +608,9 @@ def dns_delete(
549
608
  nc = config.init_client()
550
609
 
551
610
  if all and not (type or name or value) and not yes:
552
- console.print(f"[red]⚠️ Warning: This will delete ALL DNS records for {domain}[/red]")
611
+ console.print(
612
+ f"[red]⚠️ Warning: This will delete ALL DNS records for {domain}[/red]"
613
+ )
553
614
  if not Confirm.ask("Are you sure?", default=False):
554
615
  console.print("[yellow]Cancelled[/yellow]")
555
616
  return
@@ -586,7 +647,9 @@ def dns_delete(
586
647
 
587
648
  # Show what will be deleted
588
649
  if not yes and not config.quiet:
589
- console.print(f"[yellow]Will delete {len(records_to_delete)} record(s):[/yellow]")
650
+ console.print(
651
+ f"[yellow]Will delete {len(records_to_delete)} record(s):[/yellow]"
652
+ )
590
653
  for r in records_to_delete:
591
654
  console.print(f" • {r.type} {r.name} → {r.value}")
592
655
 
@@ -641,7 +704,9 @@ def dns_delete(
641
704
  default="yaml",
642
705
  help="Export format",
643
706
  )
644
- @click.option("--output", "-o", type=click.File("w"), help="Output file (default: stdout)")
707
+ @click.option(
708
+ "--output", "-o", type=click.File("w"), help="Output file (default: stdout)"
709
+ )
645
710
  @pass_config
646
711
  def dns_export(config: Config, domain: str, format: str, output) -> None:
647
712
  """Export DNS records."""
@@ -656,7 +721,9 @@ def dns_export(config: Config, domain: str, format: str, output) -> None:
656
721
 
657
722
  for r in sorted(records, key=lambda x: (x.type, x.name)):
658
723
  if r.type == "MX":
659
- lines.append(f"{r.name}\t{r.ttl}\tIN\t{r.type}\t{r.priority}\t{r.value}")
724
+ lines.append(
725
+ f"{r.name}\t{r.ttl}\tIN\t{r.type}\t{r.priority}\t{r.value}"
726
+ )
660
727
  else:
661
728
  lines.append(f"{r.name}\t{r.ttl}\tIN\t{r.type}\t{r.value}")
662
729
 
@@ -698,7 +765,9 @@ def dns_export(config: Config, domain: str, format: str, output) -> None:
698
765
  if output:
699
766
  output.write(content)
700
767
  if not config.quiet:
701
- console.print(f"[green]✅ Exported {len(records)} records to {output.name}[/green]")
768
+ console.print(
769
+ f"[green]✅ Exported {len(records)} records to {output.name}[/green]"
770
+ )
702
771
  else:
703
772
  click.echo(content)
704
773
 
@@ -721,7 +790,9 @@ def account_balance(config: Config) -> None:
721
790
 
722
791
  try:
723
792
  # This would need to be implemented in the SDK
724
- console.print("[yellow]Account balance check not yet implemented in SDK[/yellow]")
793
+ console.print(
794
+ "[yellow]Account balance check not yet implemented in SDK[/yellow]"
795
+ )
725
796
  console.print("This feature requires the users.getBalances API method")
726
797
 
727
798
  except NamecheapError as e:
@@ -748,6 +819,14 @@ def config_init() -> None:
748
819
 
749
820
  console.print("\n[bold cyan]Namecheap CLI Configuration Wizard[/bold cyan]\n")
750
821
 
822
+ console.print("[dim]To get your API key:[/dim]")
823
+ console.print(
824
+ "1. Go to [link=https://ap.www.namecheap.com/settings/tools/apiaccess/]https://ap.www.namecheap.com/settings/tools/apiaccess/[/link]"
825
+ )
826
+ console.print("2. Enable API access")
827
+ console.print("3. Whitelist your IP address")
828
+ console.print("4. Generate your API key\n")
829
+
751
830
  # Get configuration values
752
831
  api_key = Prompt.ask("API Key", password=True)
753
832
  username = Prompt.ask("Username")
@@ -783,7 +862,9 @@ def config_init() -> None:
783
862
 
784
863
  console.print(f"\n[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
785
864
  console.print("\n💡 Tips:")
786
- console.print(" • You can also use environment variables (NAMECHEAP_API_KEY, etc.)")
865
+ console.print(
866
+ " • You can also use environment variables (NAMECHEAP_API_KEY, etc.)"
867
+ )
787
868
  console.print(" • Add more profiles with: nc config add-profile <name>")
788
869
  console.print(" • Use a profile with: nc --profile <name> <command>")
789
870
 
@@ -810,7 +891,9 @@ def completion(shell: str) -> None:
810
891
  console.print("[dim]nc completion zsh >> ~/.zshrc[/dim]")
811
892
  elif shell == "fish":
812
893
  console.print("[dim]# Add to fish config:[/dim]")
813
- console.print("[dim]nc completion fish > ~/.config/fish/completions/nc.fish[/dim]")
894
+ console.print(
895
+ "[dim]nc completion fish > ~/.config/fish/completions/nc.fish[/dim]"
896
+ )
814
897
 
815
898
 
816
899
  def main() -> None:
@@ -189,7 +189,9 @@ class AddRecordModal(ModalScreen):
189
189
  with ScrollableContainer(id="form-scroll"):
190
190
  with Vertical(classes="field-group"):
191
191
  yield Label("Record Type:")
192
- initial_type = self.editing_record.type if self.editing_record else "A"
192
+ initial_type = (
193
+ self.editing_record.type if self.editing_record else "A"
194
+ )
193
195
  yield Select(
194
196
  [
195
197
  ("A - IPv4 Address", "A"),
@@ -208,7 +210,9 @@ class AddRecordModal(ModalScreen):
208
210
 
209
211
  with Vertical(classes="field-group"):
210
212
  yield Label("Name:")
211
- initial_name = self.editing_record.name if self.editing_record else "@"
213
+ initial_name = (
214
+ self.editing_record.name if self.editing_record else "@"
215
+ )
212
216
  yield Input(
213
217
  placeholder="@ for root or subdomain",
214
218
  id="record-name",
@@ -217,7 +221,9 @@ class AddRecordModal(ModalScreen):
217
221
 
218
222
  with Vertical(classes="field-group"):
219
223
  yield Label("Value:")
220
- initial_value = self.editing_record.value if self.editing_record else ""
224
+ initial_value = (
225
+ self.editing_record.value if self.editing_record else ""
226
+ )
221
227
  yield Input(
222
228
  placeholder="Enter value based on record type",
223
229
  id="record-value",
@@ -226,7 +232,9 @@ class AddRecordModal(ModalScreen):
226
232
 
227
233
  with Vertical(classes="field-group"):
228
234
  yield Label("TTL (seconds):")
229
- initial_ttl = str(self.editing_record.ttl) if self.editing_record else "1800"
235
+ initial_ttl = (
236
+ str(self.editing_record.ttl) if self.editing_record else "1800"
237
+ )
230
238
  yield Input(placeholder="1800", id="record-ttl", value=initial_ttl)
231
239
 
232
240
  with Vertical(classes="field-group", id="priority-group"):
@@ -236,7 +244,9 @@ class AddRecordModal(ModalScreen):
236
244
  if self.editing_record and self.editing_record.priority
237
245
  else ""
238
246
  )
239
- yield Input(placeholder="10", id="record-priority", value=initial_priority)
247
+ yield Input(
248
+ placeholder="10", id="record-priority", value=initial_priority
249
+ )
240
250
 
241
251
  with Horizontal(classes="button-row"):
242
252
  button_text = "Save Changes" if self.editing_record else "Add Record"
@@ -482,7 +492,9 @@ class DNSManagerApp(App):
482
492
  str(record.priority) if record.priority else "-",
483
493
  )
484
494
 
485
- self.update_status(f"Loaded {len(self.records)} records for {self.current_domain}")
495
+ self.update_status(
496
+ f"Loaded {len(self.records)} records for {self.current_domain}"
497
+ )
486
498
 
487
499
  def action_refresh(self) -> None:
488
500
  """Refresh records."""
@@ -623,7 +635,9 @@ class DNSManagerApp(App):
623
635
  )
624
636
  builder._records[
625
637
  -1
626
- ].type = "URL" # Override to use URL instead of URL301
638
+ ].type = (
639
+ "URL" # Override to use URL instead of URL301
640
+ )
627
641
  elif rec.type == "URL301":
628
642
  builder.url(
629
643
  rec.name,
@@ -674,7 +688,9 @@ class DNSManagerApp(App):
674
688
  def handle_confirm(confirmed: bool) -> None:
675
689
  """Handle delete confirmation."""
676
690
  if confirmed:
677
- self.update_status(f"Deleting {record.type} record '{record.name}'...")
691
+ self.update_status(
692
+ f"Deleting {record.type} record '{record.name}'..."
693
+ )
678
694
 
679
695
  def delete() -> None:
680
696
  try:
@@ -711,7 +727,9 @@ class DNSManagerApp(App):
711
727
  )
712
728
  builder._records[
713
729
  -1
714
- ].type = "URL" # Override to use URL instead of URL301
730
+ ].type = (
731
+ "URL" # Override to use URL instead of URL301
732
+ )
715
733
  elif rec.type == "URL301":
716
734
  builder.url(
717
735
  rec.name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: namecheap-python
3
- Version: 1.0.0
3
+ Version: 1.0.2
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
@@ -26,21 +26,17 @@ Requires-Dist: tldextract>=5.0.0
26
26
  Requires-Dist: xmltodict>=0.13.0
27
27
  Provides-Extra: all
28
28
  Requires-Dist: click>=8.1.0; extra == 'all'
29
+ Requires-Dist: platformdirs>=4.0.0; extra == 'all'
29
30
  Requires-Dist: pyyaml>=6.0.0; extra == 'all'
30
31
  Requires-Dist: rich>=13.0.0; extra == 'all'
31
32
  Requires-Dist: textual>=0.47.0; extra == 'all'
32
33
  Provides-Extra: cli
33
34
  Requires-Dist: click>=8.1.0; extra == 'cli'
35
+ Requires-Dist: platformdirs>=4.0.0; extra == 'cli'
34
36
  Requires-Dist: pyyaml>=6.0.0; extra == 'cli'
35
37
  Requires-Dist: rich>=13.0.0; extra == 'cli'
36
38
  Provides-Extra: dev
37
- Requires-Dist: mypy>=1.11.0; extra == 'dev'
38
- Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
39
- Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
40
- Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
41
- Requires-Dist: pytest>=8.0.0; extra == 'dev'
42
39
  Requires-Dist: ruff>=0.7.0; extra == 'dev'
43
- Requires-Dist: types-xmltodict>=0.13.0; extra == 'dev'
44
40
  Provides-Extra: tui
45
41
  Requires-Dist: textual>=0.47.0; extra == 'tui'
46
42
  Description-Content-Type: text/markdown
@@ -60,26 +56,17 @@ A modern, friendly Python SDK for the Namecheap API with comprehensive CLI and T
60
56
  - **Comprehensive logging** with beautiful colored output
61
57
  - **Sandbox support** for safe testing
62
58
 
63
- ## 📦 Installation
64
-
65
- ```bash
66
- # Core SDK only
67
- pip install namecheap-python
59
+ ## 🎯 Quick Start
68
60
 
69
- # With CLI tool
70
- pip install namecheap-python[cli]
61
+ **Requires Python 3.12 or higher**
71
62
 
72
- # With TUI tool
73
- pip install namecheap-python[tui]
63
+ ### `namecheap-python`: Core Python SDK Library
74
64
 
75
- # Everything
76
- pip install namecheap-python[all]
65
+ ```bash
66
+ # Add as a dependency to your project
67
+ uv add namecheap-python
77
68
  ```
78
69
 
79
- ## 🎯 Quick Start
80
-
81
- ### SDK Usage
82
-
83
70
  ```python
84
71
  from namecheap import Namecheap
85
72
 
@@ -107,18 +94,22 @@ nc.dns.set("example.com",
107
94
  )
108
95
  ```
109
96
 
110
- ### CLI Usage
97
+ ### `namecheap-cli`: CLI tool
111
98
 
112
- ```bash
113
- # Configure CLI
114
- uv run namecheap-cli config init
99
+ It was meant as a proof of concept to showcase `namecheap-python`, but it is a tool that I use
115
100
 
101
+ ```bash
116
102
  # List domains with beautiful table output
117
- uv run namecheap-cli domain list
118
- ```
119
103
 
120
- Output:
121
- ```
104
+ # Run it without install with:
105
+ uvx --from 'namecheap-python[cli]' namecheap-cli domain list
106
+
107
+ # Or install it permanently with:
108
+ uv tool install --python 3.12 'namecheap-python[cli]'
109
+
110
+ # Then run
111
+ namecheap-cli domain list
112
+
122
113
  Domains (4 total)
123
114
  ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┓
124
115
  ┃ Domain ┃ Status ┃ Expires ┃ Auto-Renew ┃ Locked ┃
@@ -130,22 +121,42 @@ Output:
130
121
  └───────────────────┴────────┴────────────┴────────────┴────────┘
131
122
  ```
132
123
 
124
+ Configure it before first use:
125
+
126
+ ```bash
127
+ # Interactive setup
128
+ namecheap-cli config init
129
+
130
+ # Creates config file at:
131
+ # - Linux/macOS: $XDG_CONFIG_HOME/namecheap/config.yaml (or ~/.config/namecheap/config.yaml)
132
+ # - Windows: %APPDATA%\namecheap\config.yaml
133
+ ```
134
+ Check domain availability and pricing:
135
+
133
136
  ```bash
134
137
  # Check domain availability
135
- uv run namecheap-cli domain check myawesome.com coolstartup.io
138
+ namecheap-cli domain check myawesome.com coolstartup.io
139
+ Domain Availability
140
+ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
141
+ ┃ Domain ┃ Available ┃ Price (USD/year) ┃
142
+ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
143
+ │ myawesome.com │ ❌ Taken │ - │
144
+ │ coolstartup.io │ ✅ Available │ $34.98 │
145
+ └────────────────┴──────────────┴──────────────────┘
146
+
147
+ 💡 Suggestions for taken domains:
148
+ • myawesome.com → myawesome.net, myawesome.io, getmyawesome.com
149
+ ```
150
+
151
+ Manage DNS records:
136
152
 
137
- # Manage DNS records
138
- uv run namecheap-cli dns list example.com
139
- uv run namecheap-cli dns add example.com A www 192.0.2.1
140
- uv run namecheap-cli dns export example.com --format yaml
153
+ In this example I'll set up GitHub Pages for my domain `tdo.garden`
141
154
 
142
- # Setup GitHub Pages (example: tdo.garden)
155
+ ```bash
143
156
  # First, check current DNS records (before setup)
144
- uv run namecheap-cli dns list tdo.garden
145
- ```
157
+ namecheap-cli dns list tdo.garden
146
158
 
147
- Initial state (Namecheap default parking page):
148
- ```
159
+ # Initial state (Namecheap default parking page):
149
160
  DNS Records for tdo.garden (2 total)
150
161
  ┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
151
162
  ┃ Type ┃ Name ┃ Value ┃ TTL ┃ Priority ┃
@@ -153,40 +164,34 @@ Initial state (Namecheap default parking page):
153
164
  │ CNAME │ www │ parkingpage.namecheap.com. │ 1800 │ 10 │
154
165
  │ URL │ @ │ http://www.tdo.garden/ │ 1800 │ 10 │
155
166
  └──────────┴──────────────────────┴────────────────────────────┴──────────┴──────────┘
156
- ```
157
167
 
158
- ```bash
159
168
  # Add GitHub Pages A records for apex domain
160
- uv run namecheap-cli dns add tdo.garden A @ 185.199.108.153
161
- Built namecheap-python @ file:///Users/adrian/Developer/namecheap-python
162
- Uninstalled 1 package in 0.77ms
163
- Installed 1 package in 1ms
169
+ ❯ namecheap-cli dns add tdo.garden A @ 185.199.108.153
164
170
  Adding A record to tdo.garden...
165
171
  ✅ Added A record successfully!
166
172
 
167
- uv run namecheap-cli dns add tdo.garden A @ 185.199.109.153
173
+ ❯ namecheap-cli dns add tdo.garden A @ 185.199.109.153
168
174
  Adding A record to tdo.garden...
169
175
  ✅ Added A record successfully!
170
176
 
171
- uv run namecheap-cli dns add tdo.garden A @ 185.199.110.153
177
+ ❯ namecheap-cli dns add tdo.garden A @ 185.199.110.153
172
178
  Adding A record to tdo.garden...
173
179
  ✅ Added A record successfully!
174
180
 
175
- uv run namecheap-cli dns add tdo.garden A @ 185.199.111.153
181
+ ❯ namecheap-cli dns add tdo.garden A @ 185.199.111.153
176
182
  Adding A record to tdo.garden...
177
183
  ✅ Added A record successfully!
178
184
 
179
185
  # Add CNAME for www subdomain
180
- uv run namecheap-cli dns add tdo.garden CNAME www adriangalilea.github.io
186
+ ❯ namecheap-cli dns add tdo.garden CNAME www adriangalilea.github.io
181
187
  Adding CNAME record to tdo.garden...
182
188
  ✅ Added CNAME record successfully!
183
189
 
184
190
  # Verify the setup
185
- uv run namecheap-cli dns list tdo.garden
186
- ```
191
+ ❯ namecheap-cli dns list tdo.garden
187
192
 
188
- Final state (with GitHub Pages + old records still present):
189
- ```
193
+ # Final state with GitHub Pages + old records still present that you may want to remove:
194
+ ```bash
190
195
  DNS Records for tdo.garden (7 total)
191
196
  ┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
192
197
  ┃ Type ┃ Name ┃ Value ┃ TTL ┃ Priority ┃
@@ -201,18 +206,27 @@ Final state (with GitHub Pages + old records still present):
201
206
  └──────────┴──────────────────────┴────────────────────────────┴──────────┴──────────┘
202
207
  ```
203
208
 
204
- Note: You may want to remove the old parking page records after confirming GitHub Pages works.
205
- ```
206
209
 
207
- ### TUI Usage
210
+ You can also export DNS records:
211
+
212
+ ```bash
213
+ namecheap-cli dns export example.com --format yaml
214
+ ```
215
+ ### `namecheap-dns-tui`: TUI for DNS management
208
216
 
209
217
  ```bash
210
218
  # Launch interactive DNS manager
211
- uv run namecheap-dns-tui
219
+ namecheap-dns-tui
212
220
  ```
213
221
 
214
222
  ![DNS Manager TUI](src/namecheap_dns_tui/assets/screenshot2.png)
215
223
 
224
+ ## Install both the CLI and TUI
225
+
226
+ ```bash
227
+ uv tool install --python 3.12 'namecheap-python[all]'
228
+ ```
229
+
216
230
  ## 📖 Documentation
217
231
 
218
232
  - **[Examples Overview](examples/README.md)** - Quick examples for all tools
@@ -251,15 +265,6 @@ nc = Namecheap(
251
265
  )
252
266
  ```
253
267
 
254
- ### CLI Configuration
255
-
256
- ```bash
257
- # Interactive setup
258
- uv run namecheap-cli config init
259
-
260
- # Creates ~/.namecheap/config.yaml with profiles
261
- ```
262
-
263
268
  ## 🔧 Advanced SDK Usage
264
269
 
265
270
  ### DNS Builder Pattern
@@ -376,4 +381,4 @@ MIT License - see [LICENSE](LICENSE) file for details.
376
381
 
377
382
  ## 🤝 Contributing
378
383
 
379
- Contributions are welcome! Please feel free to submit a Pull Request. See the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
384
+ Contributions are welcome! Please feel free to submit a Pull Request. See the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
@@ -1,25 +1,25 @@
1
1
  namecheap/__init__.py,sha256=4UD1BF-qTulQicElN3CmB5hUeKnlLPvtpbJD08YZp94,666
2
2
  namecheap/client.py,sha256=wnhCA5zeZMGypD1uNHbMYCGoP9paaZjtYZucBUsEY6k,6441
3
3
  namecheap/errors.py,sha256=5bGbV1e4_jkK8YXZXbLF6GJCVUTKw1CtMl9-mz7ogZg,5010
4
- namecheap/logging.py,sha256=craqZaxecjoxFL6SD6iyvj09VXx8QeivHHYWTeYhhbQ,3244
5
- namecheap/models.py,sha256=rOtPFeEwGaGwA7H0CCbjGSrzrF-wuiaqShV7f-LDgCY,11894
4
+ namecheap/logging.py,sha256=lMR1fr1dWWz3z2NFEY-vl8b52FmmhH76R2NjyifSdYA,3396
5
+ namecheap/models.py,sha256=IUNi103jcQ7T_WgfcqXuXsZokkjUDRNYkPwtr7KJ_qU,11910
6
6
  namecheap/_api/__init__.py,sha256=ymQxKCySphoeoo4s_J0tLziXttLNhOQ8AZbCzFcuAHs,36
7
- namecheap/_api/base.py,sha256=vC0d8csUGMIUEZ1SWLLqJpT3xlQ-2FfJMKs_aFbu4pY,6075
8
- namecheap/_api/dns.py,sha256=Yvx91QIWrDx3ZxxmH_NLo_M9MP55aL71CJ6mXyJZUT0,10813
9
- namecheap/_api/domains.py,sha256=EUXV7R5wQyPHQHfDITe2IWRlPF7V-df-K0q2XnrvshI,16113
7
+ namecheap/_api/base.py,sha256=FoczO1Q860PaFUFv-S3IoIV2xaGVJAlchkWnmTI6dlw,6121
8
+ namecheap/_api/dns.py,sha256=Eibn3NzR1o3p_ukxUFDeaKTrJYgBnIomMBKsp881ZrE,10962
9
+ namecheap/_api/domains.py,sha256=h6cWc03iCwPqOJTfbR8pyom_8MUVPoqSIRJRws1i3UY,16603
10
10
  namecheap_cli/README.md,sha256=liduIiGr8DHXGTht5swrYnvtAlcdCMQOnSdCD61g4Vw,7337
11
11
  namecheap_cli/__init__.py,sha256=nGRHc_CkO4xKhSQdAVG-koEffP8VS0TvbfbZkg7Jg4k,108
12
- namecheap_cli/__main__.py,sha256=OyE_oKtBXxWzxF-yGJQy9IbQL5FJ-5rP5ElrKS4Yu8E,28733
12
+ namecheap_cli/__main__.py,sha256=08iIreRm_30z-mmcvqd-qFEaoGWgOJJsCT7UPk7EZ-E,30768
13
13
  namecheap_cli/completion.py,sha256=JTEMnceQli7TombjZkHh-IcZKW4RFRI8Yk5VynxPsEA,2777
14
14
  namecheap_dns_tui/README.md,sha256=It16ZiZh0haEeaENfF5HX0Ec4dBawdTYiAi-TiG9wi0,1690
15
15
  namecheap_dns_tui/__init__.py,sha256=-yL_1Ha41FlQcmjG-raUrZP9CjTJD3d0w2BW2X-twJg,106
16
- namecheap_dns_tui/__main__.py,sha256=xGn07jj7krv_y-WaXWXUafK2R2P1oeFEwDw1L8NLiC4,29568
16
+ namecheap_dns_tui/__main__.py,sha256=DSSNcLAjJvQYlrVtdfped9wwM28kdisbFEZ6oPfD8jA,30034
17
17
  namecheap_dns_tui/assets/screenshot1.png,sha256=OXO2P80ll5WRzLYgaakcNnzos8svlJoX8Ai9eZM3HjE,194120
18
18
  namecheap_dns_tui/assets/screenshot2.png,sha256=5VN_qDMNhWEyrOqKw7vxl1h-TgmZQ_V9aph3Xmf_AFg,279194
19
19
  namecheap_dns_tui/assets/screenshot3.png,sha256=h39wSKxx1JCkgeAB7Q3_JlBcAtX1vsRFKtWtOwbBVso,220625
20
20
  namecheap_dns_tui/assets/screenshot4.png,sha256=J4nCOW16z3vaRiPbcMiiIRgV7q3XFbi_1N1ivD1Pa4Y,238068
21
- namecheap_python-1.0.0.dist-info/METADATA,sha256=wq_qAcQp7Tv5YuqypDFVpomVr1djHhfCc_FRBXR3RpM,13101
22
- namecheap_python-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- namecheap_python-1.0.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
24
- namecheap_python-1.0.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
25
- namecheap_python-1.0.0.dist-info/RECORD,,
21
+ namecheap_python-1.0.2.dist-info/METADATA,sha256=h9P8gDpU73MAIShvep6h74ideSuHTdvodKjdJvNmvjQ,13831
22
+ namecheap_python-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ namecheap_python-1.0.2.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
24
+ namecheap_python-1.0.2.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
25
+ namecheap_python-1.0.2.dist-info/RECORD,,