import json
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
@dataclass
class ModelVersion:
version: int
checkpoint_id: str
timestamp: str
parent_version: Optional[int]
format: str
size_bytes: int
compressed_size_bytes: int
checksum_sha256: str
metadata: Dict[str, Any]
file_path: str
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ModelVersion':
return cls(**data)
class VersionControl:
VERSION_FILE = "versions.json"
def __init__(self, vault_path: Path) -> None:
self.vault_path = vault_path
self.version_file = vault_path / self.VERSION_FILE
self.versions: Dict[str, List[ModelVersion]] = {}
self._load_versions()
def _load_versions(self) -> None:
if self.version_file.exists():
with open(self.version_file, 'r') as f:
data = json.load(f)
self.versions = {
model_name: [ModelVersion.from_dict(v) for v in versions]
for model_name, versions in data.items()
}
else:
self.versions = {}
def _save_versions(self) -> None:
data = {
model_name: [v.to_dict() for v in versions]
for model_name, versions in self.versions.items()
}
with open(self.version_file, 'w') as f:
json.dump(data, f, indent=2)
def add_version(
self,
model_name: str,
file_path: str,
format: str,
size_bytes: int,
compressed_size_bytes: int,
checksum: str,
metadata: Optional[Dict[str, Any]] = None,
parent_version: Optional[int] = None,
) -> ModelVersion:
if model_name not in self.versions:
self.versions[model_name] = []
if self.versions[model_name]:
version = max(v.version for v in self.versions[model_name]) + 1
else:
version = 1
timestamp = datetime.utcnow().isoformat() + 'Z'
checkpoint_id = self._generate_checkpoint_id(model_name, version, timestamp)
model_version = ModelVersion(
version=version,
checkpoint_id=checkpoint_id,
timestamp=timestamp,
parent_version=parent_version,
format=format,
size_bytes=size_bytes,
compressed_size_bytes=compressed_size_bytes,
checksum_sha256=checksum,
metadata=metadata or {},
file_path=file_path,
)
self.versions[model_name].append(model_version)
self._save_versions()
return model_version
def get_version(self, model_name: str, version: Optional[int] = None) -> Optional[ModelVersion]:
if model_name not in self.versions or not self.versions[model_name]:
return None
if version is None:
return max(self.versions[model_name], key=lambda v: v.version)
for v in self.versions[model_name]:
if v.version == version:
return v
return None
def list_versions(self, model_name: str) -> List[ModelVersion]:
if model_name not in self.versions:
return []
return sorted(self.versions[model_name], key=lambda v: v.version)
def get_lineage(self, model_name: str, version: int) -> List[ModelVersion]:
target = self.get_version(model_name, version)
if not target:
return []
lineage = [target]
current = target
while current.parent_version is not None:
parent = self.get_version(model_name, current.parent_version)
if parent:
lineage.insert(0, parent)
current = parent
else:
break
return lineage
def delete_version(self, model_name: str, version: int) -> bool:
if model_name not in self.versions:
return False
original_count = len(self.versions[model_name])
self.versions[model_name] = [
v for v in self.versions[model_name] if v.version != version
]
if len(self.versions[model_name]) < original_count:
self._save_versions()
return True
return False
def cleanup_old_versions(self, model_name: str, keep_count: int = 10) -> List[int]:
if model_name not in self.versions:
return []
versions = sorted(self.versions[model_name], key=lambda v: v.version, reverse=True)
if len(versions) <= keep_count:
return []
to_keep = versions[:keep_count]
to_delete = versions[keep_count:]
deleted_versions = [v.version for v in to_delete]
self.versions[model_name] = to_keep
self._save_versions()
return deleted_versions
def verify_checksum(self, model_name: str, version: int, data: bytes) -> bool:
model_version = self.get_version(model_name, version)
if not model_version:
return False
checksum = hashlib.sha256(data).hexdigest()
return checksum == model_version.checksum_sha256
@staticmethod
def _generate_checkpoint_id(model_name: str, version: int, timestamp: str) -> str:
data = f"{model_name}:{version}:{timestamp}"
return hashlib.sha256(data.encode()).hexdigest()[:16]
@staticmethod
def compute_checksum(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()