Thoth (tht) è il prodotto, PSD è il cliente. Nessun riferimento al contesto
clinico nel codice.
Rinomine:
- comando+package nsp→tht (dir nsp/→tht/, 46 import, pyproject entry point)
- gate nsp-gate.js→tht-gate.js (+ rewrite token, relayIfNspFails→relayIfThtFails)
- workspace chirone.{example,test}.yaml→tht.{example,test}.yaml (generici)
- env THOTH_→THT_ (19 var) + NSP_ stragglers (NSP_HARNESS_ROOT, NSP_SESSION)
- commenti/docstring chirone/psdwp3/policlinico neutralizzati ('the reference
implementation', 'the DWH')
Aggiunto [tool.setuptools.packages.find] include=['tht*'] (necessario: l'auto-
discovery rompeva con tht/ + workspaces/ come top-level multipli).
.env operatore aggiornato in-place (prefissi THT_, valori preservati, gitignored).
Verifica: pytest 109 passed, npm test 14 pass, tht phase meta --json OK, zero
residui nsp/THOTH_/NSP_/chirone nel package.
116 lines
4.0 KiB
Python
116 lines
4.0 KiB
Python
"""L1: rest/client RPC contract (mocked transport — no network).
|
|
|
|
Verifies the ported RestClient: each rpc carries the X-API-Key header, the right
|
|
payload, base_url slash handling, and HTTP/network errors surface as RestError.
|
|
Ported (renamed psdwp3->tht) from ChironeWp3/tests/test_rest_client.py.
|
|
"""
|
|
import pytest
|
|
import requests
|
|
|
|
from tht.config import RestConfig
|
|
from tht.rest.client import RestClient, RestError
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, payload=None, status=200, text=None):
|
|
self._payload = payload
|
|
self.status_code = status
|
|
self.text = text if text is not None else ("" if payload is None else "json")
|
|
|
|
@property
|
|
def ok(self):
|
|
return self.status_code < 400
|
|
|
|
def json(self):
|
|
if self._payload is None:
|
|
raise ValueError("no json")
|
|
return self._payload
|
|
|
|
|
|
def _client(timeout=30):
|
|
return RestClient(RestConfig(base_url="https://h/dwh/", api_key="dwh_k", timeout=timeout))
|
|
|
|
|
|
def _capture(monkeypatch, response):
|
|
calls = []
|
|
|
|
def fake_post(url, json=None, headers=None, timeout=None, verify=None):
|
|
calls.append({"url": url, "json": json, "headers": headers,
|
|
"timeout": timeout, "verify": verify})
|
|
return response
|
|
|
|
monkeypatch.setattr("tht.rest.client.requests.post", fake_post)
|
|
return calls
|
|
|
|
|
|
def test_run_query_payload_and_rows(monkeypatch):
|
|
calls = _capture(monkeypatch, FakeResponse([{"x": 1}]))
|
|
rows = _client().run_query("SELECT 1 AS x")
|
|
assert rows == [{"x": 1}]
|
|
c = calls[0]
|
|
assert c["url"] == "https://h/dwh/rpc/run_query"
|
|
assert c["json"] == {"query_text": "SELECT 1 AS x"}
|
|
assert c["headers"]["X-API-Key"] == "dwh_k"
|
|
assert c["timeout"] == 30
|
|
|
|
|
|
def test_explain_query_maps_lines(monkeypatch):
|
|
plan = [
|
|
{"line": "Aggregate (cost=3747.24..3747.25 rows=1 width=8)"},
|
|
{"line": " -> Seq Scan on dim_patient (cost=0.00..3712.97 rows=13706 width=0)"},
|
|
]
|
|
_capture(monkeypatch, FakeResponse(plan))
|
|
lines = _client().explain_query("SELECT count(*) FROM dim_patient")
|
|
assert lines == [
|
|
"Aggregate (cost=3747.24..3747.25 rows=1 width=8)",
|
|
" -> Seq Scan on dim_patient (cost=0.00..3712.97 rows=13706 width=0)",
|
|
]
|
|
|
|
|
|
def test_ping(monkeypatch):
|
|
calls = _capture(monkeypatch, FakeResponse({"db_connected": True, "role": "postgres"}))
|
|
out = _client().ping()
|
|
assert out["db_connected"] is True
|
|
assert calls[0]["url"] == "https://h/dwh/rpc/ping"
|
|
assert calls[0]["json"] == {}
|
|
|
|
|
|
def test_top_values_payload(monkeypatch):
|
|
calls = _capture(monkeypatch, FakeResponse([{"value": "MI", "count": 13737}]))
|
|
out = _client().top_values("datawarehouse", "dim_patient", "provincia", 1000)
|
|
assert out == [{"value": "MI", "count": 13737}]
|
|
assert calls[0]["json"] == {
|
|
"schema_name": "datawarehouse",
|
|
"table_name": "dim_patient",
|
|
"column_name": "provincia",
|
|
"max_values": 1000,
|
|
}
|
|
|
|
|
|
def test_validate_select_ok_and_write(monkeypatch):
|
|
_capture(monkeypatch, FakeResponse(status=204))
|
|
assert _client().validate_select("SELECT 1") is True
|
|
_capture(monkeypatch, FakeResponse({"message": "Only SELECT / WITH statements are allowed"}, status=400))
|
|
assert _client().validate_select("DELETE FROM t") is False
|
|
|
|
|
|
def test_http_error_surfaces_message(monkeypatch):
|
|
_capture(monkeypatch, FakeResponse({"message": "boom"}, status=400))
|
|
with pytest.raises(RestError, match="boom"):
|
|
_client().run_query("SELECT bad")
|
|
|
|
|
|
def test_network_error_actionable(monkeypatch):
|
|
def boom(*a, **k):
|
|
raise requests.ConnectionError("refused")
|
|
|
|
monkeypatch.setattr("tht.rest.client.requests.post", boom)
|
|
with pytest.raises(RestError, match="raggiungibile"):
|
|
_client().ping()
|
|
|
|
|
|
def test_base_url_without_trailing_slash(monkeypatch):
|
|
calls = _capture(monkeypatch, FakeResponse({"db_connected": True}))
|
|
RestClient(RestConfig(base_url="https://h/dwh", api_key="k")).ping()
|
|
assert calls[0]["url"] == "https://h/dwh/rpc/ping"
|