"""Dynadot TLD price scraper. Dynadot is a popular domain registrar known for competitive pricing and straightforward pricing structure (less aggressive upselling). """ import logging from datetime import datetime from typing import Optional import httpx from app.services.tld_scraper.base import BaseTLDScraper, TLDPriceData, ScraperError logger = logging.getLogger(__name__) class DynadotScraper(BaseTLDScraper): """ Scraper for Dynadot domain prices. Dynadot has a public TLD pricing page and relatively stable pricing. They're known for: - Competitive pricing on popular TLDs - Less aggressive promotional tactics than GoDaddy - Reasonable renewal prices """ name = "dynadot" base_url = "https://www.dynadot.com" # Dynadot TLD pricing API endpoint (if available) PRICING_API = "https://www.dynadot.com/domain/tld-pricing.html" # Known Dynadot prices (as of Dec 2024) # Source: https://www.dynadot.com/domain/tld-pricing.html DYNADOT_PRICES = { # Major TLDs "com": {"reg": 10.99, "renew": 10.99, "transfer": 10.99}, "net": {"reg": 12.99, "renew": 12.99, "transfer": 12.99}, "org": {"reg": 11.99, "renew": 11.99, "transfer": 11.99}, "info": {"reg": 3.99, "renew": 18.99, "transfer": 3.99}, "biz": {"reg": 14.99, "renew": 14.99, "transfer": 14.99}, # Premium Tech TLDs "io": {"reg": 34.99, "renew": 34.99, "transfer": 34.99}, "co": {"reg": 11.99, "renew": 25.99, "transfer": 11.99}, "ai": {"reg": 69.99, "renew": 69.99, "transfer": 69.99}, "dev": {"reg": 13.99, "renew": 13.99, "transfer": 13.99}, "app": {"reg": 15.99, "renew": 15.99, "transfer": 15.99}, # Budget TLDs "xyz": {"reg": 1.99, "renew": 12.99, "transfer": 1.99}, "tech": {"reg": 4.99, "renew": 44.99, "transfer": 4.99}, "online": {"reg": 2.99, "renew": 34.99, "transfer": 2.99}, "site": {"reg": 2.99, "renew": 29.99, "transfer": 2.99}, "store": {"reg": 2.99, "renew": 49.99, "transfer": 2.99}, "me": {"reg": 4.99, "renew": 17.99, "transfer": 4.99}, # European ccTLDs "uk": {"reg": 8.49, "renew": 8.49, "transfer": 8.49}, "de": {"reg": 7.99, "renew": 7.99, "transfer": 7.99}, "eu": {"reg": 7.99, "renew": 7.99, "transfer": 7.99}, "fr": {"reg": 9.99, "renew": 9.99, "transfer": 9.99}, "nl": {"reg": 8.99, "renew": 8.99, "transfer": 8.99}, "it": {"reg": 9.99, "renew": 9.99, "transfer": 9.99}, "es": {"reg": 8.99, "renew": 8.99, "transfer": 8.99}, "at": {"reg": 12.99, "renew": 12.99, "transfer": 12.99}, "be": {"reg": 8.99, "renew": 8.99, "transfer": 8.99}, "ch": {"reg": 11.99, "renew": 11.99, "transfer": 11.99}, # Other popular TLDs "ca": {"reg": 11.99, "renew": 11.99, "transfer": 11.99}, "us": {"reg": 8.99, "renew": 8.99, "transfer": 8.99}, "tv": {"reg": 31.99, "renew": 31.99, "transfer": 31.99}, "cc": {"reg": 11.99, "renew": 11.99, "transfer": 11.99}, "in": {"reg": 9.99, "renew": 9.99, "transfer": 9.99}, "jp": {"reg": 44.99, "renew": 44.99, "transfer": 44.99}, # New gTLDs "club": {"reg": 1.99, "renew": 14.99, "transfer": 1.99}, "shop": {"reg": 2.99, "renew": 32.99, "transfer": 2.99}, "blog": {"reg": 2.99, "renew": 28.99, "transfer": 2.99}, "cloud": {"reg": 3.99, "renew": 21.99, "transfer": 3.99}, "live": {"reg": 2.99, "renew": 24.99, "transfer": 2.99}, "world": {"reg": 2.99, "renew": 31.99, "transfer": 2.99}, "global": {"reg": 69.99, "renew": 69.99, "transfer": 69.99}, "agency": {"reg": 2.99, "renew": 22.99, "transfer": 2.99}, "digital": {"reg": 2.99, "renew": 34.99, "transfer": 2.99}, "media": {"reg": 2.99, "renew": 34.99, "transfer": 2.99}, "network": {"reg": 2.99, "renew": 22.99, "transfer": 2.99}, "software": {"reg": 2.99, "renew": 32.99, "transfer": 2.99}, "solutions": {"reg": 2.99, "renew": 22.99, "transfer": 2.99}, "systems": {"reg": 2.99, "renew": 22.99, "transfer": 2.99}, } async def scrape(self) -> list[TLDPriceData]: """ Scrape TLD prices from Dynadot. First attempts to fetch from their pricing page API, falls back to static data if unavailable. Returns: List of TLDPriceData objects with Dynadot pricing """ # Try to scrape live data first try: live_prices = await self._scrape_live() if live_prices and len(live_prices) > 50: # Got meaningful data return live_prices except Exception as e: logger.warning(f"Dynadot live scrape failed: {e}, using static data") # Fallback to static data return await self._get_static_prices() async def _scrape_live(self) -> list[TLDPriceData]: """Attempt to scrape live pricing data from Dynadot.""" # Dynadot's pricing page loads via JavaScript, # so we'd need Playwright for full scraping. # For now, return empty to use static fallback. return [] async def _get_static_prices(self) -> list[TLDPriceData]: """Return static Dynadot pricing data.""" results = [] now = datetime.utcnow() for tld, prices in self.DYNADOT_PRICES.items(): # Dynadot has reasonable renewal pricing for most TLDs is_renewal_trap = prices["renew"] > prices["reg"] * 2 results.append(TLDPriceData( tld=tld, registrar="dynadot", registration_price=prices["reg"], renewal_price=prices["renew"], transfer_price=prices.get("transfer"), currency="USD", source="static", confidence=0.9, scraped_at=now, notes="Promotional intro price" if is_renewal_trap else None, )) logger.info(f"Loaded {len(results)} Dynadot prices (static)") return results async def health_check(self) -> bool: """Check if Dynadot is accessible.""" try: async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get( self.base_url, headers=self.get_headers(), follow_redirects=True, ) return response.status_code == 200 except Exception as e: logger.debug(f"Dynadot health check failed: {e}") return False