Add LogStorm attendance client

This commit is contained in:
lizerk 2026-04-21 13:40:29 +03:00
parent 9719b65b58
commit d5e4f6d577
4 changed files with 205 additions and 0 deletions

View File

@ -42,3 +42,8 @@ NODE_ENV=production
# Other Settings
# SITE_URL=http://localhost:8000
# LogStorm attendance analyzer
LOGSTORM_API_URL=http://localhost:8000
LOGSTORM_API_TOKEN=change-me
LOGSTORM_TIMEOUT_SECONDS=30

View File

@ -0,0 +1,77 @@
"""Client for the external LogStorm attendance analyzer API."""
from dataclasses import dataclass
from typing import Any, Optional
import requests
from django.conf import settings
class LogStormClientError(RuntimeError):
"""Raised when LogStorm cannot process an attendance request."""
@dataclass(frozen=True)
class LogStormConfig:
base_url: str
token: str = ""
timeout: int = 30
def load_logstorm_config() -> LogStormConfig:
return LogStormConfig(
base_url=getattr(
settings, "LOGSTORM_API_URL", "http://127.0.0.1:8000"
).rstrip("/"),
token=getattr(settings, "LOGSTORM_API_TOKEN", ""),
timeout=getattr(settings, "LOGSTORM_TIMEOUT_SECONDS", 30),
)
class LogStormClient:
"""Small synchronous client for EUSRR -> LogStorm calls."""
def __init__(
self,
config: Optional[LogStormConfig] = None,
session: Optional[requests.Session] = None,
):
self.config = config or load_logstorm_config()
self.session = session or requests.Session()
def analyze_attendance(self, payload: dict[str, Any]) -> dict[str, Any]:
response = self.session.post(
self._url("/attendance/analyze"),
json=payload,
headers=self._headers(),
timeout=self.config.timeout,
)
if not response.ok:
raise LogStormClientError(
f"LogStorm API returned HTTP {response.status_code}: "
f"{response.text}"
)
try:
return response.json()
except ValueError as exc:
raise LogStormClientError("LogStorm API returned invalid JSON") from exc
def health(self) -> bool:
try:
response = self.session.get(
self._url("/health"),
headers=self._headers(),
timeout=self.config.timeout,
)
except requests.RequestException:
return False
return response.ok
def _url(self, path: str) -> str:
return f"{self.config.base_url}/{path.lstrip('/')}"
def _headers(self) -> dict[str, str]:
headers = {"Accept": "application/json"}
if self.config.token:
headers["Authorization"] = f"Bearer {self.config.token}"
return headers

View File

@ -203,6 +203,11 @@ LOGIN_REDIRECT_URL = "/admin/"
# Используем локальный адрес для избежания проблем с SSL
API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:9000/api")
# LogStorm attendance analyzer integration.
LOGSTORM_API_URL = os.getenv("LOGSTORM_API_URL", "http://127.0.0.1:8000")
LOGSTORM_API_TOKEN = os.getenv("LOGSTORM_API_TOKEN", "")
LOGSTORM_TIMEOUT_SECONDS = int(os.getenv("LOGSTORM_TIMEOUT_SECONDS", "30"))
PHONE_DEFAULT_REGION = os.getenv("PHONE_DEFAULT_REGION", "RU")
PHONENUMBER_DEFAULT_REGION = "RU"

View File

@ -0,0 +1,118 @@
import pytest
from common.logstorm_client import (
LogStormClient,
LogStormClientError,
LogStormConfig,
load_logstorm_config,
)
class FakeResponse:
def __init__(self, status_code=200, payload=None, text=""):
self.status_code = status_code
self._payload = payload
self.text = text
self.ok = 200 <= status_code < 400
def json(self):
if isinstance(self._payload, Exception):
raise self._payload
return self._payload
class FakeSession:
def __init__(self, response):
self.response = response
self.requests = []
def post(self, url, **kwargs):
self.requests.append(("POST", url, kwargs))
return self.response
def get(self, url, **kwargs):
self.requests.append(("GET", url, kwargs))
return self.response
def _payload():
return {
"employee_id": "100",
"period_start": "2026-04-20",
"period_end": "2026-04-20",
"schedule": {
"start_time": "09:00",
"end_time": "18:00",
"expected_hours": 9,
"workdays": ["Monday"],
"date_overrides": [],
},
}
def test_load_logstorm_config_reads_settings(settings):
settings.LOGSTORM_API_URL = "http://logstorm:8000/"
settings.LOGSTORM_API_TOKEN = "secret"
settings.LOGSTORM_TIMEOUT_SECONDS = 12
config = load_logstorm_config()
assert config.base_url == "http://logstorm:8000"
assert config.token == "secret"
assert config.timeout == 12
def test_analyze_attendance_posts_payload_with_bearer_token():
session = FakeSession(FakeResponse(payload={"records": []}))
client = LogStormClient(
LogStormConfig(
base_url="http://logstorm:8000",
token="secret",
timeout=7,
),
session=session,
)
result = client.analyze_attendance(_payload())
assert result == {"records": []}
method, url, kwargs = session.requests[0]
assert method == "POST"
assert url == "http://logstorm:8000/attendance/analyze"
assert kwargs["json"]["employee_id"] == "100"
assert kwargs["headers"]["Authorization"] == "Bearer secret"
assert kwargs["timeout"] == 7
def test_analyze_attendance_raises_on_http_error():
client = LogStormClient(
LogStormConfig(base_url="http://logstorm:8000"),
session=FakeSession(FakeResponse(status_code=500, text="boom")),
)
with pytest.raises(LogStormClientError, match="HTTP 500"):
client.analyze_attendance(_payload())
def test_analyze_attendance_raises_on_invalid_json():
client = LogStormClient(
LogStormConfig(base_url="http://logstorm:8000"),
session=FakeSession(FakeResponse(payload=ValueError("bad json"))),
)
with pytest.raises(LogStormClientError, match="invalid JSON"):
client.analyze_attendance(_payload())
def test_health_returns_boolean():
ok_client = LogStormClient(
LogStormConfig(base_url="http://logstorm:8000"),
session=FakeSession(FakeResponse(status_code=200, payload={})),
)
bad_client = LogStormClient(
LogStormConfig(base_url="http://logstorm:8000"),
session=FakeSession(FakeResponse(status_code=503, payload={})),
)
assert ok_client.health() is True
assert bad_client.health() is False