-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_server.py
More file actions
110 lines (90 loc) · 3.71 KB
/
api_server.py
File metadata and controls
110 lines (90 loc) · 3.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0 OR Commercial
"""Example: Tidewatch as a REST scoring API.
Minimal HTTP server using stdlib only. Production deployments should
use FastAPI, Flask, or similar.
Usage:
python examples/api_server.py
curl -X POST http://localhost:8090/score -H "Content-Type: application/json" -d @examples/sample_payload.json
Endpoints:
POST /score — score a batch of obligations, return ranked results
GET /health — health check
"""
from __future__ import annotations
import json
from datetime import UTC, datetime, timedelta
from http.server import BaseHTTPRequestHandler, HTTPServer
from tidewatch import Obligation, recalculate_batch
PORT = 8090
class TidewatchHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self._respond(200, {"status": "ok", "version": "0.4.4"})
else:
self._respond(404, {"error": "not found"})
def do_POST(self):
if self.path != "/score":
self._respond(404, {"error": "not found"})
return
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
payload = json.loads(body)
except json.JSONDecodeError:
self._respond(400, {"error": "invalid JSON"})
return
now = datetime.now(UTC)
obligations = []
for item in payload.get("obligations", []):
due_str = item.get("due_date")
due = datetime.fromisoformat(due_str) if due_str else now + timedelta(days=7)
obligations.append(Obligation(
id=item.get("id", 0),
title=item.get("title", ""),
due_date=due,
materiality=item.get("materiality", "routine"),
dependency_count=item.get("dependency_count", 0),
completion_pct=item.get("completion_pct", 0.0),
domain=item.get("domain"),
))
results = recalculate_batch(obligations, now=now)
response = {
"scored_at": now.isoformat(),
"count": len(results),
"results": [
{
"obligation_id": r.obligation_id,
"pressure": round(r.pressure, 6),
"zone": r.zone,
"factors": {
"time_pressure": round(r.time_pressure, 6),
"materiality": round(r.materiality_mult, 2),
"dependency_amp": round(r.dependency_amp, 6),
"completion_damp": round(r.completion_damp, 6),
},
}
for r in results
],
}
self._respond(200, response)
def _respond(self, code: int, data: dict):
body = json.dumps(data, indent=2).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
# Quiet logging
pass
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), TidewatchHandler)
print(f"Tidewatch scoring API running on http://localhost:{PORT}")
print(" POST /score — score obligations")
print(" GET /health — health check")
print()
print("Example:")
print(f' curl -X POST http://localhost:{PORT}/score \\')
print(' -H "Content-Type: application/json" \\')
print(' -d \'{"obligations": [{"id": 1, "title": "Test", "due_date": "2026-06-15T12:00:00+00:00", "materiality": "material", "dependency_count": 5}]}\'')
server.serve_forever()