mindXtrain / mindxtrain /deploy /registry.py
Gregory-L's picture
fork mindXtrain from GitHub (Professor-Codephreak/mindXtrain@661bd41) as the mindX-specific line
dfb775d verified
Raw History Blame Contribute Delete
2.36 kB
"""Model-version registry — content-addressed deploy slots.
JSON-file-backed registry with atomic writes (`os.replace`). Three slots:
`live`, `staged`, `canary`. Pure stdlib.
The registry tracks (run_id, blake3, activated_at) per slot plus a
`previous_live` slot so `mindxtrain.deploy.hot_swap.rollback_live` is a
single-step operation.
"""
from __future__ import annotations
import os
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
SlotName = Literal["live", "staged", "canary", "previous_live"]
class Slot(BaseModel):
model_config = ConfigDict(extra="forbid")
name: SlotName
run_id: str
blake3: str
activated_at: datetime = Field(default_factory=lambda: datetime.now(tz=UTC))
class RegistryState(BaseModel):
model_config = ConfigDict(extra="forbid")
schema_version: Literal["1"] = "1"
slots: dict[str, Slot] = Field(default_factory=dict)
class DeployRegistry:
"""Atomic JSON-file-backed deploy registry."""
def __init__(self, path: Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
def _read(self) -> RegistryState:
if not self.path.exists():
return RegistryState()
return RegistryState.model_validate_json(self.path.read_text())
def _write(self, state: RegistryState) -> None:
# Atomic write: tmp file in same directory, then os.replace.
with tempfile.NamedTemporaryFile(
"w",
dir=self.path.parent,
delete=False,
prefix=f".{self.path.name}.",
suffix=".tmp",
) as tmp:
tmp.write(state.model_dump_json(indent=2))
tmp_path = Path(tmp.name)
os.replace(tmp_path, self.path)
def get(self, slot: SlotName) -> Slot | None:
return self._read().slots.get(slot)
def set(self, slot: Slot) -> None:
state = self._read()
state.slots[slot.name] = slot
self._write(state)
def clear(self, slot: SlotName) -> None:
state = self._read()
state.slots.pop(slot, None)
self._write(state)
def all(self) -> dict[str, Slot]:
return dict(self._read().slots)
__all__ = ["DeployRegistry", "RegistryState", "Slot", "SlotName"]