Skip to main content
Glama
CLAUDE.md28.9 kB
# Models Layer - Development Memory ## Pydantic v2 Model Architecture (2.0+) ### Modern Base Model Pattern ```python from pydantic import BaseModel, Field, ConfigDict from typing import Annotated, Literal, Any from datetime import datetime import uuid class ContainerInfo(BaseModel): """Information about a Docker container with Pydantic v2 patterns.""" # Modern model configuration (Pydantic v2) model_config = ConfigDict( # Performance and validation settings validate_assignment=True, # Validate on assignment use_enum_values=True, # Use enum values in serialization arbitrary_types_allowed=True, # Allow custom types str_strip_whitespace=True, # Auto-strip strings populate_by_name=True, # Support field aliases # JSON schema generation json_schema_extra={ "examples": [{ "container_id": "abc123def456", "name": "web-server", "host_id": "production-1", "status": "running", "ports": [{"host": "80", "container": "80"}] }] } ) # Required fields with enhanced validation container_id: Annotated[str, Field( min_length=8, max_length=64, pattern=r"^[a-f0-9]{8,64}$", description="Docker container ID (hex)" )] name: Annotated[str, Field( min_length=1, max_length=128, pattern=r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*$", description="Container name (Docker naming rules)" )] host_id: Annotated[str, Field( min_length=1, max_length=64, description="Host identifier where container runs" )] # Status with literal types for type safety status: Literal["created", "restarting", "running", "removing", "paused", "exited", "dead"] = "unknown" # Collections with proper defaults ports: Annotated[list[dict[str, Any]], Field( default_factory=list, description="Port mappings between host and container" )] labels: Annotated[dict[str, str], Field( default_factory=dict, description="Docker labels as key-value pairs" )] # Timestamps with proper types created_at: datetime | None = Field( default=None, description="When the container was created" ) # Computed fields (Pydantic v2 feature) @property def is_running(self) -> bool: """Computed property indicating if container is running.""" return self.status == "running" # Model validators (Pydantic v2 syntax) def model_post_init(self, __context: Any) -> None: """Post-init validation and processing.""" # Normalize container name if needed if self.name and not self.name.startswith('/'): self.name = self.name.lstrip('/') # Advanced field validation patterns class StackDeployRequest(BaseModel): """Modern stack deployment request with comprehensive validation.""" model_config = ConfigDict( validate_assignment=True, str_strip_whitespace=True, # Custom field aliases for API compatibility alias_generator=lambda field_name: field_name, # Keep snake_case ) # Host validation with custom constraints host_id: Annotated[str, Field( min_length=1, max_length=64, pattern=r"^[a-zA-Z0-9_-]+$", description="Target host for deployment" )] # Stack name with Docker Compose constraints stack_name: Annotated[str, Field( min_length=1, max_length=63, # Docker service name limit pattern=r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", description="Stack name (must follow Docker naming rules)" )] # Compose content with size validation compose_content: Annotated[str, Field( min_length=50, # Must be substantial max_length=1024*1024, # 1MB limit description="Docker Compose YAML content" )] # Environment with secret detection environment: Annotated[dict[str, str], Field( default_factory=dict, description="Environment variables for the stack" )] # Boolean options with defaults pull_images: Annotated[bool, Field( default=True, description="Pull latest images before deployment" )] recreate: Annotated[bool, Field( default=False, description="Recreate containers even if no changes" )] ``` ### Pydantic v2 Model Dependencies - **Pydantic v2**: Latest validation and serialization features - **ConfigDict**: Modern configuration approach (replaces Config class) - **Annotated**: Python 3.9+ annotation metadata - **Field**: Enhanced field configuration with better validation - **Literal**: Type-safe enum-like constants - **computed fields**: Dynamic properties with @property or computed_field ## Model Categories ### Information Models (Core Entity Data) ```python class ContainerInfo(BaseModel): """Static information about a Docker container.""" container_id: str name: str image: str status: str host_id: str class HostInfo(BaseModel): """Configuration information about a Docker host.""" host_id: str hostname: str user: str port: int = 22 ``` ### Status Models (Real-time State) ```python class HostStatus(BaseModel): """Current status of a Docker host.""" host_id: str online: bool ssh_connected: bool docker_connected: bool error_message: str | None = None last_check: str # ISO timestamp response_time_ms: float | None = None ``` ### Statistics Models (Performance Metrics) ```python class ContainerStats(BaseModel): """Resource statistics for a container.""" container_id: str host_id: str cpu_percentage: float | None = None memory_usage: int | None = None # bytes memory_limit: int | None = None # bytes network_rx: int | None = None # bytes network_tx: int | None = None # bytes ``` ### Request Models (API Input) ```python class DeployStackRequest(BaseModel): """Request to deploy a Docker Compose stack.""" host_id: str stack_name: str compose_content: str environment: dict[str, str] = Field(default_factory=dict) pull_images: bool = True recreate: bool = False ``` ### Response Models (Complex Output) ```python class PortListResponse(BaseModel): """Complete port listing response with analysis.""" host_id: str total_ports: int total_containers: int port_mappings: list[PortMapping] conflicts: list[PortConflict] = Field(default_factory=list) summary: dict[str, Any] = Field(default_factory=dict) timestamp: str ``` ## Field Patterns ### Required vs Optional Fields ```python # Required fields (no default value) container_id: str host_id: str name: str # Optional fields with None default description: str | None = None error_message: str | None = None docker_version: str | None = None # Optional fields with value defaults port: int = 22 enabled: bool = True pull_images: bool = True ``` ### Mutable Default Handling ```python # CORRECT: Use Field(default_factory=...) for mutable defaults ports: list[dict[str, Any]] = Field(default_factory=list) labels: dict[str, str] = Field(default_factory=dict) tags: list[str] = Field(default_factory=list) environment: dict[str, str] = Field(default_factory=dict) # INCORRECT: Never use mutable defaults directly # ports: list[dict[str, Any]] = [] # This causes shared state bugs! ``` ### Type Annotations ```python # Modern Python 3.11+ union syntax (PREFERRED) description: str | None = None memory_usage: int | None = None load_average: list[float] | None = None # Legacy Union syntax (avoid if possible) from typing import Union, Optional description: Optional[str] = None memory_usage: Union[int, None] = None ``` ### Unit Documentation ```python # Always document units in comments for numeric fields memory_usage: int | None = None # bytes memory_limit: int | None = None # bytes response_time_ms: float | None = None # milliseconds disk_total: int | None = None # bytes port: int = 22 # TCP port number ``` ## Common Field Patterns ### Resource Identifiers ```python # Always include host context for multi-host operations host_id: str # Host identifier (required) container_id: str # Container identifier stack_name: str # Stack identifier ``` ### Timestamp Fields ```python # Use ISO format strings for timestamps created: str | None = None # ISO 8601 format updated: str | None = None # ISO 8601 format timestamp: str # Current operation timestamp last_check: str # ISO 8601 format last_ping: str | None = None # ISO 8601 format ``` ### Status Fields ```python # Boolean status flags online: bool enabled: bool = True connected: bool = False ssh_connected: bool docker_connected: bool # String status descriptions status: str # Human-readable status state: str # Machine state error_message: str | None = None # Error details when failed ``` ### Collection Fields ```python # Always use default_factory for collections services: list[str] = Field(default_factory=list) tags: list[str] = Field(default_factory=list) labels: dict[str, str] = Field(default_factory=dict) environment: dict[str, str] = Field(default_factory=dict) ports: list[dict[str, Any]] = Field(default_factory=list) ``` ## Naming Conventions ### Model Names ```python # Pattern: {Resource}{Purpose} ContainerInfo # Entity information HostStatus # Current status ContainerStats # Performance metrics DeployStackRequest # API request PortListResponse # API response ContainerAction # Operation request LogStreamRequest # Configuration ``` ### Field Names ```python # Use snake_case consistently container_id # Not containerId host_id # Not hostId memory_usage # Not memoryUsage response_time_ms # Not responseTimeMs ssh_connected # Not sshConnected ``` ## Modern Validation Patterns (Pydantic v2) ### Field-Level Validation with Annotated ```python from typing import Annotated from pydantic import Field, field_validator, model_validator # Modern constraint-based validation class HostInfo(BaseModel): """Host configuration with comprehensive validation.""" # Port validation with constraints port: Annotated[int, Field( ge=1, le=65535, description="Valid TCP port number", json_schema_extra={"examples": [22, 80, 443, 8080]} )] = 22 # Hostname with pattern validation hostname: Annotated[str, Field( min_length=1, max_length=253, # RFC limit pattern=r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$", description="Valid hostname or IP address" )] # SSH user validation user: Annotated[str, Field( min_length=1, max_length=32, pattern=r"^[a-z_][a-z0-9_-]*$", description="Valid Unix username" )] # Optional SSH key path validation ssh_key_path: Annotated[str | None, Field( default=None, min_length=1, max_length=4096, description="Path to SSH private key file" )] # Model-level field validator (Pydantic v2 syntax) @field_validator('port') @classmethod def validate_port_not_reserved(cls, v: int) -> int: """Ensure port is not a system reserved port below 1024 unless common.""" common_system_ports = {22, 80, 443} if v < 1024 and v not in common_system_ports: raise ValueError(f'Port {v} is reserved. Use ports > 1024 or common ports: {common_system_ports}') return v # Model validator for cross-field validation (Pydantic v2) @model_validator(mode='after') def validate_ssh_key_exists(self) -> 'HostInfo': """Validate SSH key file exists if specified.""" if self.ssh_key_path: import os if not os.path.exists(self.ssh_key_path): raise ValueError(f'SSH key file not found: {self.ssh_key_path}') # Check key file permissions stat_info = os.stat(self.ssh_key_path) if stat_info.st_mode & 0o077: # Check if group/other have any permissions raise ValueError(f'SSH key file has insecure permissions: {self.ssh_key_path}') return self # Advanced validation with custom types from pydantic import BeforeValidator, PlainValidator from typing import Any def validate_memory_size(value: Any) -> int: """Convert memory size strings to bytes.""" if isinstance(value, str): value = value.lower().strip() units = { 'b': 1, 'kb': 1024, 'k': 1024, 'mb': 1024**2, 'm': 1024**2, 'gb': 1024**3, 'g': 1024**3, 'tb': 1024**4, 't': 1024**4 } for unit, multiplier in units.items(): if value.endswith(unit): size_str = value[:-len(unit)].strip() try: return int(float(size_str) * multiplier) except ValueError: break # Try parsing as plain number try: return int(value) except ValueError: raise ValueError(f'Invalid memory size format: {value}') return int(value) MemorySize = Annotated[int, BeforeValidator(validate_memory_size)] class ContainerConstraints(BaseModel): """Container resource constraints with smart parsing.""" # Memory limit with smart parsing memory_limit: MemorySize | None = Field( default=None, description="Memory limit (supports: 512m, 1g, 2gb, 1024, etc.)" ) # CPU limit with validation cpu_limit: Annotated[float | None, Field( default=None, ge=0.001, # Minimum 0.1% CPU le=64.0, # Maximum 64 CPUs description="CPU limit in cores (e.g., 0.5, 1.5, 2.0)" )] # Custom field validator @field_validator('memory_limit', mode='after') @classmethod def validate_reasonable_memory(cls, v: int | None) -> int | None: """Ensure memory limit is reasonable.""" if v is not None: if v < 4 * 1024 * 1024: # 4MB minimum raise ValueError('Memory limit too small (minimum 4MB)') if v > 1024 * 1024 * 1024 * 1024: # 1TB maximum raise ValueError('Memory limit too large (maximum 1TB)') return v ``` ### Async Validation (Pydantic v2) ```python import asyncio from pydantic import field_validator, ValidationInfo class AsyncValidatedHost(BaseModel): """Host model with async validation capabilities.""" hostname: str port: int = 22 @field_validator('hostname') @classmethod def validate_hostname_format(cls, v: str) -> str: """Sync validation for hostname format.""" if not v or len(v) > 253: raise ValueError('Invalid hostname length') return v # For async validation, use custom methods called after model creation async def async_validate_connectivity(self, timeout: float = 5.0) -> bool: """Async method to validate host connectivity.""" try: import asyncio import socket # Create connection with timeout future = asyncio.open_connection(self.hostname, self.port) reader, writer = await asyncio.wait_for(future, timeout=timeout) writer.close() await writer.wait_closed() return True except Exception: return False async def async_validate_docker_available(self) -> bool: """Check if Docker is available on the host.""" # This would use SSH or Docker context to check # Implementation depends on your context manager return True # Placeholder # Usage pattern for async validation async def create_validated_host(hostname: str, port: int = 22) -> AsyncValidatedHost: """Create host with full async validation.""" # Create model (sync validation) host = AsyncValidatedHost(hostname=hostname, port=port) # Perform async validation if not await host.async_validate_connectivity(): raise ValueError(f'Cannot connect to {hostname}:{port}') if not await host.async_validate_docker_available(): raise ValueError(f'Docker not available on {hostname}') return host ``` ## Modern Type Features (Python 3.11+) ### Generic Models with TypeVars ```python from typing import TypeVar, Generic from pydantic import BaseModel T = TypeVar('T') U = TypeVar('U') class OperationResult(BaseModel, Generic[T]): """Generic result model for type-safe operations.""" success: bool data: T | None = None error: str | None = None timestamp: datetime = Field(default_factory=datetime.now) # Type-safe factory methods @classmethod def success_result(cls, data: T) -> 'OperationResult[T]': return cls(success=True, data=data) @classmethod def error_result(cls, error: str) -> 'OperationResult[T]': return cls(success=False, error=error) # Usage with full type safety container_result: OperationResult[ContainerInfo] = OperationResult.success_result( ContainerInfo(container_id="abc123", name="web", host_id="prod-1") ) stats_result: OperationResult[list[ContainerStats]] = OperationResult.success_result([ ContainerStats(container_id="abc123", host_id="prod-1") ]) # Type-safe access if container_result.success: container: ContainerInfo | None = container_result.data # Type checker knows this ``` ### Union Types with Discriminated Models ```python from typing import Literal, Union from pydantic import BaseModel, Field, Discriminator class DockerEvent(BaseModel): """Base class for Docker events.""" timestamp: datetime host_id: str class ContainerStartEvent(DockerEvent): """Container started event.""" event_type: Literal["container_start"] = "container_start" container_id: str container_name: str class ContainerStopEvent(DockerEvent): """Container stopped event.""" event_type: Literal["container_stop"] = "container_stop" container_id: str exit_code: int | None = None class StackDeployEvent(DockerEvent): """Stack deployment event.""" event_type: Literal["stack_deploy"] = "stack_deploy" stack_name: str services: list[str] success: bool # Discriminated union for type safety DockerEventUnion = Union[ ContainerStartEvent, ContainerStopEvent, StackDeployEvent ] class EventLog(BaseModel): """Event log with discriminated union types.""" events: list[DockerEventUnion] = Field( discriminator='event_type', # Pydantic v2 discriminated unions default_factory=list ) def add_event(self, event: DockerEventUnion) -> None: """Add event with type safety.""" self.events.append(event) def get_container_events(self) -> list[ContainerStartEvent | ContainerStopEvent]: """Get only container-related events with type narrowing.""" return [ event for event in self.events if isinstance(event, (ContainerStartEvent, ContainerStopEvent)) ] ``` ### Computed Fields and Properties (Pydantic v2) ```python from pydantic import computed_field from functools import cached_property class EnhancedContainerInfo(BaseModel): """Container info with computed properties.""" container_id: str name: str host_id: str status: Literal["created", "restarting", "running", "removing", "paused", "exited", "dead"] memory_usage: int | None = None # bytes memory_limit: int | None = None # bytes cpu_percentage: float | None = None # Computed field (serialized) @computed_field @property def is_healthy(self) -> bool: """Computed field indicating if container is healthy.""" if self.status != "running": return False if self.cpu_percentage and self.cpu_percentage > 90: return False if (self.memory_usage and self.memory_limit and self.memory_usage / self.memory_limit > 0.95): return False return True # Regular property (not serialized) @property def memory_usage_percentage(self) -> float | None: """Memory usage as percentage (not serialized).""" if self.memory_usage and self.memory_limit: return (self.memory_usage / self.memory_limit) * 100 return None # Cached property for expensive computations @cached_property def resource_score(self) -> float: """Complex resource health score (cached).""" score = 100.0 if self.status != "running": return 0.0 if self.cpu_percentage: score -= max(0, self.cpu_percentage - 50) # Penalty above 50% if self.memory_usage_percentage: score -= max(0, self.memory_usage_percentage - 70) # Penalty above 70% return max(0.0, min(100.0, score)) # Serialization includes computed fields container = EnhancedContainerInfo( container_id="abc123", name="web-server", host_id="prod-1", status="running", memory_usage=512 * 1024 * 1024, memory_limit=1024 * 1024 * 1024, cpu_percentage=25.5 ) data = container.model_dump() # Includes 'is_healthy' computed field print(data["is_healthy"]) # True print(container.resource_score) # 74.5 (cached after first access) ``` ### Advanced Serialization Control ```python from pydantic import BaseModel, Field, field_serializer, model_serializer from typing import Any class AdvancedContainerStats(BaseModel): """Container stats with advanced serialization control.""" container_id: str host_id: str memory_usage: int | None = None # bytes memory_limit: int | None = None # bytes network_rx: int | None = None # bytes network_tx: int | None = None # bytes uptime: float | None = None # seconds # Custom field serialization @field_serializer('memory_usage', 'memory_limit', 'network_rx', 'network_tx') def serialize_bytes(self, value: int | None) -> dict[str, Any] | None: """Serialize byte values with human-readable format.""" if value is None: return None return { "bytes": value, "human": self._format_bytes(value) } @field_serializer('uptime') def serialize_uptime(self, value: float | None) -> dict[str, Any] | None: """Serialize uptime with human-readable format.""" if value is None: return None return { "seconds": value, "human": self._format_duration(value) } # Custom model serialization @model_serializer(mode='wrap') def serialize_model(self, serializer, info): """Custom model serialization with metadata.""" # Get default serialization data = serializer(self) # Add metadata data['_metadata'] = { 'serialized_at': datetime.now().isoformat(), 'version': '1.0', 'host_id': self.host_id } return data def _format_bytes(self, bytes_value: int) -> str: """Format bytes to human-readable string.""" for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if bytes_value < 1024.0: return f"{bytes_value:.1f}{unit}" bytes_value /= 1024.0 return f"{bytes_value:.1f}PB" def _format_duration(self, seconds: float) -> str: """Format duration to human-readable string.""" if seconds < 60: return f"{seconds:.1f}s" elif seconds < 3600: return f"{seconds/60:.1f}m" elif seconds < 86400: return f"{seconds/3600:.1f}h" else: return f"{seconds/86400:.1f}d" # Example usage stats = AdvancedContainerStats( container_id="abc123", host_id="prod-1", memory_usage=512 * 1024 * 1024, # 512MB uptime=3665.5 # ~1 hour ) serialized = stats.model_dump() # Output includes: # { # "memory_usage": {"bytes": 536870912, "human": "512.0MB"}, # "uptime": {"seconds": 3665.5, "human": "1.0h"}, # "_metadata": {"serialized_at": "2024-01-01T12:00:00", ...} # } ``` ## Model Usage Patterns ### Modern Creation and Serialization (Pydantic v2) ```python # Create model with enhanced validation stats = ContainerStats( container_id="abc123", host_id="production-1", cpu_percentage=45.2, memory_usage=512 * 1024 * 1024 # 512MB in bytes ) # Pydantic v2 serialization methods return stats.model_dump() # Dictionary with all fields return stats.model_dump(exclude={'host_id'}) # Exclude specific fields return stats.model_dump(include={'cpu_percentage', 'memory_usage'}) # Include only specific # JSON serialization with options json_data = stats.model_dump_json( exclude_none=True, # Don't include None values by_alias=True, # Use field aliases if defined indent=2 # Pretty printing ) # Model copying with updates updated_stats = stats.model_copy(update={'cpu_percentage': 50.0}) ``` ### Type-Safe Model Composition ```python # Models with nested type safety class TypedPortListResponse(BaseModel, Generic[T]): """Type-safe port list response.""" host_id: str port_mappings: list[PortMapping] additional_data: T | None = None # Generic additional data # Type-safe methods def get_port_by_number(self, port: int) -> PortMapping | None: """Get port mapping by port number with type safety.""" return next( (mapping for mapping in self.port_mappings if mapping.host_port == str(port)), None ) class PortMapping(BaseModel): """Individual port mapping with validation.""" host_port: Annotated[str, Field(pattern=r"^\d+$")] container_port: Annotated[str, Field(pattern=r"^\d+(/tcp|/udp)?$")] protocol: Literal["tcp", "udp"] = "tcp" @computed_field @property def port_number(self) -> int: """Get port as integer.""" return int(self.host_port) ``` ## Error Handling ### Validation Errors ```python from pydantic import ValidationError try: container = ContainerInfo( container_id="", # Invalid empty string name="test", host_id="prod-1" ) except ValidationError as e: # Handle validation errors logger.error("Model validation failed", errors=e.errors()) ``` ### Flexible Field Access ```python # Models provide safe field access stats = ContainerStats(container_id="abc", host_id="prod") # Safe access - returns None for optional fields cpu_usage = stats.cpu_percentage # Returns None if not set # Dictionary access for dynamic fields stats_dict = stats.model_dump() memory = stats_dict.get("memory_usage", 0) # Default to 0 if None ``` ## Integration Patterns ### With Tools Layer ```python # Tools create and return model data async def get_container_stats(self, host_id: str, container_id: str): raw_stats = await self._get_raw_stats(host_id, container_id) # Create validated model stats = ContainerStats( container_id=container_id, host_id=host_id, cpu_percentage=self._parse_percentage(raw_stats.get("cpu")), memory_usage=self._parse_memory(raw_stats.get("memory")) ) return stats.model_dump() ``` ### With Services Layer ```python # Services receive model data from tools async def format_container_stats(self, stats_data: dict): # Validate and create model stats = ContainerStats(**stats_data) # Format for user display return f"CPU: {stats.cpu_percentage}%, Memory: {self._format_bytes(stats.memory_usage)}" ``` ## Model Testing ### Test Data Creation ```python def test_container_stats(): stats = ContainerStats( container_id="test-123", host_id="test-host", cpu_percentage=25.5, memory_usage=1024 * 1024 * 512 # 512MB ) assert stats.cpu_percentage == 25.5 assert stats.memory_usage == 536870912 assert stats.host_id == "test-host" ``` Models provide type-safe data structures with automatic validation, ensuring data consistency and reducing runtime errors across the entire application stack.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/docker-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server