summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudflare_api.py143
-rw-r--r--namecheap_api.py165
-rw-r--r--pyproject.toml4
-rw-r--r--regflow/__init__.py3
-rw-r--r--regflow/config.py (renamed from config.py)7
-rwxr-xr-xregflow/domain_manager.py (renamed from domain_manager.py)251
-rw-r--r--regflow/providers/__init__.py1
-rw-r--r--regflow/providers/cloudflare_api.py165
-rw-r--r--regflow/providers/namecheap_api.py163
-rw-r--r--regflow/tests/__init__.py1
-rw-r--r--regflow/tests/test_integration.py (renamed from tests/test_integration.py)6
-rw-r--r--tests/__init__.py1
12 files changed, 485 insertions, 425 deletions
diff --git a/cloudflare_api.py b/cloudflare_api.py
deleted file mode 100644
index 16fd926..0000000
--- a/cloudflare_api.py
+++ /dev/null
@@ -1,143 +0,0 @@
-import requests
-from typing import Dict, Any, List, Optional
-from config import Config
-
-class CloudflareAPI:
- def __init__(self, config: Config):
- self.config = config
- self.base_url = "https://api.cloudflare.com/client/v4"
- self.headers = {
- 'Authorization': f'Bearer {config.cloudflare_api_token}',
- 'Content-Type': 'application/json'
- }
-
- def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
- """Make a request to Cloudflare API"""
- url = f"{self.base_url}{endpoint}"
-
- response = requests.request(method, url, headers=self.headers, json=data, timeout=30)
-
- try:
- response.raise_for_status()
- except requests.exceptions.HTTPError as e:
- # Try to get the error details from the response
- try:
- error_details = response.json()
- errors = error_details.get('errors', [])
- if errors:
- error_msg = ', '.join([error.get('message', 'Unknown error') for error in errors])
- raise Exception(f"Cloudflare API Error: {error_msg}")
- else:
- raise Exception(f"Cloudflare API Error: {response.status_code} - {response.text}")
- except ValueError:
- raise Exception(f"Cloudflare API Error: {response.status_code} - {response.text}")
-
- result = response.json()
-
- if not result.get('success', False):
- errors = result.get('errors', [])
- if errors:
- error_msg = ', '.join([error.get('message', 'Unknown error') for error in errors])
- raise Exception(f"Cloudflare API Error: {error_msg}")
- else:
- raise Exception(f"Cloudflare API Error: Request failed but no error details provided")
-
- return result
-
- def add_zone(self, domain: str) -> Dict[str, Any]:
- """Add a new zone (domain) to Cloudflare"""
- data = {
- 'name': domain,
- 'type': 'full'
- }
-
- result = self._make_request('POST', '/zones', data)
- return result['result']
-
- def get_zone_info(self, domain: str) -> Optional[Dict[str, Any]]:
- """Get zone information for a domain"""
- result = self._make_request('GET', f'/zones?name={domain}')
-
- zones = result.get('result', [])
- if zones:
- return zones[0]
-
- return None
-
- def get_zone_nameservers(self, zone_id: str) -> List[str]:
- """Get nameservers for a zone"""
- result = self._make_request('GET', f'/zones/{zone_id}')
-
- zone = result.get('result', {})
- return zone.get('name_servers', [])
-
- def create_dns_record(self, zone_id: str, record_type: str, name: str,
- content: str, ttl: int = 300, proxied: bool = False) -> Dict[str, Any]:
- """Create a DNS record"""
- data = {
- 'type': record_type,
- 'name': name,
- 'content': content,
- 'ttl': ttl
- }
-
- if record_type in ['A', 'AAAA', 'CNAME']:
- data['proxied'] = proxied
-
- result = self._make_request('POST', f'/zones/{zone_id}/dns_records', data)
- return result['result']
-
- def create_worker_subdomain(self, zone_id: str, subdomain: str) -> Dict[str, Any]:
- """Create a worker route for a subdomain"""
- # Create A record pointing to dummy IP (will be overridden by worker)
- return self.create_dns_record(
- zone_id=zone_id,
- record_type='A',
- name=subdomain,
- content='192.0.2.1', # Dummy IP
- proxied=True
- )
-
- def setup_google_analytics_dns(self, zone_id: str, domain: str) -> List[Dict[str, Any]]:
- """Set up DNS records for Google Analytics"""
- records = []
-
- # Google Analytics doesn't typically require specific DNS records
- # But we can add common verification records if needed
- # This is a placeholder for future GA4 requirements
-
- return records
-
- def setup_basic_dns_records(self, zone_id: str, domain: str) -> List[Dict[str, Any]]:
- """Set up basic DNS records for a domain"""
- records = []
-
- # Root domain A record (placeholder)
- records.append(self.create_dns_record(
- zone_id=zone_id,
- record_type='A',
- name=domain,
- content='192.0.2.1',
- proxied=True
- ))
-
- # WWW CNAME record
- records.append(self.create_dns_record(
- zone_id=zone_id,
- record_type='CNAME',
- name=f'www.{domain}',
- content=domain,
- proxied=True
- ))
-
- return records
-
- def list_zones(self) -> List[Dict[str, Any]]:
- """List all zones in the account"""
- result = self._make_request('GET', '/zones')
- return result.get('result', [])
-
- def get_zone_dns_records(self, zone_id: str) -> List[Dict[str, Any]]:
- """Get all DNS records for a zone"""
- result = self._make_request('GET', f'/zones/{zone_id}/dns_records')
- return result.get('result', []) \ No newline at end of file
diff --git a/namecheap_api.py b/namecheap_api.py
deleted file mode 100644
index c85da02..0000000
--- a/namecheap_api.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import requests
-import xml.etree.ElementTree as ET
-from typing import Dict, Any, Optional
-from config import Config
-
-class NamecheapAPI:
- def __init__(self, config: Config):
- self.config = config
- self.base_url = "https://api.namecheap.com/xml.response"
-
- def _make_request(self, command: str, params: Dict[str, Any]) -> ET.Element:
- """Make a request to Namecheap API"""
- default_params = {
- 'ApiUser': self.config.namecheap_api_user,
- 'ApiKey': self.config.namecheap_api_key,
- 'UserName': self.config.namecheap_username,
- 'ClientIp': self.config.namecheap_client_ip,
- 'Command': command
- }
-
- all_params = {**default_params, **params}
-
- try:
- response = requests.get(self.base_url, params=all_params, timeout=60)
- response.raise_for_status()
- except requests.exceptions.Timeout:
- raise Exception(f"API request timed out for command: {command}")
- except requests.exceptions.RequestException as e:
- raise Exception(f"API request failed for command {command}: {str(e)}")
-
- root = ET.fromstring(response.text)
-
- # Check for API errors
- if root.get('Status') == 'ERROR':
- ns = {'ns': 'http://api.namecheap.com/xml.response'}
- errors = root.find('.//ns:Errors', ns)
- if errors is not None:
- error_elem = errors.find('.//ns:Error', ns)
- if error_elem is not None:
- error_msg = error_elem.text
- raise Exception(f"Namecheap API Error: {error_msg}")
- raise Exception("Namecheap API returned an error")
-
- return root
-
- def check_domain_availability(self, domain: str) -> bool:
- """Check if domain is available for registration"""
- params = {
- 'DomainList': domain
- }
-
- root = self._make_request('namecheap.domains.check', params)
-
- # Parse the response
- ns = {'ns': 'http://api.namecheap.com/xml.response'}
- domain_check = root.find('.//ns:DomainCheckResult', ns)
-
- if domain_check is None:
- raise Exception(f"Could not find domain check result for {domain}")
-
- return domain_check.get('Available') == 'true'
-
- def get_domain_pricing(self, domain: str) -> Dict[str, float]:
- """Get pricing information for a domain"""
- tld = domain.split('.')[-1].upper()
-
- params = {
- 'ProductType': 'DOMAIN',
- 'ProductCategory': 'DOMAINS',
- 'ActionName': 'REGISTER',
- 'ProductName': tld
- }
-
- root = self._make_request('namecheap.users.getPricing', params)
-
- # Find pricing for the TLD
- ns = {'ns': 'http://api.namecheap.com/xml.response'}
-
- # Look for the specific TLD in the response
- for product in root.findall('.//ns:Product', ns):
- product_name = product.get('Name')
- if product_name and product_name.lower() == tld.lower():
- for price in product.findall('.//ns:Price', ns):
- duration = price.get('Duration')
- if duration == '1':
- price_val = float(price.get('Price', 0))
-
- # Get renewal price (might be in a separate call or same structure)
- renew_val = price_val # Use same price for renewal if not specified
-
- return {
- 'register': price_val,
- 'renew': renew_val
- }
-
- raise Exception(f"No pricing information found for .{tld} domains")
-
- def get_account_balance(self) -> float:
- """Get current account balance"""
- root = self._make_request('namecheap.users.getBalances', {})
-
- ns = {'ns': 'http://api.namecheap.com/xml.response'}
- balance_result = root.find('.//ns:UserGetBalancesResult', ns)
-
- if balance_result is not None:
- available_balance = balance_result.get('AvailableBalance')
- if available_balance:
- return float(available_balance)
-
- raise Exception("Could not retrieve account balance")
-
- def register_domain(self, domain: str, years: int = 1,
- registrant_info: Optional[Dict[str, str]] = None) -> bool:
- """Register a domain"""
- if not registrant_info:
- registrant_info = {
- 'FirstName': 'John',
- 'LastName': 'Doe',
- 'Address1': '123 Main St',
- 'City': 'Anytown',
- 'StateProvince': 'NY',
- 'PostalCode': '12345',
- 'Country': 'US',
- 'Phone': '+1.5551234567',
- 'EmailAddress': 'john.doe@example.com'
- }
-
- params = {
- 'DomainName': domain,
- 'Years': str(years),
- **{f'Registrant{key}': value for key, value in registrant_info.items()},
- **{f'Tech{key}': value for key, value in registrant_info.items()},
- **{f'Admin{key}': value for key, value in registrant_info.items()},
- **{f'AuxBilling{key}': value for key, value in registrant_info.items()}
- }
-
- root = self._make_request('namecheap.domains.create', params)
-
- # Check if registration was successful
- ns = {'ns': 'http://api.namecheap.com/xml.response'}
- domain_create = root.find('.//ns:DomainCreateResult', ns)
- if domain_create is None:
- raise Exception(f"Could not find domain creation result for {domain}")
-
- return domain_create.get('Registered') == 'true'
-
- def set_dns_servers(self, domain: str, nameservers: list) -> bool:
- """Set DNS servers for a domain"""
- sld, tld = domain.split('.', 1)
-
- params = {
- 'SLD': sld,
- 'TLD': tld,
- 'Nameservers': ','.join(nameservers)
- }
-
- root = self._make_request('namecheap.domains.dns.setCustom', params)
-
- # Check if DNS update was successful
- ns = {'ns': 'http://api.namecheap.com/xml.response'}
- dns_result = root.find('.//ns:DomainDNSSetCustomResult', ns)
- if dns_result is None:
- raise Exception(f"Could not find DNS update result for {domain}")
-
- return dns_result.get('Updated') == 'true' \ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 5c6435d..ec332f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ dev = [
]
[project.scripts]
-regflow = "domain_manager:main"
+regflow = "regflow.domain_manager:main"
[tool.hatch.build.targets.wheel]
-packages = ["."] \ No newline at end of file
+packages = ["regflow"] \ No newline at end of file
diff --git a/regflow/__init__.py b/regflow/__init__.py
new file mode 100644
index 0000000..1d12396
--- /dev/null
+++ b/regflow/__init__.py
@@ -0,0 +1,3 @@
+"""RegFlow - Domain registration and DNS management automation tool."""
+
+__version__ = "0.1.0" \ No newline at end of file
diff --git a/config.py b/regflow/config.py
index 488c52d..164d20d 100644
--- a/config.py
+++ b/regflow/config.py
@@ -4,13 +4,14 @@ from pydantic import BaseModel
load_dotenv()
+
class Config(BaseModel):
namecheap_api_user: str
namecheap_api_key: str
namecheap_username: str
namecheap_client_ip: str
cloudflare_api_token: str
-
+
@classmethod
def from_env(cls):
return cls(
@@ -18,5 +19,5 @@ class Config(BaseModel):
namecheap_api_key=os.getenv("NAMECHEAP_API_KEY", ""),
namecheap_username=os.getenv("NAMECHEAP_USERNAME", ""),
namecheap_client_ip=os.getenv("NAMECHEAP_CLIENT_IP", ""),
- cloudflare_api_token=os.getenv("CLOUDFLARE_API_TOKEN", "")
- ) \ No newline at end of file
+ cloudflare_api_token=os.getenv("CLOUDFLARE_API_TOKEN", ""),
+ )
diff --git a/domain_manager.py b/regflow/domain_manager.py
index 707ce4f..2f1d15a 100755
--- a/domain_manager.py
+++ b/regflow/domain_manager.py
@@ -2,21 +2,25 @@
import sys
from typing import Dict, Any, Optional
-from config import Config
-from namecheap_api import NamecheapAPI
-from cloudflare_api import CloudflareAPI
+from .config import Config
+from .providers.namecheap_api import NamecheapAPI
+from .providers.cloudflare_api import CloudflareAPI
+
class DomainManager:
def __init__(self, config: Config):
self.config = config
self.namecheap = NamecheapAPI(config)
self.cloudflare = CloudflareAPI(config)
-
- def register_and_setup_domain(self, domain: str,
- registrant_info: Optional[Dict[str, str]] = None,
- setup_workers: bool = True,
- dry_run: bool = False,
- skip_registration: bool = False) -> Dict[str, Any]:
+
+ def register_and_setup_domain(
+ self,
+ domain: str,
+ registrant_info: Optional[Dict[str, str]] = None,
+ setup_workers: bool = True,
+ dry_run: bool = False,
+ skip_registration: bool = False,
+ ) -> Dict[str, Any]:
"""
Complete domain registration and setup workflow:
1. Check domain availability
@@ -26,216 +30,247 @@ class DomainManager:
5. Update nameservers in Namecheap
6. Set up basic DNS records
"""
- result = {
- 'domain': domain,
- 'steps_completed': [],
- 'errors': []
- }
-
+ result = {"domain": domain, "steps_completed": [], "errors": []}
+
try:
if not skip_registration:
# Step 1: Check domain availability
print(f"Checking availability for {domain}...")
if not self.namecheap.check_domain_availability(domain):
- result['errors'].append(f"Domain {domain} is not available")
+ result["errors"].append(f"Domain {domain} is not available")
return result
-
- result['steps_completed'].append('availability_check')
+
+ result["steps_completed"].append("availability_check")
print(f"✓ Domain {domain} is available")
-
+
# Step 2: Check pricing and balance
print("Checking pricing and account balance...")
-
+
try:
pricing = self.namecheap.get_domain_pricing(domain)
except Exception as e:
- result['errors'].append(f"Failed to get domain pricing: {str(e)}")
+ result["errors"].append(f"Failed to get domain pricing: {str(e)}")
return result
-
+
try:
balance = self.namecheap.get_account_balance()
except Exception as e:
- result['errors'].append(f"Failed to get account balance: {str(e)}")
+ result["errors"].append(f"Failed to get account balance: {str(e)}")
return result
-
- if balance < pricing['register']:
- result['errors'].append(
+
+ if balance < pricing["register"]:
+ result["errors"].append(
f"Insufficient balance. Required: ${pricing['register']:.2f}, "
f"Available: ${balance:.2f}"
)
return result
-
- result['steps_completed'].append('pricing_check')
+
+ result["steps_completed"].append("pricing_check")
print(f"✓ Pricing: ${pricing['register']:.2f}, Balance: ${balance:.2f}")
-
+
# Step 3: User confirmation with multiple safeguards
- print(f"\n" + "="*50)
+ print(f"\n" + "=" * 50)
print(f"DOMAIN REGISTRATION CONFIRMATION")
- print(f"="*50)
+ print(f"=" * 50)
print(f"Domain: {domain}")
print(f"Registration Price: ${pricing['register']:.2f}")
print(f"Account Balance: ${balance:.2f}")
print(f"Remaining Balance: ${balance - pricing['register']:.2f}")
- print(f"="*50)
-
+ print(f"=" * 50)
+
# Multiple confirmation steps to prevent accidents
- print(f"\nWARNING: This will charge ${pricing['register']:.2f} to your account!")
- first_confirm = input(f"Type 'REGISTER' to proceed with registration of {domain}: ").strip()
- if first_confirm != 'REGISTER':
- result['errors'].append("Registration cancelled by user")
+ print(
+ f"\nWARNING: This will charge ${pricing['register']:.2f} to your account!"
+ )
+ first_confirm = input(
+ f"Type 'REGISTER' to proceed with registration of {domain}: "
+ ).strip()
+ if first_confirm != "REGISTER":
+ result["errors"].append("Registration cancelled by user")
return result
-
- second_confirm = input(f"Are you absolutely sure you want to register {domain} for ${pricing['register']:.2f}? (yes/no): ").lower().strip()
- if second_confirm != 'yes':
- result['errors'].append("Registration cancelled by user")
+
+ second_confirm = (
+ input(
+ f"Are you absolutely sure you want to register {domain} for ${pricing['register']:.2f}? (yes/no): "
+ )
+ .lower()
+ .strip()
+ )
+ if second_confirm != "yes":
+ result["errors"].append("Registration cancelled by user")
return result
-
+
# Step 4: Register domain
if dry_run:
- print(f"DRY RUN: Would register domain {domain} (skipping actual registration)")
- result['steps_completed'].append('domain_registration_dry_run')
+ print(
+ f"DRY RUN: Would register domain {domain} (skipping actual registration)"
+ )
+ result["steps_completed"].append("domain_registration_dry_run")
else:
print(f"Registering domain {domain}...")
- if not self.namecheap.register_domain(domain, registrant_info=registrant_info):
- result['errors'].append(f"Failed to register domain {domain}")
+ if not self.namecheap.register_domain(
+ domain, registrant_info=registrant_info
+ ):
+ result["errors"].append(f"Failed to register domain {domain}")
return result
- result['steps_completed'].append('domain_registration')
-
+ result["steps_completed"].append("domain_registration")
+
print(f"✓ Domain {domain} registered successfully")
else:
- print(f"✓ Skipping domain registration for {domain} (assuming already registered)")
- result['steps_completed'].append('domain_registration_skipped')
-
+ print(
+ f"✓ Skipping domain registration for {domain} (assuming already registered)"
+ )
+ result["steps_completed"].append("domain_registration_skipped")
+
# Step 5: Add domain to Cloudflare
print(f"Adding {domain} to Cloudflare...")
try:
zone_info = self.cloudflare.add_zone(domain)
- zone_id = zone_info['id']
-
- result['steps_completed'].append('cloudflare_zone_creation')
- result['zone_id'] = zone_id
+ zone_id = zone_info["id"]
+
+ result["steps_completed"].append("cloudflare_zone_creation")
+ result["zone_id"] = zone_id
print(f"✓ Domain added to Cloudflare (Zone ID: {zone_id})")
except Exception as e:
- result['errors'].append(f"Failed to add domain to Cloudflare: {str(e)}")
+ result["errors"].append(f"Failed to add domain to Cloudflare: {str(e)}")
return result
-
+
# Step 6: Get Cloudflare nameservers
print("Getting Cloudflare nameservers...")
try:
nameservers = self.cloudflare.get_zone_nameservers(zone_id)
-
- result['nameservers'] = nameservers
+
+ result["nameservers"] = nameservers
print(f"✓ Cloudflare nameservers: {', '.join(nameservers)}")
except Exception as e:
- result['errors'].append(f"Failed to get Cloudflare nameservers: {str(e)}")
+ result["errors"].append(
+ f"Failed to get Cloudflare nameservers: {str(e)}"
+ )
return result
-
+
# Step 7: Update nameservers in Namecheap
if not dry_run:
print(f"Updating nameservers in Namecheap...")
try:
if not self.namecheap.set_dns_servers(domain, nameservers):
- result['errors'].append("Failed to update nameservers in Namecheap")
+ result["errors"].append(
+ "Failed to update nameservers in Namecheap"
+ )
return result
-
- result['steps_completed'].append('nameserver_update')
+
+ result["steps_completed"].append("nameserver_update")
print(f"✓ Nameservers updated in Namecheap")
except Exception as e:
- result['errors'].append(f"Failed to update nameservers in Namecheap: {str(e)}")
+ result["errors"].append(
+ f"Failed to update nameservers in Namecheap: {str(e)}"
+ )
return result
else:
- print(f"DRY RUN: Would update nameservers in Namecheap to: {', '.join(nameservers)}")
- result['steps_completed'].append('nameserver_update_dry_run')
-
+ print(
+ f"DRY RUN: Would update nameservers in Namecheap to: {', '.join(nameservers)}"
+ )
+ result["steps_completed"].append("nameserver_update_dry_run")
+
# Step 8: Set up basic DNS records (skip for now, empty DNS list)
print("Setting up basic DNS records...")
try:
# For now, just create an empty DNS list as requested
dns_records = []
- result['dns_records'] = dns_records
- result['steps_completed'].append('basic_dns_setup')
+ result["dns_records"] = dns_records
+ result["steps_completed"].append("basic_dns_setup")
print(f"✓ Basic DNS records ready (empty list as requested)")
except Exception as e:
- result['errors'].append(f"Failed to set up DNS records: {str(e)}")
+ result["errors"].append(f"Failed to set up DNS records: {str(e)}")
return result
-
+
# Step 9: Set up worker subdomain if requested
if setup_workers:
print("Setting up worker subdomain...")
try:
- worker_record = self.cloudflare.create_worker_subdomain(zone_id, f"app.{domain}")
- result['worker_record'] = worker_record
- result['steps_completed'].append('worker_subdomain_setup')
+ worker_record = self.cloudflare.create_worker_subdomain(
+ zone_id, f"app.{domain}"
+ )
+ result["worker_record"] = worker_record
+ result["steps_completed"].append("worker_subdomain_setup")
print(f"✓ Worker subdomain app.{domain} created")
except Exception as e:
- result['errors'].append(f"Failed to set up worker subdomain: {str(e)}")
+ result["errors"].append(
+ f"Failed to set up worker subdomain: {str(e)}"
+ )
return result
-
- result['success'] = True
+
+ result["success"] = True
print(f"\n🎉 Domain {domain} setup completed successfully!")
print(f"Zone ID: {zone_id}")
print(f"Nameservers: {', '.join(nameservers)}")
-
+
return result
-
+
except Exception as e:
- result['errors'].append(f"Unexpected error: {str(e)}")
+ result["errors"].append(f"Unexpected error: {str(e)}")
return result
-
+
def setup_google_analytics_dns(self, domain: str) -> Dict[str, Any]:
"""Set up DNS records for Google Analytics"""
try:
zone_info = self.cloudflare.get_zone_info(domain)
if not zone_info:
- return {'error': f'Domain {domain} not found in Cloudflare'}
-
- records = self.cloudflare.setup_google_analytics_dns(zone_info['id'], domain)
- return {'success': True, 'records': records}
-
+ return {"error": f"Domain {domain} not found in Cloudflare"}
+
+ records = self.cloudflare.setup_google_analytics_dns(
+ zone_info["id"], domain
+ )
+ return {"success": True, "records": records}
+
except Exception as e:
- return {'error': str(e)}
+ return {"error": str(e)}
+
def main():
if len(sys.argv) < 2:
print("Usage: python domain_manager.py <domain> [--dry-run] [--setup-only]")
sys.exit(1)
-
+
domain = sys.argv[1]
- dry_run = '--dry-run' in sys.argv
- setup_only = '--setup-only' in sys.argv
-
+ dry_run = "--dry-run" in sys.argv
+ setup_only = "--setup-only" in sys.argv
+
# Load configuration
config = Config.from_env()
-
-
+
# Validate required configuration
- if not all([
- config.namecheap_api_key,
- config.namecheap_api_user,
- config.namecheap_username,
- config.namecheap_client_ip,
- config.cloudflare_api_token
- ]):
+ if not all(
+ [
+ config.namecheap_api_key,
+ config.namecheap_api_user,
+ config.namecheap_username,
+ config.namecheap_client_ip,
+ config.cloudflare_api_token,
+ ]
+ ):
print("Error: Missing required API credentials. Please check your .env file.")
sys.exit(1)
-
+
# Initialize domain manager
manager = DomainManager(config)
-
+
# Register and setup domain
if setup_only:
print("Running in SETUP-ONLY mode - skipping domain registration")
if dry_run:
print("Running in DRY RUN mode - no actual registration will occur")
- result = manager.register_and_setup_domain(domain, dry_run=dry_run, skip_registration=setup_only)
-
- if result.get('success'):
+ result = manager.register_and_setup_domain(
+ domain, dry_run=dry_run, skip_registration=setup_only
+ )
+
+ if result.get("success"):
print(f"\nSuccess! Domain {domain} is ready for use.")
else:
print(f"\nErrors occurred:")
- for error in result.get('errors', []):
+ for error in result.get("errors", []):
print(f" - {error}")
sys.exit(1)
+
if __name__ == "__main__":
- main() \ No newline at end of file
+ main()
diff --git a/regflow/providers/__init__.py b/regflow/providers/__init__.py
new file mode 100644
index 0000000..1babe54
--- /dev/null
+++ b/regflow/providers/__init__.py
@@ -0,0 +1 @@
+"""DNS and domain registration providers.""" \ No newline at end of file
diff --git a/regflow/providers/cloudflare_api.py b/regflow/providers/cloudflare_api.py
new file mode 100644
index 0000000..e1154f1
--- /dev/null
+++ b/regflow/providers/cloudflare_api.py
@@ -0,0 +1,165 @@
+import requests
+from typing import Dict, Any, List, Optional
+from ..config import Config
+
+
+class CloudflareAPI:
+ def __init__(self, config: Config):
+ self.config = config
+ self.base_url = "https://api.cloudflare.com/client/v4"
+ self.headers = {
+ "Authorization": f"Bearer {config.cloudflare_api_token}",
+ "Content-Type": "application/json",
+ }
+
+ def _make_request(
+ self, method: str, endpoint: str, data: Optional[Dict] = None
+ ) -> Dict[str, Any]:
+ """Make a request to Cloudflare API"""
+ url = f"{self.base_url}{endpoint}"
+
+ response = requests.request(
+ method, url, headers=self.headers, json=data, timeout=30
+ )
+
+ try:
+ response.raise_for_status()
+ except requests.exceptions.HTTPError as e:
+ # Try to get the error details from the response
+ try:
+ error_details = response.json()
+ errors = error_details.get("errors", [])
+ if errors:
+ error_msg = ", ".join(
+ [error.get("message", "Unknown error") for error in errors]
+ )
+ raise Exception(f"Cloudflare API Error: {error_msg}")
+ else:
+ raise Exception(
+ f"Cloudflare API Error: {response.status_code} - {response.text}"
+ )
+ except ValueError:
+ raise Exception(
+ f"Cloudflare API Error: {response.status_code} - {response.text}"
+ )
+
+ result = response.json()
+
+ if not result.get("success", False):
+ errors = result.get("errors", [])
+ if errors:
+ error_msg = ", ".join(
+ [error.get("message", "Unknown error") for error in errors]
+ )
+ raise Exception(f"Cloudflare API Error: {error_msg}")
+ else:
+ raise Exception(
+ f"Cloudflare API Error: Request failed but no error details provided"
+ )
+
+ return result
+
+ def add_zone(self, domain: str) -> Dict[str, Any]:
+ """Add a new zone (domain) to Cloudflare"""
+ data = {"name": domain, "type": "full"}
+
+ result = self._make_request("POST", "/zones", data)
+ return result["result"]
+
+ def get_zone_info(self, domain: str) -> Optional[Dict[str, Any]]:
+ """Get zone information for a domain"""
+ result = self._make_request("GET", f"/zones?name={domain}")
+
+ zones = result.get("result", [])
+ if zones:
+ return zones[0]
+
+ return None
+
+ def get_zone_nameservers(self, zone_id: str) -> List[str]:
+ """Get nameservers for a zone"""
+ result = self._make_request("GET", f"/zones/{zone_id}")
+
+ zone = result.get("result", {})
+ return zone.get("name_servers", [])
+
+ def create_dns_record(
+ self,
+ zone_id: str,
+ record_type: str,
+ name: str,
+ content: str,
+ ttl: int = 300,
+ proxied: bool = False,
+ ) -> Dict[str, Any]:
+ """Create a DNS record"""
+ data = {"type": record_type, "name": name, "content": content, "ttl": ttl}
+
+ if record_type in ["A", "AAAA", "CNAME"]:
+ data["proxied"] = proxied
+
+ result = self._make_request("POST", f"/zones/{zone_id}/dns_records", data)
+ return result["result"]
+
+ def create_worker_subdomain(self, zone_id: str, subdomain: str) -> Dict[str, Any]:
+ """Create a worker route for a subdomain"""
+ # Create A record pointing to dummy IP (will be overridden by worker)
+ return self.create_dns_record(
+ zone_id=zone_id,
+ record_type="A",
+ name=subdomain,
+ content="192.0.2.1", # Dummy IP
+ proxied=True,
+ )
+
+ def setup_google_analytics_dns(
+ self, zone_id: str, domain: str
+ ) -> List[Dict[str, Any]]:
+ """Set up DNS records for Google Analytics"""
+ records = []
+
+ # Google Analytics doesn't typically require specific DNS records
+ # But we can add common verification records if needed
+ # This is a placeholder for future GA4 requirements
+
+ return records
+
+ def setup_basic_dns_records(
+ self, zone_id: str, domain: str
+ ) -> List[Dict[str, Any]]:
+ """Set up basic DNS records for a domain"""
+ records = []
+
+ # Root domain A record (placeholder)
+ records.append(
+ self.create_dns_record(
+ zone_id=zone_id,
+ record_type="A",
+ name=domain,
+ content="192.0.2.1",
+ proxied=True,
+ )
+ )
+
+ # WWW CNAME record
+ records.append(
+ self.create_dns_record(
+ zone_id=zone_id,
+ record_type="CNAME",
+ name=f"www.{domain}",
+ content=domain,
+ proxied=True,
+ )
+ )
+
+ return records
+
+ def list_zones(self) -> List[Dict[str, Any]]:
+ """List all zones in the account"""
+ result = self._make_request("GET", "/zones")
+ return result.get("result", [])
+
+ def get_zone_dns_records(self, zone_id: str) -> List[Dict[str, Any]]:
+ """Get all DNS records for a zone"""
+ result = self._make_request("GET", f"/zones/{zone_id}/dns_records")
+ return result.get("result", [])
diff --git a/regflow/providers/namecheap_api.py b/regflow/providers/namecheap_api.py
new file mode 100644
index 0000000..6449ecd
--- /dev/null
+++ b/regflow/providers/namecheap_api.py
@@ -0,0 +1,163 @@
+import requests
+import xml.etree.ElementTree as ET
+from typing import Dict, Any, Optional
+from ..config import Config
+
+
+class NamecheapAPI:
+ def __init__(self, config: Config):
+ self.config = config
+ self.base_url = "https://api.namecheap.com/xml.response"
+
+ def _make_request(self, command: str, params: Dict[str, Any]) -> ET.Element:
+ """Make a request to Namecheap API"""
+ default_params = {
+ "ApiUser": self.config.namecheap_api_user,
+ "ApiKey": self.config.namecheap_api_key,
+ "UserName": self.config.namecheap_username,
+ "ClientIp": self.config.namecheap_client_ip,
+ "Command": command,
+ }
+
+ all_params = {**default_params, **params}
+
+ try:
+ response = requests.get(self.base_url, params=all_params, timeout=60)
+ response.raise_for_status()
+ except requests.exceptions.Timeout:
+ raise Exception(f"API request timed out for command: {command}")
+ except requests.exceptions.RequestException as e:
+ raise Exception(f"API request failed for command {command}: {str(e)}")
+
+ root = ET.fromstring(response.text)
+
+ # Check for API errors
+ if root.get("Status") == "ERROR":
+ ns = {"ns": "http://api.namecheap.com/xml.response"}
+ errors = root.find(".//ns:Errors", ns)
+ if errors is not None:
+ error_elem = errors.find(".//ns:Error", ns)
+ if error_elem is not None:
+ error_msg = error_elem.text
+ raise Exception(f"Namecheap API Error: {error_msg}")
+ raise Exception("Namecheap API returned an error")
+
+ return root
+
+ def check_domain_availability(self, domain: str) -> bool:
+ """Check if domain is available for registration"""
+ params = {"DomainList": domain}
+
+ root = self._make_request("namecheap.domains.check", params)
+
+ # Parse the response
+ ns = {"ns": "http://api.namecheap.com/xml.response"}
+ domain_check = root.find(".//ns:DomainCheckResult", ns)
+
+ if domain_check is None:
+ raise Exception(f"Could not find domain check result for {domain}")
+
+ return domain_check.get("Available") == "true"
+
+ def get_domain_pricing(self, domain: str) -> Dict[str, float]:
+ """Get pricing information for a domain"""
+ tld = domain.split(".")[-1].upper()
+
+ params = {
+ "ProductType": "DOMAIN",
+ "ProductCategory": "DOMAINS",
+ "ActionName": "REGISTER",
+ "ProductName": tld,
+ }
+
+ root = self._make_request("namecheap.users.getPricing", params)
+
+ # Find pricing for the TLD
+ ns = {"ns": "http://api.namecheap.com/xml.response"}
+
+ # Look for the specific TLD in the response
+ for product in root.findall(".//ns:Product", ns):
+ product_name = product.get("Name")
+ if product_name and product_name.lower() == tld.lower():
+ for price in product.findall(".//ns:Price", ns):
+ duration = price.get("Duration")
+ if duration == "1":
+ price_val = float(price.get("Price", 0))
+
+ # Get renewal price (might be in a separate call or same structure)
+ renew_val = (
+ price_val # Use same price for renewal if not specified
+ )
+
+ return {"register": price_val, "renew": renew_val}
+
+ raise Exception(f"No pricing information found for .{tld} domains")
+
+ def get_account_balance(self) -> float:
+ """Get current account balance"""
+ root = self._make_request("namecheap.users.getBalances", {})
+
+ ns = {"ns": "http://api.namecheap.com/xml.response"}
+ balance_result = root.find(".//ns:UserGetBalancesResult", ns)
+
+ if balance_result is not None:
+ available_balance = balance_result.get("AvailableBalance")
+ if available_balance:
+ return float(available_balance)
+
+ raise Exception("Could not retrieve account balance")
+
+ def register_domain(
+ self,
+ domain: str,
+ years: int = 1,
+ registrant_info: Optional[Dict[str, str]] = None,
+ ) -> bool:
+ """Register a domain"""
+ if not registrant_info:
+ registrant_info = {
+ "FirstName": "John",
+ "LastName": "Doe",
+ "Address1": "123 Main St",
+ "City": "Anytown",
+ "StateProvince": "NY",
+ "PostalCode": "12345",
+ "Country": "US",
+ "Phone": "+1.5551234567",
+ "EmailAddress": "john.doe@example.com",
+ }
+
+ params = {
+ "DomainName": domain,
+ "Years": str(years),
+ **{f"Registrant{key}": value for key, value in registrant_info.items()},
+ **{f"Tech{key}": value for key, value in registrant_info.items()},
+ **{f"Admin{key}": value for key, value in registrant_info.items()},
+ **{f"AuxBilling{key}": value for key, value in registrant_info.items()},
+ }
+
+ root = self._make_request("namecheap.domains.create", params)
+
+ # Check if registration was successful
+ ns = {"ns": "http://api.namecheap.com/xml.response"}
+ domain_create = root.find(".//ns:DomainCreateResult", ns)
+ if domain_create is None:
+ raise Exception(f"Could not find domain creation result for {domain}")
+
+ return domain_create.get("Registered") == "true"
+
+ def set_dns_servers(self, domain: str, nameservers: list) -> bool:
+ """Set DNS servers for a domain"""
+ sld, tld = domain.split(".", 1)
+
+ params = {"SLD": sld, "TLD": tld, "Nameservers": ",".join(nameservers)}
+
+ root = self._make_request("namecheap.domains.dns.setCustom", params)
+
+ # Check if DNS update was successful
+ ns = {"ns": "http://api.namecheap.com/xml.response"}
+ dns_result = root.find(".//ns:DomainDNSSetCustomResult", ns)
+ if dns_result is None:
+ raise Exception(f"Could not find DNS update result for {domain}")
+
+ return dns_result.get("Updated") == "true"
diff --git a/regflow/tests/__init__.py b/regflow/tests/__init__.py
new file mode 100644
index 0000000..a9a6965
--- /dev/null
+++ b/regflow/tests/__init__.py
@@ -0,0 +1 @@
+"""Test suite for RegFlow.""" \ No newline at end of file
diff --git a/tests/test_integration.py b/regflow/tests/test_integration.py
index 4748172..7cb52b9 100644
--- a/tests/test_integration.py
+++ b/regflow/tests/test_integration.py
@@ -1,8 +1,8 @@
import pytest
import os
-from config import Config
-from namecheap_api import NamecheapAPI
-from cloudflare_api import CloudflareAPI
+from ..config import Config
+from ..providers.namecheap_api import NamecheapAPI
+from ..providers.cloudflare_api import CloudflareAPI
@pytest.fixture
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index 9b534f0..0000000
--- a/tests/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Integration tests for regflow \ No newline at end of file