Add LogStorm attendance client
This commit is contained in:
parent
9719b65b58
commit
d5e4f6d577
@ -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
|
||||
|
||||
77
backend/common/logstorm_client.py
Normal file
77
backend/common/logstorm_client.py
Normal 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
|
||||
@ -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"
|
||||
|
||||
|
||||
118
backend/tests/integration/test_logstorm_client.py
Normal file
118
backend/tests/integration/test_logstorm_client.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user