From 41cdb47b0ec678d515532871330e0bdd74d8b656 Mon Sep 17 00:00:00 2001 From: Prakhar Verma Date: Sun, 10 May 2026 21:56:35 -0400 Subject: [PATCH 1/2] Add typed runtime settings --- gateway/app/main.py | 41 ++++------------- gateway/app/settings.py | 80 ++++++++++++++++++++++++++++++++++ tests/test_cache.py | 2 +- tests/test_gateway_settings.py | 47 ++++++++++++++++++++ tests/test_worker_settings.py | 36 +++++++++++++++ worker/app/main.py | 13 +++--- worker/app/settings.py | 55 +++++++++++++++++++++++ 7 files changed, 235 insertions(+), 39 deletions(-) create mode 100644 gateway/app/settings.py create mode 100644 tests/test_gateway_settings.py create mode 100644 tests/test_worker_settings.py create mode 100644 worker/app/settings.py diff --git a/gateway/app/main.py b/gateway/app/main.py index f94ae1f..aa1225d 100644 --- a/gateway/app/main.py +++ b/gateway/app/main.py @@ -1,9 +1,9 @@ from __future__ import annotations -import os import time +from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from typing import Any, AsyncIterator +from typing import Any import httpx from fastapi import FastAPI, HTTPException @@ -14,37 +14,13 @@ from .metrics import MetricsStore from .router import Worker, WorkerPool from .schemas import GatewayPrediction, PredictRequest, WorkerControlRequest +from .settings import GatewaySettings -DEFAULT_WORKER_URL = "http://localhost:8011" MAX_WORKER_ATTEMPTS = 2 JsonObject = dict[str, Any] WorkerRow = dict[str, str | int | float | None] -def env_flag(name: str, default: bool = True) -> bool: - value = os.getenv(name) - if value is None: - return default - return value.lower() not in {"0", "false", "no", "off"} - - -def parse_worker_hostports(env: dict[str, str] | None = None) -> list[str]: - values = env or os.environ - urls = [] - for key in sorted(values): - if key.startswith("WORKER_") and key.endswith("_HOSTPORT"): - hostport = values[key].strip() - if hostport: - urls.append(f"http://{hostport}") - return urls - - -def parse_worker_urls(raw: str | None) -> list[str]: - if not raw: - return parse_worker_hostports() or [DEFAULT_WORKER_URL] - return [item.strip() for item in raw.split(",") if item.strip()] - - @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: app.state.client = httpx.AsyncClient() @@ -53,16 +29,17 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI(title="ModelMesh Gateway", version="0.1.0", lifespan=lifespan) +settings = GatewaySettings.from_env() router = WorkerPool( - parse_worker_urls(os.getenv("WORKER_URLS")), - strategy=os.getenv("ROUTER_STRATEGY", "load_aware"), + settings.worker_urls, + strategy=settings.router_strategy, ) cache = PredictionCache( - os.getenv("REDIS_URL"), - enabled=env_flag("CACHE_ENABLED", default=True), + settings.redis_url, + enabled=settings.cache_enabled, ) metrics = MetricsStore() -request_timeout = float(os.getenv("REQUEST_TIMEOUT_SECONDS", "1.0")) +request_timeout = settings.request_timeout_seconds def elapsed_ms(started: float) -> float: diff --git a/gateway/app/settings.py b/gateway/app/settings.py new file mode 100644 index 0000000..73f22db --- /dev/null +++ b/gateway/app/settings.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from os import environ + +DEFAULT_WORKER_URL = "http://localhost:8011" +ROUTER_STRATEGIES = {"load_aware", "round_robin"} +FALSE_VALUES = {"0", "false", "no", "off"} +TRUE_VALUES = {"1", "true", "yes", "on"} + + +def parse_bool(raw: str | None, *, default: bool) -> bool: + if raw is None: + return default + + value = raw.strip().lower() + if value in TRUE_VALUES: + return True + if value in FALSE_VALUES: + return False + raise ValueError(f"expected a boolean value, got {raw!r}") + + +def parse_worker_hostports(env: Mapping[str, str] | None = None) -> list[str]: + values = env or environ + urls = [] + for key in sorted(values): + if key.startswith("WORKER_") and key.endswith("_HOSTPORT"): + hostport = values[key].strip() + if hostport: + urls.append(f"http://{hostport}") + return urls + + +def parse_worker_urls(raw: str | None, env: Mapping[str, str] | None = None) -> list[str]: + if raw: + return [item.strip().rstrip("/") for item in raw.split(",") if item.strip()] + return parse_worker_hostports(env) or [DEFAULT_WORKER_URL] + + +def parse_timeout(raw: str | None, *, default: float) -> float: + if raw is None: + return default + + try: + value = float(raw) + except ValueError as exc: + raise ValueError(f"REQUEST_TIMEOUT_SECONDS must be a number, got {raw!r}") from exc + + if value <= 0: + raise ValueError("REQUEST_TIMEOUT_SECONDS must be greater than zero") + return value + + +@dataclass(frozen=True) +class GatewaySettings: + worker_urls: list[str] + router_strategy: str = "load_aware" + redis_url: str | None = None + cache_enabled: bool = True + request_timeout_seconds: float = 1.0 + + @classmethod + def from_env(cls, env: Mapping[str, str] | None = None) -> GatewaySettings: + values = env or environ + router_strategy = values.get("ROUTER_STRATEGY", "load_aware") + if router_strategy not in ROUTER_STRATEGIES: + raise ValueError("ROUTER_STRATEGY must be load_aware or round_robin") + + return cls( + worker_urls=parse_worker_urls(values.get("WORKER_URLS"), values), + router_strategy=router_strategy, + redis_url=values.get("REDIS_URL"), + cache_enabled=parse_bool(values.get("CACHE_ENABLED"), default=True), + request_timeout_seconds=parse_timeout( + values.get("REQUEST_TIMEOUT_SECONDS"), + default=1.0, + ), + ) diff --git a/tests/test_cache.py b/tests/test_cache.py index eaf38b3..7a31338 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,5 +1,5 @@ from gateway.app.cache import PredictionCache -from gateway.app.main import parse_worker_hostports, parse_worker_urls +from gateway.app.settings import parse_worker_hostports, parse_worker_urls def test_memory_cache_round_trip() -> None: diff --git a/tests/test_gateway_settings.py b/tests/test_gateway_settings.py new file mode 100644 index 0000000..500d2c8 --- /dev/null +++ b/tests/test_gateway_settings.py @@ -0,0 +1,47 @@ +import pytest + +from gateway.app.settings import GatewaySettings, parse_bool, parse_worker_urls + + +def test_gateway_settings_reads_explicit_worker_urls() -> None: + settings = GatewaySettings.from_env({ + "WORKER_URLS": "http://a:9000/, http://b:9000", + "ROUTER_STRATEGY": "round_robin", + "CACHE_ENABLED": "false", + "REQUEST_TIMEOUT_SECONDS": "2.5", + "REDIS_URL": "redis://cache:6379/0", + }) + + assert settings.worker_urls == ["http://a:9000", "http://b:9000"] + assert settings.router_strategy == "round_robin" + assert settings.cache_enabled is False + assert settings.request_timeout_seconds == 2.5 + assert settings.redis_url == "redis://cache:6379/0" + + +def test_gateway_settings_reads_render_hostports() -> None: + settings = GatewaySettings.from_env({ + "WORKER_B_HOSTPORT": "worker-b:9000", + "WORKER_A_HOSTPORT": "worker-a:9000", + }) + + assert settings.worker_urls == ["http://worker-a:9000", "http://worker-b:9000"] + + +def test_gateway_settings_defaults_to_local_worker() -> None: + assert parse_worker_urls(None, {}) == ["http://localhost:8011"] + + +def test_gateway_settings_rejects_unknown_router_strategy() -> None: + with pytest.raises(ValueError, match="ROUTER_STRATEGY"): + GatewaySettings.from_env({"ROUTER_STRATEGY": "random"}) + + +def test_gateway_settings_rejects_invalid_timeout() -> None: + with pytest.raises(ValueError, match="REQUEST_TIMEOUT_SECONDS"): + GatewaySettings.from_env({"REQUEST_TIMEOUT_SECONDS": "0"}) + + +def test_parse_bool_rejects_ambiguous_value() -> None: + with pytest.raises(ValueError, match="boolean"): + parse_bool("maybe", default=True) diff --git a/tests/test_worker_settings.py b/tests/test_worker_settings.py new file mode 100644 index 0000000..68b5380 --- /dev/null +++ b/tests/test_worker_settings.py @@ -0,0 +1,36 @@ +import pytest + +from worker.app.settings import WorkerSettings + + +def test_worker_settings_reads_runtime_env() -> None: + settings = WorkerSettings.from_env({ + "WORKER_ID": "worker-a", + "MODEL_ARTIFACT": "/tmp/model.json", + "SIMULATE_DELAY_MS": "250", + "FAIL_RATE": "0.25", + }) + + assert settings.worker_id == "worker-a" + assert settings.model_artifact == "/tmp/model.json" + assert settings.delay_ms == 250 + assert settings.fail_rate == 0.25 + + +def test_worker_settings_uses_local_defaults() -> None: + settings = WorkerSettings.from_env({}) + + assert settings.worker_id == "worker-local" + assert settings.model_artifact is None + assert settings.delay_ms == 0 + assert settings.fail_rate == 0.0 + + +def test_worker_settings_rejects_negative_delay() -> None: + with pytest.raises(ValueError, match="SIMULATE_DELAY_MS"): + WorkerSettings.from_env({"SIMULATE_DELAY_MS": "-1"}) + + +def test_worker_settings_rejects_invalid_fail_rate() -> None: + with pytest.raises(ValueError, match="FAIL_RATE"): + WorkerSettings.from_env({"FAIL_RATE": "1.5"}) diff --git a/worker/app/main.py b/worker/app/main.py index a95e7b2..e1ce565 100644 --- a/worker/app/main.py +++ b/worker/app/main.py @@ -1,14 +1,14 @@ from __future__ import annotations -import os -import random import asyncio +import random from dataclasses import dataclass from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from .model import load_model +from .settings import WorkerSettings class PredictRequest(BaseModel): @@ -36,11 +36,12 @@ def snapshot(self) -> dict[str, int | float | str]: app = FastAPI(title="ModelMesh Worker", version="0.1.0") -model = load_model(os.getenv("MODEL_ARTIFACT")) -worker_id = os.getenv("WORKER_ID", "worker-local") +settings = WorkerSettings.from_env() +model = load_model(settings.model_artifact) +worker_id = settings.worker_id runtime = WorkerRuntime( - delay_ms=int(os.getenv("SIMULATE_DELAY_MS", "0")), - fail_rate=float(os.getenv("FAIL_RATE", "0")), + delay_ms=settings.delay_ms, + fail_rate=settings.fail_rate, ) diff --git a/worker/app/settings.py b/worker/app/settings.py new file mode 100644 index 0000000..3ab4212 --- /dev/null +++ b/worker/app/settings.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from os import environ + + +def parse_non_negative_int(name: str, raw: str | None, *, default: int) -> int: + if raw is None: + return default + + try: + value = int(raw) + except ValueError as exc: + raise ValueError(f"{name} must be an integer, got {raw!r}") from exc + + if value < 0: + raise ValueError(f"{name} must be greater than or equal to zero") + return value + + +def parse_probability(name: str, raw: str | None, *, default: float) -> float: + if raw is None: + return default + + try: + value = float(raw) + except ValueError as exc: + raise ValueError(f"{name} must be a number, got {raw!r}") from exc + + if value < 0 or value > 1: + raise ValueError(f"{name} must be between 0 and 1") + return value + + +@dataclass(frozen=True) +class WorkerSettings: + worker_id: str = "worker-local" + model_artifact: str | None = None + delay_ms: int = 0 + fail_rate: float = 0.0 + + @classmethod + def from_env(cls, env: Mapping[str, str] | None = None) -> WorkerSettings: + values = env or environ + return cls( + worker_id=values.get("WORKER_ID", "worker-local"), + model_artifact=values.get("MODEL_ARTIFACT"), + delay_ms=parse_non_negative_int( + "SIMULATE_DELAY_MS", + values.get("SIMULATE_DELAY_MS"), + default=0, + ), + fail_rate=parse_probability("FAIL_RATE", values.get("FAIL_RATE"), default=0.0), + ) From d2328d719d34ca53a8a4dbba10664b6fbd140b44 Mon Sep 17 00:00:00 2001 From: Prakhar Verma Date: Sun, 10 May 2026 21:56:45 -0400 Subject: [PATCH 2/2] Add lint and compile checks --- .github/workflows/test.yml | 7 +++++ bench/replay_traffic.py | 34 ++++++++++++++++++----- bench/run_bench.py | 48 ++++++++++++++++++++++----------- bench/run_scenarios.py | 1 - gateway/app/dashboard.py | 1 - gateway/app/metrics.py | 8 +++++- pyproject.toml | 15 +++++++++++ requirements-dev.txt | 1 + scripts/smoke_check.py | 24 ++++++++++++----- training/evaluate_text_model.py | 1 - training/train_text_model.py | 27 +++++++++++++++---- worker/app/model.py | 3 +-- 12 files changed, 130 insertions(+), 40 deletions(-) create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d429bf9..bd1b05b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,13 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -r requirements.txt + python -m pip install -r requirements-dev.txt + + - name: Lint + run: python -m ruff check . + + - name: Compile Python + run: python -m compileall -q gateway worker training bench scripts tests - name: Run tests run: python -m pytest -q diff --git a/bench/replay_traffic.py b/bench/replay_traffic.py index e51f3b3..7ee65a0 100644 --- a/bench/replay_traffic.py +++ b/bench/replay_traffic.py @@ -9,7 +9,6 @@ import httpx - ROOT = Path(__file__).resolve().parents[1] RESULT_PATH = ROOT / "bench" / "results" / "replay-latest.json" TEXTS = [ @@ -31,7 +30,14 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -async def control_worker(client: httpx.AsyncClient, base_url: str, index: int, *, delay_ms: int, fail_rate: float) -> bool: +async def control_worker( + client: httpx.AsyncClient, + base_url: str, + index: int, + *, + delay_ms: int, + fail_rate: float, +) -> bool: try: response = await client.post( f"{base_url}/workers/{index}/control", @@ -63,7 +69,13 @@ async def snapshot(client: httpx.AsyncClient, base_url: str, stage: str) -> dict } -async def send_stage(client: httpx.AsyncClient, base_url: str, stage: str, seconds: float, rps: float) -> dict[str, Any]: +async def send_stage( + client: httpx.AsyncClient, + base_url: str, + stage: str, + seconds: float, + rps: float, +) -> dict[str, Any]: deadline = time.perf_counter() + seconds interval = 1 / rps sent = 0 @@ -106,17 +118,25 @@ async def main() -> None: async with httpx.AsyncClient(timeout=5.0) as client: await reset_workers(client, base_url) - stages.append(await send_stage(client, base_url, "baseline", args.seconds_per_stage, args.rps)) + stages.append( + await send_stage(client, base_url, "baseline", args.seconds_per_stage, args.rps) + ) await control_worker(client, base_url, 1, delay_ms=350, fail_rate=0) - stages.append(await send_stage(client, base_url, "slow-worker", args.seconds_per_stage, args.rps)) + stages.append( + await send_stage(client, base_url, "slow-worker", args.seconds_per_stage, args.rps) + ) await reset_workers(client, base_url) await control_worker(client, base_url, 0, delay_ms=0, fail_rate=0.45) - stages.append(await send_stage(client, base_url, "flaky-worker", args.seconds_per_stage, args.rps)) + stages.append( + await send_stage(client, base_url, "flaky-worker", args.seconds_per_stage, args.rps) + ) await reset_workers(client, base_url) - stages.append(await send_stage(client, base_url, "recovery", args.seconds_per_stage, args.rps)) + stages.append( + await send_stage(client, base_url, "recovery", args.seconds_per_stage, args.rps) + ) payload = { "base_url": base_url, diff --git a/bench/run_bench.py b/bench/run_bench.py index 6a76386..ee6bcd4 100644 --- a/bench/run_bench.py +++ b/bench/run_bench.py @@ -8,7 +8,6 @@ import httpx - SAMPLES = [ "fast stable model service", "worker timeout and broken response", @@ -26,7 +25,13 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -async def call(client: httpx.AsyncClient, url: str, index: int, *, unique_requests: bool) -> tuple[float, bool, bool]: +async def call( + client: httpx.AsyncClient, + url: str, + index: int, + *, + unique_requests: bool, +) -> tuple[float, bool, bool]: started = time.perf_counter() text = SAMPLES[index % len(SAMPLES)] if unique_requests: @@ -55,19 +60,32 @@ async def limited(index: int) -> tuple[float, bool, bool]: latencies = [latency for latency, ok, _ in results if ok] failures = sum(1 for _, ok, _ in results if not ok) cache_hits = sum(1 for _, ok, cached in results if ok and cached) - p95 = statistics.quantiles(latencies, n=20)[18] if len(latencies) >= 20 else max(latencies, default=0) - - print(json.dumps({ - "requests": args.requests, - "successes": len(latencies), - "failures": failures, - "cache_hits": cache_hits, - "cache_hit_rate": round(cache_hits / len(latencies), 4) if latencies else 0, - "total_ms": round(total_ms, 2), - "requests_per_second": round(args.requests / (total_ms / 1000), 2), - "p50_latency_ms": round(statistics.median(latencies), 2) if latencies else 0, - "p95_latency_ms": round(p95, 2), - }, indent=2)) + p95 = ( + statistics.quantiles(latencies, n=20)[18] + if len(latencies) >= 20 + else max(latencies, default=0) + ) + + print( + json.dumps( + { + "requests": args.requests, + "successes": len(latencies), + "failures": failures, + "cache_hits": cache_hits, + "cache_hit_rate": round(cache_hits / len(latencies), 4) + if latencies + else 0, + "total_ms": round(total_ms, 2), + "requests_per_second": round(args.requests / (total_ms / 1000), 2), + "p50_latency_ms": round(statistics.median(latencies), 2) + if latencies + else 0, + "p95_latency_ms": round(p95, 2), + }, + indent=2, + ) + ) if __name__ == "__main__": diff --git a/bench/run_scenarios.py b/bench/run_scenarios.py index fe931b8..ec80b68 100644 --- a/bench/run_scenarios.py +++ b/bench/run_scenarios.py @@ -16,7 +16,6 @@ import httpx - ROOT = Path(__file__).resolve().parents[1] RESULTS_DIR = ROOT / "bench" / "results" BASE_PORT = 8300 diff --git a/gateway/app/dashboard.py b/gateway/app/dashboard.py index aa3c2fa..27f862d 100644 --- a/gateway/app/dashboard.py +++ b/gateway/app/dashboard.py @@ -1,6 +1,5 @@ from __future__ import annotations - DASHBOARD_HTML = """ diff --git a/gateway/app/metrics.py b/gateway/app/metrics.py index 503e90f..0b94c90 100644 --- a/gateway/app/metrics.py +++ b/gateway/app/metrics.py @@ -23,7 +23,13 @@ class MetricsStore: worker_errors: int = 0 latencies_ms: deque[float] = field(default_factory=lambda: deque(maxlen=500)) - def record_request(self, latency_ms: float, *, cached: bool, worker_error: bool = False) -> None: + def record_request( + self, + latency_ms: float, + *, + cached: bool, + worker_error: bool = False, + ) -> None: self.requests += 1 self.latencies_ms.append(latency_ms) if cached: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a8670f0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] + +[tool.ruff.lint.per-file-ignores] +"gateway/app/dashboard.py" = ["E501"] + +[tool.ruff.format] +quote-style = "double" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..b4507a2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +ruff==0.8.6 diff --git a/scripts/smoke_check.py b/scripts/smoke_check.py index 29b718e..3bb143d 100644 --- a/scripts/smoke_check.py +++ b/scripts/smoke_check.py @@ -20,7 +20,12 @@ def get_json(url: str, timeout: float = 2.0) -> dict: def post_json(url: str, payload: dict, timeout: float = 2.0) -> dict: data = json.dumps(payload).encode("utf-8") - req = request.Request(url, data=data, headers={"content-type": "application/json"}, method="POST") + req = request.Request( + url, + data=data, + headers={"content-type": "application/json"}, + method="POST", + ) with request.urlopen(req, timeout=timeout) as response: return json.loads(response.read().decode("utf-8")) @@ -55,12 +60,17 @@ def main() -> None: if not workers: raise RuntimeError("gateway returned no workers") - print(json.dumps({ - "health": health, - "prediction": prediction, - "metrics": metrics, - "workers": workers, - }, indent=2)) + print( + json.dumps( + { + "health": health, + "prediction": prediction, + "metrics": metrics, + "workers": workers, + }, + indent=2, + ) + ) if __name__ == "__main__": diff --git a/training/evaluate_text_model.py b/training/evaluate_text_model.py index c038edc..565232d 100644 --- a/training/evaluate_text_model.py +++ b/training/evaluate_text_model.py @@ -7,7 +7,6 @@ from pathlib import Path from typing import Any - ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) diff --git a/training/train_text_model.py b/training/train_text_model.py index 3b08443..5eef830 100644 --- a/training/train_text_model.py +++ b/training/train_text_model.py @@ -10,7 +10,6 @@ import torch from torch import nn - ROOT = Path(__file__).resolve().parents[1] OUTPUT_PATH = ROOT / "worker" / "app" / "artifacts" / "tiny_text_model.json" RUN_PATH = ROOT / "training" / "runs" / "latest.json" @@ -103,10 +102,18 @@ def build_dataset(samples_per_class: int) -> tuple[list[str], torch.Tensor, torc row[index[token]] += 1.0 rows.append(row) - return vocab, torch.tensor(rows, dtype=torch.float32), torch.tensor(labels, dtype=torch.float32).view(-1, 1) + return ( + vocab, + torch.tensor(rows, dtype=torch.float32), + torch.tensor(labels, dtype=torch.float32).view(-1, 1), + ) -def split_dataset(x: torch.Tensor, y: torch.Tensor, train_fraction: float = 0.8) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: +def split_dataset( + x: torch.Tensor, + y: torch.Tensor, + train_fraction: float = 0.8, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: train_count = int(len(x) * train_fraction) return x[:train_count], y[:train_count], x[train_count:], y[train_count:] @@ -170,7 +177,12 @@ def train( return vocab, model.cpu(), stats -def write_artifact(path: Path, vocab: list[str], model: nn.Linear, stats: dict[str, float | int | str]) -> None: +def write_artifact( + path: Path, + vocab: list[str], + model: nn.Linear, + stats: dict[str, float | int | str], +) -> None: weights = model.weight.detach().reshape(-1).tolist() artifact = { "model_version": "tiny-text-mps-v1", @@ -182,7 +194,12 @@ def write_artifact(path: Path, vocab: list[str], model: nn.Linear, stats: dict[s path.write_text(json.dumps(artifact, indent=2) + "\n") -def write_run_files(json_path: Path, md_path: Path, stats: dict[str, float | int | str], model_path: Path) -> None: +def write_run_files( + json_path: Path, + md_path: Path, + stats: dict[str, float | int | str], + model_path: Path, +) -> None: try: display_model_path = str(model_path.relative_to(ROOT)) except ValueError: diff --git a/worker/app/model.py b/worker/app/model.py index 9cafc63..8380ad4 100644 --- a/worker/app/model.py +++ b/worker/app/model.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from pathlib import Path - TOKEN_RE = re.compile(r"[a-z']+") DEFAULT_ARTIFACT = Path(__file__).resolve().parent / "artifacts" / "tiny_text_model.json" DEFAULT_WEIGHTS = { @@ -29,7 +28,7 @@ class TinyTextModel: bias: float = 0.0 @classmethod - def from_artifact(cls, path: Path) -> "TinyTextModel": + def from_artifact(cls, path: Path) -> TinyTextModel: artifact = json.loads(path.read_text()) return cls( version=artifact.get("model_version", "tiny-text-trained"),