import json import mimetypes from pathlib import Path from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Optional from urllib.parse import parse_qs, urlparse from src.api.broker_state import get_broker_state from src.api.candles import get_recent_candles from src.api.execution_tickets import get_recent_execution_tickets from src.api.health import get_health from src.api.live_operation_audits import get_recent_live_operation_audits from src.api.trade_plan_candidates import get_recent_trade_plan_candidates from src.api.trading_runtime import ( activate_trading_plan, build_ticket_from_candidate, cancel_execution, cancel_trading_plan, close_execution, confirm_ticket_only, execute_ticket, generate_trading_journal, generate_trading_plan, get_trading_decision_alerts, get_trading_decision_packet, get_latest_trading_plan, get_trading_candidate_by_id, get_trading_market_state, get_trading_operator_notes, get_trading_plan_by_id, get_trading_review_packet, get_trading_live_readiness, get_trading_runtime_status, get_trading_session_view, get_trading_ticket_by_id, get_trading_trust_score, get_trading_executions, get_weekly_review, rerun_ticket_risk_check, run_candidate_confirm_check, create_trading_operator_note, run_trading_reconciliation, run_trading_runtime_once, run_trading_watch, sync_execution, ) from src.api.trading_journal import get_recent_trading_journal, get_trading_journal_summary from src.api.trading_plans import get_recent_trading_plans from src.api.training import ( add_training_annotation, create_training_attempt, export_training_case_package, get_training_case, get_training_cases, get_training_progress, import_training_case_package, parse_min_quality_score_query, parse_quality_rank_query, parse_quality_sort_query, submit_training_attempt, ) from src.services.execution import ExecutionService from src.services.market_state import start_market_truth_runtime from src.services.runtime import start_trading_runtime_service def build_api_dependencies() -> dict: return { "health": get_health, "broker_state": get_broker_state, "candles": get_recent_candles, "execution_tickets": get_recent_execution_tickets, "live_operation_audits": get_recent_live_operation_audits, "trade_plan_candidates": get_recent_trade_plan_candidates, "trading_plan_generate": generate_trading_plan, "trading_plan_latest": get_latest_trading_plan, "trading_plan_by_id": get_trading_plan_by_id, "trading_plan_activate": activate_trading_plan, "trading_plan_cancel": cancel_trading_plan, "trading_market_state": get_trading_market_state, "trading_decision_packet": get_trading_decision_packet, "trading_decision_alerts": get_trading_decision_alerts, "trading_reconciliation_run": run_trading_reconciliation, "trading_runtime_status": get_trading_runtime_status, "trading_runtime_run_once": run_trading_runtime_once, "trading_trust_score": get_trading_trust_score, "trading_session_view": get_trading_session_view, "trading_operator_notes": get_trading_operator_notes, "trading_operator_note_create": create_trading_operator_note, "trading_review_packet": get_trading_review_packet, "trading_live_readiness": get_trading_live_readiness, "trading_watch_run": run_trading_watch, "trading_candidate_by_id": get_trading_candidate_by_id, "trading_candidate_confirm_check": run_candidate_confirm_check, "trading_candidate_build_ticket": build_ticket_from_candidate, "trading_ticket_by_id": get_trading_ticket_by_id, "trading_executions": get_trading_executions, "trading_ticket_risk_check": rerun_ticket_risk_check, "trading_ticket_confirm": confirm_ticket_only, "trading_ticket_execute": execute_ticket, "trading_execution_cancel": cancel_execution, "trading_execution_close": close_execution, "trading_execution_sync": sync_execution, "trading_journal": get_recent_trading_journal, "trading_journal_summary": get_trading_journal_summary, "trading_journal_generate": generate_trading_journal, "trading_weekly_review": get_weekly_review, "trading_plans": get_recent_trading_plans, "training_cases": get_training_cases, "training_case": get_training_case, "training_export_cases": export_training_case_package, "training_import_cases": import_training_case_package, "training_create_attempt": create_training_attempt, "training_add_annotation": add_training_annotation, "training_submit_attempt": submit_training_attempt, "training_progress": get_training_progress, } STATIC_DIR = Path(__file__).resolve().parents[1] / "web" MAX_TRAINING_LIMIT = 100 def route_request( path: str, query: dict, dependencies: Optional[dict] = None, method: str = "GET", body: Optional[dict] = None, ) -> tuple[int, dict]: method = method.upper() path_parts = _path_parts(path) if method == "POST": _validate_post_route_path(path) if _is_training_progress_post(method, path): _validate_progress_identifier_body(body or {}) return 404, {"error": "not_found"} if _is_training_attempt_create_post(method, path): _validate_attempt_create_body(body or {}) deps = dependencies or build_api_dependencies() if method == "GET" and path == "/health": return 200, deps["health"]() if method == "GET" and path == "/trading/broker-state": instrument_id = _require_int(query, "instrument_id") return 200, deps["broker_state"](instrument_id=instrument_id) if method == "GET" and path == "/candles": instrument_id = _require_int(query, "instrument_id") timeframe = _get_single(query, "timeframe", "1m") limit = _get_int(query, "limit", 100) return 200, {"items": deps["candles"](instrument_id=instrument_id, timeframe=timeframe, limit=limit)} if method == "POST" and path == "/trading/plans/generate": instrument_id = _require_json_int(body or {}, "instrument_id") return 200, deps["trading_plan_generate"]( instrument_id=instrument_id, bias_tf=_get_body_value(body, "bias_tf", "1m"), setup_tf=_get_body_value(body, "setup_tf", "1m"), trigger_tf=_get_body_value(body, "trigger_tf", "1m"), activate=_get_body_bool(body, "activate", False), ) if method == "GET" and path == "/trading/plans": instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["trading_plans"](instrument_id=instrument_id, limit=limit)} if method == "GET" and path == "/trading/plans/latest": instrument_id = _require_int(query, "instrument_id") plan = deps["trading_plan_latest"](instrument_id=instrument_id) if plan is None: return 404, {"error": "not_found"} return 200, plan if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "plans"]: plan = deps["trading_plan_by_id"](_parse_path_int(path_parts[2], "plan_id")) if plan is None: return 404, {"error": "not_found"} return 200, plan if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "plans"] and path_parts[3] == "activate": plan = deps["trading_plan_activate"](_parse_path_int(path_parts[2], "plan_id")) if plan is None: return 404, {"error": "not_found"} return 200, plan if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "plans"] and path_parts[3] == "cancel": plan = deps["trading_plan_cancel"](_parse_path_int(path_parts[2], "plan_id")) if plan is None: return 404, {"error": "not_found"} return 200, plan if method == "GET" and path == "/trading/market-state": instrument_id = _require_int(query, "instrument_id") timeframe = _get_single(query, "timeframe", "1m") return 200, deps["trading_market_state"](instrument_id=instrument_id, timeframe=timeframe) if method == "GET" and path == "/trading/runtime-status": return 200, deps["trading_runtime_status"]() if method == "POST" and path == "/trading/runtime/run-once": return 200, deps["trading_runtime_run_once"]() if method == "GET" and path == "/trading/decision-packet": instrument_id = _require_int(query, "instrument_id") return 200, deps["trading_decision_packet"](instrument_id=instrument_id) if method == "GET" and path == "/trading/alerts": instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["trading_decision_alerts"](instrument_id=instrument_id, limit=limit)} if method == "POST" and path == "/trading/reconcile": instrument_id = _require_json_int(body or {}, "instrument_id") return 200, deps["trading_reconciliation_run"](instrument_id=instrument_id) if method == "GET" and path == "/trading/trust-score": instrument_id = _require_int(query, "instrument_id") return 200, deps["trading_trust_score"](instrument_id=instrument_id) if method == "GET" and path == "/trading/session-view": instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 12) return 200, deps["trading_session_view"](instrument_id=instrument_id, limit=limit) if method == "GET" and path == "/trading/operator-notes": instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["trading_operator_notes"](instrument_id=instrument_id, limit=limit)} if method == "POST" and path == "/trading/operator-notes": instrument_id = _require_json_int(body or {}, "instrument_id") return 201, deps["trading_operator_note_create"]( instrument_id=instrument_id, note_scope=_get_body_value(body, "note_scope", "general"), author=_get_body_value(body, "author", "local_operator"), body=_get_body_value(body, "body", ""), session_code=_get_body_value(body, "session_code"), execution_id=_get_body_optional_int(body, "execution_id"), signal_id=_get_body_optional_int(body, "signal_id"), candidate_id=_get_body_optional_int(body, "candidate_id"), decision_packet_id=_get_body_optional_int(body, "decision_packet_id"), tags=_get_body_list(body, "tags"), ) if method == "GET" and path == "/trading/review-packet": instrument_id = _require_int(query, "instrument_id") return 200, deps["trading_review_packet"](instrument_id=instrument_id) if method == "GET" and path == "/trading/live-readiness": instrument_id = _require_int(query, "instrument_id") timeframe = _get_single(query, "timeframe", "1m") return 200, deps["trading_live_readiness"](instrument_id=instrument_id, timeframe=timeframe) if method == "POST" and path == "/trading/watch/run": instrument_id = _require_json_int(body or {}, "instrument_id") candidate = deps["trading_watch_run"]( instrument_id=instrument_id, setup_tf=_get_body_value(body, "setup_tf", "1m"), trigger_tf=_get_body_value(body, "trigger_tf", "1m"), ) return 200, candidate or {"candidate_generated": False} if method == "GET" and path == "/trading/candidates": instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["trade_plan_candidates"](instrument_id=instrument_id, limit=limit)} if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "candidates"]: candidate = deps["trading_candidate_by_id"](_parse_path_int(path_parts[2], "candidate_id")) if candidate is None: return 404, {"error": "not_found"} return 200, candidate if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "candidates"] and path_parts[3] == "confirm-check": payload = deps["trading_candidate_confirm_check"](_parse_path_int(path_parts[2], "candidate_id")) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "candidates"] and path_parts[3] == "build-ticket": payload = deps["trading_candidate_build_ticket"]( candidate_id=_parse_path_int(path_parts[2], "candidate_id"), execution_mode=_get_body_value(body, "execution_mode", "semi_auto"), account_equity=_get_body_float(body, "account_equity", 10000.0), risk_pct=_get_body_float(body, "risk_pct", 0.01), ) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "GET" and path == "/trading/journal": instrument_id = _get_optional_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["trading_journal"](instrument_id=instrument_id, limit=limit)} if method == "GET" and path == "/trading/journal/summary": instrument_id = _get_optional_int(query, "instrument_id") return 200, deps["trading_journal_summary"](instrument_id=instrument_id) if method == "GET" and path == "/trading/research/weekly-review": instrument_id = _get_optional_int(query, "instrument_id") return 200, deps["trading_weekly_review"](instrument_id=instrument_id) if method == "POST" and path == "/trading/journal/generate": execution_id = _get_body_optional_int(body, "execution_id") return 200, deps["trading_journal_generate"](execution_id=execution_id) if method == "GET" and path == "/trading/executions": instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["trading_executions"](instrument_id=instrument_id, limit=limit)} if method == "GET" and path == "/trading/tickets": instrument_id = _get_optional_int(query, "instrument_id") signal_id = _get_optional_int(query, "signal_id") if instrument_id is None and signal_id is None: raise ValueError("missing_required_query_param:instrument_id_or_signal_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["execution_tickets"](instrument_id=instrument_id, signal_id=signal_id, limit=limit)} if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "tickets"]: ticket = deps["trading_ticket_by_id"](_parse_path_int(path_parts[2], "ticket_id")) if ticket is None: return 404, {"error": "not_found"} return 200, ticket if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "risk-check": payload = deps["trading_ticket_risk_check"]( _parse_path_int(path_parts[2], "ticket_id"), account_equity=_get_body_float(body, "account_equity", 10000.0), risk_pct=_get_body_float(body, "risk_pct", 0.01), ) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "confirm": payload = deps["trading_ticket_confirm"]( _parse_path_int(path_parts[2], "ticket_id"), confirmed_by=_get_body_value(body, "confirmed_by", "local_operator"), ) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "paper-execute": payload = deps["trading_ticket_execute"]( _parse_path_int(path_parts[2], "ticket_id"), confirmed_by=_get_body_value(body, "confirmed_by", "local_operator"), execution_mode_expected="paper", ) return 200, payload if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "semi-auto-submit": payload = deps["trading_ticket_execute"]( _parse_path_int(path_parts[2], "ticket_id"), confirmed_by=_get_body_value(body, "confirmed_by", "local_operator"), execution_mode_expected="semi_auto", ) return 200, payload if method == "GET" and path == "/live-operation-audits": instrument_id = _get_optional_int(query, "instrument_id") execution_ticket_id = _get_optional_int(query, "execution_ticket_id") if instrument_id is None and execution_ticket_id is None: raise ValueError("missing_required_query_param:instrument_id_or_execution_ticket_id") limit = _get_int(query, "limit", 20) return 200, { "items": deps["live_operation_audits"]( instrument_id=instrument_id, execution_ticket_id=execution_ticket_id, limit=limit, ) } if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "executions"] and path_parts[3] == "cancel": payload = deps["trading_execution_cancel"](_parse_path_int(path_parts[2], "execution_id")) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "executions"] and path_parts[3] == "close": payload = deps["trading_execution_close"](_parse_path_int(path_parts[2], "execution_id")) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "executions"] and path_parts[3] == "sync": payload = deps["trading_execution_sync"](_parse_path_int(path_parts[2], "execution_id")) if payload is None: return 404, {"error": "not_found"} return 200, payload if method == "GET" and path == "/training/cases": status = _get_single(query, "status") skill_tag = _get_single(query, "skill_tag") instrument_id = _get_optional_int(query, "instrument_id") limit = _get_limit(query, "limit", 100, max_value=MAX_TRAINING_LIMIT) quality_rank = parse_quality_rank_query(_get_single(query, "quality_rank")) min_quality_score = parse_min_quality_score_query(_get_single(query, "min_quality_score")) sort_by = parse_quality_sort_query(_get_single(query, "sort_by")) case_kwargs = { "status": status, "skill_tag": skill_tag, "instrument_id": instrument_id, "limit": limit, } if quality_rank is not None: case_kwargs["quality_rank"] = quality_rank if min_quality_score is not None: case_kwargs["min_quality_score"] = min_quality_score if sort_by is not None: case_kwargs["sort_by"] = sort_by return 200, { "items": deps["training_cases"](**case_kwargs) } if method == "GET" and path == "/training/cases/export": status = _get_single(query, "status") skill_tag = _get_single(query, "skill_tag") instrument_id = _get_optional_int(query, "instrument_id") limit = _get_limit(query, "limit", 100, max_value=MAX_TRAINING_LIMIT) quality_rank = parse_quality_rank_query(_get_single(query, "quality_rank")) min_quality_score = parse_min_quality_score_query(_get_single(query, "min_quality_score")) sort_by = parse_quality_sort_query(_get_single(query, "sort_by")) export_kwargs = { "status": status, "skill_tag": skill_tag, "instrument_id": instrument_id, "limit": limit, } if quality_rank is not None: export_kwargs["quality_rank"] = quality_rank if min_quality_score is not None: export_kwargs["min_quality_score"] = min_quality_score if sort_by is not None: export_kwargs["sort_by"] = sort_by return 200, deps["training_export_cases"](**export_kwargs) if method == "POST" and path == "/training/cases/import": return 201, deps["training_import_cases"](body or {}) if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["training", "cases"]: case_id = _parse_path_int(path_parts[2], "case_id") case = deps["training_case"](case_id=case_id) if case is None: return 404, {"error": "not_found"} return 200, case if method == "POST" and path == "/training/attempts": return 201, deps["training_create_attempt"](body or {}) if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["training", "attempts"] and path_parts[3] == "annotations": attempt_id = _parse_path_int(path_parts[2], "attempt_id") return 201, deps["training_add_annotation"](attempt_id, body or {}) if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["training", "attempts"] and path_parts[3] == "submit": attempt_id = _parse_path_int(path_parts[2], "attempt_id") return 200, deps["training_submit_attempt"](attempt_id, body or {}) if method == "GET" and path == "/training/progress": user_id = _get_single(query, "user_id", "local") skill_tag = _get_single(query, "skill_tag") limit = _get_limit(query, "limit", 100, max_value=MAX_TRAINING_LIMIT) return 200, {"items": deps["training_progress"](user_id=user_id, skill_tag=skill_tag, limit=limit)} return 404, {"error": "not_found"} class APIRequestHandler(BaseHTTPRequestHandler): dependencies = build_api_dependencies() def do_GET(self) -> None: parsed = urlparse(self.path) if _should_serve_static(parsed.path): self._serve_static(parsed.path) return try: status, payload = route_request(parsed.path, parse_qs(parsed.query, keep_blank_values=True), dependencies=self.dependencies) except ValueError as exc: status, payload = 400, {"error": str(exc)} except Exception: status, payload = 500, {"error": "internal_server_error"} self._send_json(status, payload) def do_POST(self) -> None: parsed = urlparse(self.path) try: _validate_post_route_path(parsed.path) body = _read_json_body(self) status, payload = route_request( parsed.path, parse_qs(parsed.query), dependencies=self.dependencies, method="POST", body=body, ) except ValueError as exc: status, payload = 400, {"error": str(exc)} except Exception: status, payload = 500, {"error": "internal_server_error"} self._send_json(status, payload) def _send_json(self, status: int, payload: dict) -> None: body = json.dumps(payload, default=str, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _serve_static(self, path: str) -> None: file_path = _resolve_static_file(path) if file_path is None or not file_path.exists() or not file_path.is_file(): self.send_response(404) self.end_headers() return body = file_path.read_bytes() mime_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream" self.send_response(200) self.send_header("Content-Type", f"{mime_type}; charset=utf-8" if mime_type.startswith("text/") or mime_type == "application/javascript" else mime_type) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, format: str, *args) -> None: return def serve_api(host: str = "127.0.0.1", port: int = 8000) -> None: start_market_truth_runtime() start_trading_runtime_service() server = ThreadingHTTPServer((host, port), APIRequestHandler) print(f"AI ICT API listening on http://{host}:{port}") server.serve_forever() def _require_int(query: dict, key: str) -> int: value = _get_single(query, key) if value is None: raise ValueError(f"missing_required_query_param:{key}") try: return int(value) except ValueError as exc: raise ValueError(f"invalid_int_query_param:{key}") from exc def _get_optional_int(query: dict, key: str) -> Optional[int]: value = _get_single(query, key) if value is None: return None try: return int(value) except ValueError as exc: raise ValueError(f"invalid_int_query_param:{key}") from exc def _get_int(query: dict, key: str, default: int) -> int: value = _get_single(query, key) if value is None: return default try: return int(value) except ValueError as exc: raise ValueError(f"invalid_int_query_param:{key}") from exc def _get_limit(query: dict, key: str, default: int, max_value: int) -> int: value = _get_single(query, key) if value is None: return default try: parsed = int(value) except ValueError as exc: raise ValueError(f"invalid_limit_query_param:{key}") from exc if parsed < 1 or parsed > max_value: raise ValueError(f"invalid_limit_query_param:{key}") return parsed def _get_body_value(body: Optional[dict], key: str, default=None): if not isinstance(body, dict): return default value = body.get(key) if value is None or value == "": return default return value def _get_body_bool(body: Optional[dict], key: str, default: bool) -> bool: value = _get_body_value(body, key, default) if isinstance(value, bool): return value return str(value).strip().lower() in {"1", "true", "yes", "on"} def _get_body_float(body: Optional[dict], key: str, default: float) -> float: value = _get_body_value(body, key, default) return float(value) def _get_body_optional_int(body: Optional[dict], key: str): if not isinstance(body, dict): return None value = body.get(key) if value is None or value == "": return None return int(value) def _get_body_list(body: Optional[dict], key: str) -> list: if not isinstance(body, dict): return [] value = body.get(key) if value is None: return [] if isinstance(value, list): return value return [value] def _get_single(query: dict, key: str, default: Optional[str] = None) -> Optional[str]: values = query.get(key) if not values: return default return values[0] def _is_training_progress_post(method: str, path: str) -> bool: return method == "POST" and path == "/training/progress" def _is_training_attempt_create_post(method: str, path: str) -> bool: return method == "POST" and path == "/training/attempts" def _path_parts(path: str) -> list[str]: return [part for part in path.strip("/").split("/") if part] def _validate_post_route_path(path: str) -> None: path_parts = _path_parts(path) if ( len(path_parts) == 4 and path_parts[:2] == ["training", "attempts"] and path_parts[3] in {"annotations", "submit"} ): _parse_path_int(path_parts[2], "attempt_id") def _parse_path_int(value: str, key: str) -> int: try: parsed = int(value) except ValueError as exc: raise ValueError(f"invalid_path_param:{key}") from exc if parsed < 1: raise ValueError(f"invalid_path_param:{key}") return parsed def _validate_progress_identifier_body(body: dict) -> None: for key in ("attempt_id", "case_id"): if key in body: _parse_body_identifier(body[key], key) def _validate_attempt_create_body(body: dict) -> None: _parse_required_json_identifier(body, "case_id") def _parse_required_json_identifier(body: dict, key: str) -> int: if not isinstance(body, dict): raise ValueError("invalid_json_body") if body.get(key) is None: raise ValueError(f"missing_required_json_field:{key}") value = body[key] if isinstance(value, bool): raise ValueError(f"invalid_json_field:{key}") try: parsed = int(value) except (TypeError, ValueError) as exc: raise ValueError(f"invalid_json_field:{key}") from exc if parsed < 1: raise ValueError(f"invalid_json_field:{key}") return parsed def _require_json_int(body: dict, key: str) -> int: return _parse_required_json_identifier(body, key) def _parse_body_identifier(value, key: str) -> int: if isinstance(value, bool): raise ValueError(f"invalid_body_field:{key}") try: parsed = int(value) except (TypeError, ValueError) as exc: raise ValueError(f"invalid_body_field:{key}") from exc if parsed < 1: raise ValueError(f"invalid_body_field:{key}") return parsed def _read_json_body(handler: BaseHTTPRequestHandler) -> dict: try: length = int(handler.headers.get("Content-Length", "0")) except ValueError as exc: raise ValueError("invalid_content_length") from exc raw_body = handler.rfile.read(length) if length else b"" if not raw_body: return {} try: payload = json.loads(raw_body.decode("utf-8")) except (json.JSONDecodeError, UnicodeDecodeError) as exc: raise ValueError("invalid_json_body") from exc if not isinstance(payload, dict): raise ValueError("invalid_json_body") return payload def _should_serve_static(path: str) -> bool: return path in {"/", "/training", "/training.html"} or path.startswith("/static/") def _resolve_static_file(path: str) -> Optional[Path]: if path == "/": return STATIC_DIR / "index.html" if path in {"/training", "/training.html"}: return STATIC_DIR / "training.html" relative = path.removeprefix("/static/") resolved = (STATIC_DIR / relative).resolve() try: resolved.relative_to(STATIC_DIR.resolve()) except ValueError: return None return resolved