import re from datetime import date, timedelta from .base import BaseProfile HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } # Matches any opening tag that contains data-room-type-id="..." _TAG_RE = re.compile(r'<[^>]*data-room-type-id="[^"]*"[^>]*>', re.DOTALL) # Individual data-* attribute extractor _ATTR_RE = re.compile(r'data-([\w-]+)="([^"]*)"') def _slugify(text: str) -> str: """Lowercase and replace whitespace runs with underscores.""" return re.sub(r'\s+', '_', text.strip().lower()) def _extract_data_attrs(tag: str) -> dict: """Return all data-* attributes from an HTML opening tag as a flat dict.""" return {m.group(1): m.group(2) for m in _ATTR_RE.finditer(tag)} class EviivoProfile(BaseProfile): name = "eviivo" label = "Eviivo" required_params = [ { "key": "property_slug", "label": "Property Slug", "help": "Path segment after via.eviivo.com/, e.g. StagLdgeGL54", }, ] @classmethod def detect(cls, url: str) -> dict | None: # Matches: https://via.eviivo.com/StagLdgeGL54 (with optional path/query) m = re.search(r'via\.eviivo\.com/([A-Za-z0-9_-]+)', url) if m: return {"property_slug": m.group(1)} return None async def fetch_category_names(self, client, params: dict) -> tuple[dict, dict]: slug = params["property_slug"] try: r = await client.get(f"https://via.eviivo.com/{slug}", headers=HEADERS, timeout=20) except Exception: return {}, {} if r.status_code != 200: return {}, {} room_names: dict[str, str] = {} rate_names: dict[str, str] = {} for tag in _TAG_RE.finditer(r.text): attrs = _extract_data_attrs(tag.group(0)) room_id = attrs.get("room-type-id", "").strip() if not room_id: continue room_name = attrs.get("item-name", "").strip() rate_plan = attrs.get("rate-plan-name", "").strip() rate_id = f"{room_id}_{_slugify(rate_plan) if rate_plan else 'standard'}" if room_name: room_names[room_id] = room_name if rate_plan: rate_names[rate_id] = rate_plan return room_names, rate_names async def fetch_arrival_dates(self, client, params: dict) -> list[str]: """Return the next 90 days as ISO date strings starting from tomorrow. Eviivo has no dedicated calendar endpoint; availability is determined by whether rooms appear in the fetch_night_rates response. """ tomorrow = date.today() + timedelta(days=1) return [ (tomorrow + timedelta(days=i)).isoformat() for i in range(90) ] async def fetch_night_rates(self, client, params: dict, arrival: str, nights: int = 1) -> list[dict]: """Scrape Eviivo room rates for a given arrival date and stay length.""" slug = params["property_slug"] arrival_date = date.fromisoformat(arrival) departure_date = arrival_date + timedelta(days=nights) url = f"https://via.eviivo.com/{slug}" query = { "startdate": arrival, "enddate": departure_date.isoformat(), "adults1": 2, "children1": 0, } try: r = await client.get(url, params=query, headers=HEADERS, timeout=20) except Exception: return [] if r.status_code != 200: return [] html = r.text results = [] for tag in _TAG_RE.finditer(html): attrs = _extract_data_attrs(tag.group(0)) room_type_id = attrs.get("room-type-id", "").strip() if not room_type_id: continue rate_plan_name = attrs.get("rate-plan-name", "").strip() rate_slug = _slugify(rate_plan_name) if rate_plan_name else "standard" rate_id = f"{room_type_id}_{rate_slug}" price_str = attrs.get("price", "").strip() try: total_price = float(price_str) except (ValueError, TypeError): continue per_night = round(total_price / nights, 2) if nights > 0 else total_price currency = attrs.get("currency-code", "GBP").strip() or "GBP" prices = [ {"amountBeforeTax": per_night, "amountAfterTax": per_night} for _ in range(nights) ] results.append({ "roomId": room_type_id, "rateId": rate_id, "availability": 1, "prices": prices, "currencyCode": currency, }) return results