import re import httpx from datetime import date, timedelta from .base import BaseProfile API_BASE = "https://api.travelclick.com" TOKEN_URL = f"{API_BASE}/oauth/token-referer?grant_type=client_credentials" HEADERS_BASE = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json", "Content-Type": "application/json", } async def _get_token(client, referer: str) -> str: r = await client.post(TOKEN_URL, headers={**HEADERS_BASE, "Referer": referer}, timeout=20) r.raise_for_status() return r.json()["access_token"] def _auth_headers(token: str, referer: str) -> dict: return {**HEADERS_BASE, "Authorization": f"Bearer {token}", "Referer": referer} class TravelClickProfile(BaseProfile): name = "travelclick" label = "TravelClick / Amadeus" required_params = [ {"key": "hotel_code", "label": "Hotel Code", "help": "Numeric hotel ID, e.g. 77346 — visible in the booking engine network requests"}, {"key": "booking_url", "label": "Booking URL Base", "help": "e.g. https://reservations.bespokehotels.com/noelarmshotel/book/dates-of-stay"}, ] @classmethod def detect(cls, url: str) -> dict | None: m = re.search(r'(https://reservations\.bespokehotels\.com/[^/]+/book/[^?#\s]+)', url) if not m: return None booking_url = m.group(1) # hotel_code is in inline JS as bookingEngineHotelId: '77346' on the booking page hotel_code = "" try: r = httpx.get(booking_url, timeout=10, follow_redirects=True, headers={"User-Agent": "Mozilla/5.0"}) hm = re.search(r'bookingEngineHotelId\s*:\s*[\'"](\d+)[\'"]', r.text) if hm: hotel_code = hm.group(1) except Exception: pass return {"booking_url": booking_url, "hotel_code": hotel_code} async def fetch_category_names(self, client, params: dict) -> tuple[dict[str, str], dict[str, str]]: hotel_code = params["hotel_code"] referer = params["booking_url"] token = await _get_token(client, referer) r = await client.get( f"{API_BASE}/be5-entity/v2/hotels/{hotel_code}/content", params={"include": "roomtypes,rateplans", "lang": "EN_US"}, headers=_auth_headers(token, referer), timeout=30, ) r.raise_for_status() data = r.json() # roomtypes[].roomTypeId (numeric, used as roomId in avail) -> roomTypeName room_labels = { str(rt["roomTypeId"]): rt.get("roomTypeName", str(rt["roomTypeId"])) for rt in data.get("roomtypes", []) } # ratePlans[].rateplanId (numeric, used as rateId in avail) -> rateplanName rate_labels = { str(rp["rateplanId"]): rp.get("rateplanName", str(rp["rateplanId"])) for rp in data.get("ratePlans", []) } return room_labels, rate_labels async def fetch_arrival_dates(self, client, params: dict) -> list[str]: hotel_code = params["hotel_code"] referer = params["booking_url"] token = await _get_token(client, referer) today = date.today() end = today + timedelta(days=365) body = { "hotelCode": int(hotel_code), "currency": "GBP", "lang": "EN_US", "dateIn": today.isoformat(), "dateOut": end.isoformat(), "multiRoomOccupancy": [{"adults": 2, "infant": 0, "children": 0}], "bookerIdentifier": "", "partnerIdentifier": "", } r = await client.post( f"{API_BASE}/be5-shop/v1/hotel/{hotel_code}/basicavail/multi-room", json=body, headers=_auth_headers(token, referer), timeout=30, ) r.raise_for_status() data = r.json() return [ d["date"] for d in data.get("dates", []) if d.get("isAvailable") and d["date"] >= today.isoformat() ] async def fetch_night_rates(self, client, params: dict, arrival: str, nights: int = 1) -> list[dict]: hotel_code = params["hotel_code"] referer = params["booking_url"] token = await _get_token(client, referer) departure = (date.fromisoformat(arrival) + timedelta(days=nights)).isoformat() body = { "roomStay": { "startDate": arrival, "endDate": departure, "guestCount": {"adults": 2, "infants": 0, "children": {"ages": None, "count": 0}}, "roomQuantity": 1, "productSearchCriteria": { "sortingPreference": "SORT_BY_ORDER", "includeUnavailable": False, }, }, "languageCode": "EN_US", "disableLocaitonSharing": False, "currencyCode": "GBP", "tpaExtension": [], "includeMemberRate": True, "includeNightlyRates": True, } r = await client.post( f"{API_BASE}/be5-shop/v2/hotels/{hotel_code}/avail", json=body, headers=_auth_headers(token, referer), timeout=30, ) if r.status_code == 404: return [] r.raise_for_status() data = r.json() # Response: roomStays[0].roomtypes[].products[] (both Regular rates and Packages) room_stay = (data.get("roomStays") or [{}])[0] rooms = [] for rt in room_stay.get("roomtypes", []): room_id = str(rt["roomtypeId"]) for product in rt.get("products", []): rate_id = str(product["productId"]) nightly = product.get("nightlyRates", []) if not nightly: continue avail = nightly[0].get("inventoryCount", 0) prices = [ {"amountBeforeTax": n["amountBeforeTax"], "amountAfterTax": n.get("amountTotal", n["amountBeforeTax"])} for n in nightly ] rooms.append({ "roomId": room_id, "rateId": rate_id, "availability": avail, "prices": prices, "min_stay_nights": None, # min-stay comes from basicavail per date "currencyCode": "GBP", }) return rooms