Release v4928.1.4.2 stable
This commit is contained in:
145
app/packlink_client.py
Normal file
145
app/packlink_client.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""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 []
|
||||
Reference in New Issue
Block a user