"""HTTP helpers for the HubSpot PLG pipeline.""" import logging import urllib.parse from typing import Any, Dict, Iterator, List, Optional from dlt.sources.helpers import requests logger = logging.getLogger(__name__) BASE_URL = "https://api.hubapi.com/" def get_url(endpoint: str) -> str: return urllib.parse.urljoin(BASE_URL, endpoint) def _get_headers(api_key: str) -> Dict[str, str]: return {"authorization": f"Bearer {api_key}"} def fetch_crm_objects( endpoint: str, api_key: str, params: Optional[Dict[str, Any]] = None, ) -> Iterator[List[Dict[str, Any]]]: """Paginated GET for CRM v3 endpoints. Flattens each result's ``properties`` dict into the top-level row and preserves ``id``.""" url = get_url(endpoint) headers = _get_headers(api_key) params = dict(params or {}) while True: r = requests.get(url, headers=headers, params=params) data = r.json() results = data.get("results", []) if results: page: List[Dict[str, Any]] = [] for item in results: obj = item.get("properties", item) if "id" not in obj and "id" in item: obj["id"] = item["id"] page.append(obj) yield page after = (data.get("paging") or {}).get("next", {}).get("after") if not after: break params["after"] = after def fetch_get_pages( endpoint: str, api_key: str, params: Optional[Dict[str, Any]] = None, results_key: str = "results", ) -> Iterator[List[Dict[str, Any]]]: """Generic paginated GET that yields raw result dicts (no property flattening). Works for /marketing/v3/emails, /automation/v4/flows, /marketing/v3/forms, etc.""" url = get_url(endpoint) headers = _get_headers(api_key) params = dict(params or {}) while True: r = requests.get(url, headers=headers, params=params) data = r.json() results = data.get(results_key, []) if results: yield results after = (data.get("paging") or {}).get("next", {}).get("after") if not after: break params["after"] = after def fetch_post_pages( endpoint: str, api_key: str, body: Optional[Dict[str, Any]] = None, results_key: str = "lists", offset_key: str = "offset", count_key: str = "count", page_size: int = 250, ) -> Iterator[List[Dict[str, Any]]]: """Paginated POST (used by /crm/v3/lists/search which uses offset-based pagination).""" url = get_url(endpoint) headers = {**_get_headers(api_key), "Content-Type": "application/json"} body = dict(body or {}) body.setdefault(offset_key, 0) body.setdefault(count_key, page_size) while True: r = requests.post(url, headers=headers, json=body) data = r.json() results = data.get(results_key, []) if results: yield results total = data.get("total") body[offset_key] += len(results) if not results or (total is not None and body[offset_key] >= total): break def fetch_json( endpoint: str, api_key: str, params: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Single GET request returning raw JSON (no pagination).""" url = get_url(endpoint) headers = _get_headers(api_key) r = requests.get(url, headers=headers, params=params) return r.json() def fetch_form_submissions( form_guid: str, api_key: str, limit: int = 50, ) -> Iterator[List[Dict[str, Any]]]: """Paginated GET for form submission endpoint which uses cursor-based ``after`` in the response body.""" endpoint = f"/form-integrations/v1/submissions/forms/{form_guid}" url = get_url(endpoint) headers = _get_headers(api_key) params: Dict[str, Any] = {"limit": limit} while True: r = requests.get(url, headers=headers, params=params) data = r.json() results = data.get("results", []) if results: yield results after = data.get("paging", {}).get("next", {}).get("after") if not after: break params["after"] = after