209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Client Turni-Live — turni.loogle.it (JWT Bearer)."""
|
|
|
|
import logging
|
|
import os
|
|
import threading
|
|
import time
|
|
from typing import Any, Optional
|
|
from urllib.parse import urljoin
|
|
|
|
import httpx
|
|
|
|
LOGGER = logging.getLogger("loogle_mcp.turni")
|
|
|
|
MCP_USERS = ("daniele", "lucia", "davide", "luca")
|
|
MCP_TO_SERVICE_USER = {
|
|
"daniele": "daniely",
|
|
"lucia": "lucia",
|
|
"davide": "davide",
|
|
"luca": "luca",
|
|
}
|
|
|
|
PUBLIC_URL = os.environ.get("TURNI_URL", "https://turni.loogle.it").rstrip("/")
|
|
API_URL = os.environ.get("TURNI_API_URL", PUBLIC_URL).rstrip("/")
|
|
|
|
_jwt_cache: dict[str, tuple[str, float]] = {}
|
|
_jwt_lock = threading.Lock()
|
|
JWT_TTL = 3600 * 6
|
|
|
|
|
|
def _service_username(mcp_username: str) -> str:
|
|
return MCP_TO_SERVICE_USER.get(mcp_username.lower(), mcp_username.lower())
|
|
|
|
|
|
def _password_for_user(mcp_username: str) -> Optional[str]:
|
|
user = mcp_username.lower()
|
|
pwd = os.environ.get(f"TURNI_PASSWORD_{user.upper()}", "").strip()
|
|
if pwd:
|
|
return pwd
|
|
return os.environ.get("TURNI_PASSWORD", "").strip() or None
|
|
|
|
|
|
def _jwt_for_user(mcp_username: str) -> Optional[str]:
|
|
user = mcp_username.lower()
|
|
direct = os.environ.get(f"TURNI_JWT_{user.upper()}", "").strip()
|
|
if direct:
|
|
return direct
|
|
return os.environ.get("TURNI_JWT", "").strip() or None
|
|
|
|
|
|
def is_configured(username: Optional[str] = None) -> bool:
|
|
user = (username or "daniele").lower()
|
|
if _jwt_for_user(user):
|
|
return True
|
|
if _password_for_user(user):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _store_jwt(mcp_username: str, token: str) -> None:
|
|
with _jwt_lock:
|
|
_jwt_cache[mcp_username.lower()] = (token, time.time() + JWT_TTL)
|
|
|
|
|
|
def _cached_jwt(mcp_username: str) -> Optional[str]:
|
|
with _jwt_lock:
|
|
row = _jwt_cache.get(mcp_username.lower())
|
|
if not row:
|
|
return None
|
|
token, expires = row
|
|
if time.time() > expires:
|
|
_jwt_cache.pop(mcp_username.lower(), None)
|
|
return None
|
|
return token
|
|
|
|
|
|
def login(mcp_username: str) -> str:
|
|
cached = _cached_jwt(mcp_username)
|
|
if cached:
|
|
return cached
|
|
preset = _jwt_for_user(mcp_username)
|
|
if preset:
|
|
_store_jwt(mcp_username, preset)
|
|
return preset
|
|
password = _password_for_user(mcp_username)
|
|
if not password:
|
|
raise RuntimeError(
|
|
f"Turni non configurato per {mcp_username}. "
|
|
f"Imposta TURNI_PASSWORD_{mcp_username.upper()} o TURNI_JWT_{mcp_username.upper()}"
|
|
)
|
|
service_user = _service_username(mcp_username)
|
|
verify = os.environ.get("TURNI_VERIFY_SSL", "true").strip().lower() not in (
|
|
"0", "false", "no", "off",
|
|
)
|
|
url = urljoin(API_URL + "/", "api/auth/login")
|
|
with httpx.Client(timeout=30.0, verify=verify) as client:
|
|
resp = client.post(url, json={"username": service_user, "password": password})
|
|
if resp.status_code != 200:
|
|
raise RuntimeError(f"Login Turni fallito: HTTP {resp.status_code}")
|
|
data = resp.json()
|
|
token = data.get("token")
|
|
if not token:
|
|
raise RuntimeError("Login Turni: token JWT mancante")
|
|
_store_jwt(mcp_username, token)
|
|
return token
|
|
|
|
|
|
def _request(
|
|
method: str,
|
|
path: str,
|
|
*,
|
|
mcp_username: str,
|
|
params: Optional[dict] = None,
|
|
) -> Any:
|
|
token = login(mcp_username)
|
|
verify = os.environ.get("TURNI_VERIFY_SSL", "true").strip().lower() not in (
|
|
"0", "false", "no", "off",
|
|
)
|
|
url = path if path.startswith("http") else urljoin(API_URL + "/", path.lstrip("/"))
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
with httpx.Client(timeout=60.0, verify=verify) as client:
|
|
resp = client.request(method, url, headers=headers, params=params)
|
|
if resp.status_code == 401:
|
|
with _jwt_lock:
|
|
_jwt_cache.pop(mcp_username.lower(), None)
|
|
headers["Authorization"] = f"Bearer {login(mcp_username)}"
|
|
resp = client.request(method, url, headers=headers, params=params)
|
|
if resp.status_code >= 400:
|
|
raise RuntimeError(f"Turni {path}: HTTP {resp.status_code} {resp.text[:200]}")
|
|
return resp.json()
|
|
|
|
|
|
def get_status() -> dict:
|
|
verify = os.environ.get("TURNI_VERIFY_SSL", "true").strip().lower() not in (
|
|
"0", "false", "no", "off",
|
|
)
|
|
url = urljoin(API_URL + "/", "api/status")
|
|
with httpx.Client(timeout=30.0, verify=verify) as client:
|
|
resp = client.get(url)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def list_doctors(mcp_username: str) -> Any:
|
|
return _request("GET", "/api/doctors", mcp_username=mcp_username)
|
|
|
|
|
|
def get_shift_assignments(
|
|
mcp_username: str,
|
|
*,
|
|
from_date: Optional[str] = None,
|
|
to_date: Optional[str] = None,
|
|
limit: int = 100,
|
|
) -> Any:
|
|
params: dict = {}
|
|
if from_date:
|
|
params["from"] = from_date
|
|
if to_date:
|
|
params["to"] = to_date
|
|
data = _request("GET", "/api/shift-assignments", mcp_username=mcp_username, params=params or None)
|
|
if isinstance(data, list):
|
|
return data[:limit]
|
|
if isinstance(data, dict):
|
|
items = data.get("assignments") or data.get("items") or data.get("results")
|
|
if isinstance(items, list):
|
|
return items[:limit]
|
|
return data
|
|
|
|
|
|
def get_my_shifts(
|
|
mcp_username: str,
|
|
*,
|
|
from_date: Optional[str] = None,
|
|
to_date: Optional[str] = None,
|
|
limit: int = 50,
|
|
) -> dict:
|
|
"""Turni dell'utente MCP: filtra per doctorId collegato o per nome medico."""
|
|
user_info = _request("GET", "/api/users/me", mcp_username=mcp_username)
|
|
doctor_id = user_info.get("doctorId")
|
|
assignments = get_shift_assignments(
|
|
mcp_username, from_date=from_date, to_date=to_date, limit=500,
|
|
)
|
|
if not isinstance(assignments, list):
|
|
return {"user": user_info, "assignments": assignments}
|
|
if doctor_id:
|
|
mine = [a for a in assignments if a.get("doctorId") == doctor_id or a.get("doctor_id") == doctor_id]
|
|
else:
|
|
service_user = _service_username(mcp_username)
|
|
doctors = list_doctors(mcp_username)
|
|
doc_ids = set()
|
|
if isinstance(doctors, list):
|
|
for doc in doctors:
|
|
name = (doc.get("name") or doc.get("fullName") or "").lower()
|
|
if service_user.lower() in name or mcp_username.lower() in name:
|
|
doc_ids.add(doc.get("id") or doc.get("doctorId"))
|
|
mine = [
|
|
a for a in assignments
|
|
if (a.get("doctorId") or a.get("doctor_id")) in doc_ids
|
|
] if doc_ids else assignments[:limit]
|
|
return {
|
|
"user": {
|
|
"username": user_info.get("username"),
|
|
"role": user_info.get("role"),
|
|
"doctorId": doctor_id,
|
|
},
|
|
"assignments": mine[:limit],
|
|
"count": len(mine),
|
|
}
|