"""Cliente mínimo para Packlink PRO. A Packlink PRO usa API key no header Authorization, sem Bearer. Base testada: https://api.packlink.com/v1 """ from __future__ import annotations from dataclasses import dataclass from typing import Any, Dict, List, Optional import httpx from app.config import settings class PacklinkError(RuntimeError): pass @dataclass(frozen=True) class PacklinkConfig: base_url: str api_key: str def get_packlink_config() -> PacklinkConfig: base_url = (settings.packlink_base_url or "https://api.packlink.com/v1").rstrip("/") api_key = (settings.packlink_api_key or "").strip() if not api_key: raise PacklinkError("PACKLINK_API_KEY em falta") return PacklinkConfig(base_url=base_url, api_key=api_key) class PacklinkClient: def __init__(self, config: Optional[PacklinkConfig] = None, timeout: float = 30.0): self.config = config or get_packlink_config() self.timeout = timeout @property def headers(self) -> Dict[str, str]: return { "Authorization": self.config.api_key, "Accept": "application/json", "Content-Type": "application/json", } async def _request( self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None, ) -> Any: url = f"{self.config.base_url}/{path.lstrip('/')}" async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.request( method.upper(), url, headers=self.headers, params=params, json=json_body, ) if response.status_code >= 400: detail = response.text try: detail = response.json() except Exception: pass raise PacklinkError(f"Packlink error {response.status_code} em {method.upper()} {path}: {detail}") if not response.content: return None try: return response.json() except Exception as exc: raise PacklinkError(f"Resposta Packlink não é JSON em {method.upper()} {path}: {response.text[:500]}") from exc async def get_client(self) -> Dict[str, Any]: return await self._request("GET", "/clients") async def get_warehouses(self) -> List[Dict[str, Any]]: data = await self._request("GET", "/clients/warehouses") return data or [] async def get_parcels(self) -> List[Dict[str, Any]]: data = await self._request("GET", "/users/parcels") return data or [] async def validate_postal_code(self, country: str, zip_code: str) -> List[Dict[str, Any]]: return await self._request("GET", f"/locations/postalcodes/{country}/{zip_code}") async def quote_services( self, *, from_country: str, from_zip: str, to_country: str, to_zip: str, packages: List[Dict[str, Any]], source: str = "PRO", ) -> List[Dict[str, Any]]: params: Dict[str, Any] = { "from[country]": from_country, "from[zip]": from_zip, "to[country]": to_country, "to[zip]": to_zip, "source": source, } for idx, package in enumerate(packages): params[f"packages[{idx}][height]"] = package.get("height") params[f"packages[{idx}][width]"] = package.get("width") params[f"packages[{idx}][length]"] = package.get("length") params[f"packages[{idx}][weight]"] = package.get("weight") data = await self._request("GET", "/services", params=params) return data or [] async def get_service_details(self, service_id: str | int) -> Dict[str, Any]: return await self._request("GET", f"/services/available/{service_id}/details") async def create_shipment(self, payload: Dict[str, Any]) -> Dict[str, Any]: return await self._request("POST", "/shipments", json_body=payload) async def list_shipments(self, *, limit: int = 100, offset: int = 0) -> Any: """List shipments when Packlink exposes the collection endpoint. Some Packlink tenants/API versions may restrict this endpoint. The reconciliation sync catches those API errors and reports them instead of creating operational tasks. """ params = {"limit": min(max(int(limit), 1), 100), "offset": max(int(offset), 0)} return await self._request("GET", "/shipments", params=params) async def get_shipment(self, reference: str) -> Dict[str, Any]: return await self._request("GET", f"/shipments/{reference}") async def get_labels(self, reference: str) -> List[Any]: data = await self._request("GET", f"/shipments/{reference}/labels") return data or [] async def get_tracking(self, reference: str) -> List[Dict[str, Any]]: data = await self._request("GET", f"/shipments/{reference}/track") return data or []