186 lines
6.3 KiB
Python
186 lines
6.3 KiB
Python
"""
|
|
SITAR API Base Service
|
|
|
|
Base service class for SITAR API authentication and HTTP requests.
|
|
All specific resource services inherit from this.
|
|
"""
|
|
|
|
import os
|
|
from typing import Optional, Dict, Any, List, Tuple
|
|
from datetime import datetime, timedelta
|
|
import httpx
|
|
|
|
|
|
class SitarAPIBaseService:
|
|
"""Base service for SITAR API integration with authentication"""
|
|
|
|
_token: Optional[str] = None
|
|
_token_expires: Optional[datetime] = None
|
|
|
|
@staticmethod
|
|
def _parse_paginated_list_and_total(data: Any) -> Tuple[List[Any], int]:
|
|
"""
|
|
Paginated list endpoints (fracciones, fracciones-usa) return:
|
|
{ "data": [...], "total", "page", "limit", "total_pages" }.
|
|
Older responses may be a plain JSON array; then total is len(rows) for that page only.
|
|
"""
|
|
if isinstance(data, list):
|
|
return data, len(data)
|
|
if isinstance(data, dict) and isinstance(data.get("data"), list):
|
|
rows = data["data"]
|
|
raw_total = data.get("total")
|
|
total = int(raw_total) if raw_total is not None else len(rows)
|
|
return rows, total
|
|
raise ValueError(
|
|
f"Unexpected SITAR list response shape: {type(data).__name__}"
|
|
)
|
|
|
|
@staticmethod
|
|
def _unwrap_paginated_list(data: Any) -> List[Any]:
|
|
rows, _ = SitarAPIBaseService._parse_paginated_list_and_total(data)
|
|
return rows
|
|
|
|
def __init__(self):
|
|
"""Initialize base service with API credentials"""
|
|
self.base_url = os.getenv("SITAR_API_URL")
|
|
self.username = os.getenv("SITAR_API_USER")
|
|
self.password = os.getenv("SITAR_API_PASSWORD")
|
|
self.timeout = 30.0
|
|
|
|
if not all([self.base_url, self.username, self.password]):
|
|
raise ValueError(
|
|
"Missing SITAR API configuration. "
|
|
"Set SITAR_API_URL, SITAR_API_USER, and SITAR_API_PASSWORD environment variables."
|
|
)
|
|
|
|
async def _get_token(self) -> str:
|
|
"""
|
|
Get authentication token, refreshing if necessary
|
|
|
|
Returns:
|
|
str: Bearer token for API authentication
|
|
|
|
Raises:
|
|
httpx.HTTPError: If authentication fails
|
|
"""
|
|
# Return cached token if still valid
|
|
if self._token and self._token_expires and datetime.now() < self._token_expires:
|
|
return self._token
|
|
|
|
# Authenticate and get new token
|
|
login_url = f"{self.base_url}/fractions/api/v1/auth/login"
|
|
payload = {"username": self.username, "password": self.password}
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
response = await client.post(login_url, json=payload)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
self._token = data.get("access_token") or data.get("token")
|
|
|
|
if not self._token:
|
|
raise ValueError("No token received from SITAR API")
|
|
|
|
# Set token expiration (assume 1 hour if not specified)
|
|
self._token_expires = datetime.now() + timedelta(hours=1)
|
|
|
|
return self._token
|
|
|
|
async def _make_request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
json_data: Optional[Dict[str, Any]] = None,
|
|
) -> Any:
|
|
"""
|
|
Make authenticated request to SITAR API
|
|
|
|
Args:
|
|
method: HTTP method (GET, POST, etc.)
|
|
endpoint: API endpoint path
|
|
params: Query parameters
|
|
json_data: JSON body data
|
|
|
|
Returns:
|
|
JSON response data
|
|
|
|
Raises:
|
|
httpx.HTTPError: If request fails
|
|
"""
|
|
token = await self._get_token()
|
|
# Ensure no double slash between fractures and endpoint
|
|
endpoint = endpoint.lstrip("/")
|
|
url = f"{self.base_url}/fractions/{endpoint}"
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
response = await client.request(
|
|
method=method,
|
|
url=url,
|
|
params=params,
|
|
json=json_data,
|
|
headers=headers,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# DEBUG LOGGING for SITAR inspection
|
|
if "fracciones" in url:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
try:
|
|
data = response.json()
|
|
return data
|
|
except Exception:
|
|
logger.error(f"Error parsing SITAR API Response Body: {response.text}")
|
|
pass
|
|
|
|
return response.json()
|
|
|
|
def _get_token_sync(self) -> str:
|
|
"""Same token cache as async path; safe for sync validators (no running asyncio loop)."""
|
|
if self._token and self._token_expires and datetime.now() < self._token_expires:
|
|
return self._token
|
|
|
|
login_url = f"{self.base_url}/fractions/api/v1/auth/login"
|
|
payload = {"username": self.username, "password": self.password}
|
|
|
|
with httpx.Client(timeout=self.timeout) as client:
|
|
response = client.post(login_url, json=payload)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
token = data.get("access_token") or data.get("token")
|
|
if not token:
|
|
raise ValueError("No token received from SITAR API")
|
|
self._token = token
|
|
self._token_expires = datetime.now() + timedelta(hours=1)
|
|
return token
|
|
|
|
def _make_request_sync(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
json_data: Optional[Dict[str, Any]] = None,
|
|
) -> Any:
|
|
token = self._get_token_sync()
|
|
endpoint = endpoint.lstrip("/")
|
|
url = f"{self.base_url}/fractions/{endpoint}"
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
with httpx.Client(timeout=self.timeout) as client:
|
|
response = client.request(
|
|
method=method,
|
|
url=url,
|
|
params=params,
|
|
json=json_data,
|
|
headers=headers,
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|