a35be94996
Клиенты и билдеры payload'ов, сервисы и Celery-задачи push identity/данных во внешние порталы, HTTPS-guard в проде. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.4 KiB
Python
76 lines
2.4 KiB
Python
"""HTTP client ACORE → PNMS identity sync."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PnmsClientError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class PnmsClient:
|
|
def __init__(
|
|
self,
|
|
base_url: str | None = None,
|
|
token: str | None = None,
|
|
timeout: int = 60,
|
|
):
|
|
self.base_url = (base_url or getattr(settings, 'PNMS_BASE_URL', '') or '').rstrip('/')
|
|
self.token = token or getattr(settings, 'PNMS_ACORE_TOKEN', '') or ''
|
|
self.timeout = timeout
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return bool(
|
|
getattr(settings, 'PNMS_SYNC_ENABLED', False)
|
|
and self.base_url
|
|
and self.token
|
|
)
|
|
|
|
def _headers(self) -> dict[str, str]:
|
|
return {
|
|
'X-Acore-Token': self.token,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
}
|
|
|
|
def _ensure_tls(self) -> None:
|
|
"""В проде payload может содержать password_hash (Django PBKDF2)."""
|
|
if getattr(settings, 'DEBUG', True):
|
|
return
|
|
scheme = urlparse(self.base_url).scheme.lower()
|
|
if scheme != 'https':
|
|
raise PnmsClientError(
|
|
'PNMS_BASE_URL must use https:// when DEBUG is False '
|
|
'(identity payload may include password hashes).'
|
|
)
|
|
|
|
def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
if not self.enabled:
|
|
raise PnmsClientError('PNMS sync disabled or PNMS_BASE_URL/TOKEN missing')
|
|
self._ensure_tls()
|
|
url = f'{self.base_url}{path}'
|
|
resp = requests.post(
|
|
url, json=payload, headers=self._headers(), timeout=self.timeout
|
|
)
|
|
if resp.status_code >= 400:
|
|
raise PnmsClientError(
|
|
f'PNMS {path} HTTP {resp.status_code}: {resp.text[:500]}'
|
|
)
|
|
if not resp.content:
|
|
return {}
|
|
return resp.json()
|
|
|
|
def push_employees(self, employees: list[dict]) -> dict:
|
|
return self.post('/api/sync/from-acore/employees/', {'employees': employees})
|
|
|
|
def push_pharmacies(self, pharmacies: list[dict]) -> dict:
|
|
return self.post('/api/sync/from-acore/pharmacies/', {'pharmacies': pharmacies})
|