feat: add reference tables schema and Pydantic models

Implements Step 1.4: Creates migration for species, locations, products,
feed_types, and users tables with full CHECK constraints. Adds Pydantic
models with validation for all reference types including enums for
ProductUnit and UserRole.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 19:18:08 +00:00
parent 1e2a0208e6
commit 2e28827683
5 changed files with 968 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# ABOUTME: Models package for AnimalTrack.
# ABOUTME: Exports all Pydantic models and enums for reference data.
from animaltrack.models.reference import (
FeedType,
Location,
Product,
ProductUnit,
Species,
User,
UserRole,
)
__all__ = [
"FeedType",
"Location",
"Product",
"ProductUnit",
"Species",
"User",
"UserRole",
]

View File

@@ -0,0 +1,129 @@
# ABOUTME: Pydantic models for reference tables (species, locations, products, feed_types, users).
# ABOUTME: These models validate data before database insertion and provide type safety.
from enum import Enum
from pydantic import BaseModel, Field, field_validator
class ProductUnit(str, Enum):
"""Unit of measurement for products."""
PIECE = "piece"
KG = "kg"
class UserRole(str, Enum):
"""Role for system users."""
ADMIN = "admin"
RECORDER = "recorder"
class Species(BaseModel):
"""Animal species reference data."""
code: str
name: str
active: bool = True
created_at_utc: int
updated_at_utc: int
@field_validator("created_at_utc", "updated_at_utc")
@classmethod
def timestamp_must_be_non_negative(cls, v: int) -> int:
"""Timestamps must be >= 0 (milliseconds since Unix epoch)."""
if v < 0:
msg = "Timestamp must be non-negative"
raise ValueError(msg)
return v
class Location(BaseModel):
"""Physical location reference data."""
id: str = Field(..., min_length=26, max_length=26)
name: str
active: bool = True
created_at_utc: int
updated_at_utc: int
@field_validator("id")
@classmethod
def id_must_be_26_chars(cls, v: str) -> str:
"""Location ID must be exactly 26 characters (ULID format)."""
if len(v) != 26:
msg = "Location ID must be exactly 26 characters"
raise ValueError(msg)
return v
@field_validator("created_at_utc", "updated_at_utc")
@classmethod
def timestamp_must_be_non_negative(cls, v: int) -> int:
"""Timestamps must be >= 0 (milliseconds since Unix epoch)."""
if v < 0:
msg = "Timestamp must be non-negative"
raise ValueError(msg)
return v
class Product(BaseModel):
"""Product reference data (eggs, meat, etc.)."""
code: str
name: str
unit: ProductUnit
collectable: bool
sellable: bool
active: bool = True
created_at_utc: int
updated_at_utc: int
@field_validator("created_at_utc", "updated_at_utc")
@classmethod
def timestamp_must_be_non_negative(cls, v: int) -> int:
"""Timestamps must be >= 0 (milliseconds since Unix epoch)."""
if v < 0:
msg = "Timestamp must be non-negative"
raise ValueError(msg)
return v
class FeedType(BaseModel):
"""Feed type reference data."""
code: str
name: str
default_bag_size_kg: int = Field(..., ge=1)
protein_pct: float | None = None
active: bool = True
created_at_utc: int
updated_at_utc: int
@field_validator("created_at_utc", "updated_at_utc")
@classmethod
def timestamp_must_be_non_negative(cls, v: int) -> int:
"""Timestamps must be >= 0 (milliseconds since Unix epoch)."""
if v < 0:
msg = "Timestamp must be non-negative"
raise ValueError(msg)
return v
class User(BaseModel):
"""System user reference data."""
username: str
role: UserRole
active: bool = True
created_at_utc: int
updated_at_utc: int
@field_validator("created_at_utc", "updated_at_utc")
@classmethod
def timestamp_must_be_non_negative(cls, v: int) -> int:
"""Timestamps must be >= 0 (milliseconds since Unix epoch)."""
if v < 0:
msg = "Timestamp must be non-negative"
raise ValueError(msg)
return v