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:
22
src/animaltrack/models/__init__.py
Normal file
22
src/animaltrack/models/__init__.py
Normal 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",
|
||||
]
|
||||
129
src/animaltrack/models/reference.py
Normal file
129
src/animaltrack/models/reference.py
Normal 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
|
||||
Reference in New Issue
Block a user