""" Shared utilities for model attributes and labels. """ from pydantic import BaseModel, Field, ConfigDict from typing import Dict, Any, List, Type, Optional, Union import inspect import importlib import os from datetime import datetime class ModelMixin: """Mixin class that provides serialization methods for Pydantic models.""" def to_dict(self) -> Dict[str, Any]: """ Convert a Pydantic model to a dictionary. Handles both Pydantic v1 and v2. All timestamp fields remain as float values. Returns: Dict[str, Any]: Dictionary representation of the model """ # Get the raw dictionary if hasattr(self, "model_dump"): data: Dict[str, Any] = self.model_dump() # Pydantic v2 else: data: Dict[str, Any] = self.dict() # Pydantic v1 # All fields (including timestamps) remain in their original format # No conversions needed - timestamps are already float return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ModelMixin": """ Create a Pydantic model instance from a dictionary. Args: data: Dictionary containing the model data Returns: ModelMixin: New instance of the model class """ return cls(**data) # Define the AttributeDefinition class here instead of importing it class AttributeDefinition(BaseModel, ModelMixin): """Definition of a model attribute with its metadata.""" name: str type: str label: str description: Optional[str] = None required: bool = False default: Any = None options: Optional[List[Any]] = None validation: Optional[Dict[str, Any]] = None ui: Optional[Dict[str, Any]] = None # New frontend metadata fields readonly: bool = False editable: bool = True visible: bool = True order: int = 0 placeholder: Optional[str] = None # Global registry for model labels MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {} def to_dict(model: BaseModel) -> Dict[str, Any]: """ Convert a Pydantic model to a dictionary. Handles both Pydantic v1 and v2. Args: model: The Pydantic model instance to convert Returns: Dict[str, Any]: Dictionary representation of the model """ if hasattr(model, "model_dump"): return model.model_dump() # Pydantic v2 return model.dict() # Pydantic v1 def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel: """ Create a Pydantic model instance from a dictionary. Args: model_class: The Pydantic model class to instantiate data: Dictionary containing the model data Returns: BaseModel: New instance of the model class """ return model_class(**data) def register_model_labels( model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]] ): """ Register labels for a model's attributes and the model itself. Args: model_name: Name of the model class model_label: Dictionary mapping language codes to model labels e.g. {"en": "Prompt", "fr": "Invite"} labels: Dictionary mapping attribute names to their translations e.g. {"name": {"en": "Name", "fr": "Nom"}} """ MODEL_LABELS[model_name] = {"model": model_label, "attributes": labels} def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]: """ Get labels for a model's attributes in the specified language. Args: model_name: Name of the model class language: Language code (default: "en") Returns: Dictionary mapping attribute names to their labels in the specified language """ model_data = MODEL_LABELS.get(model_name, {}) attribute_labels = model_data.get("attributes", {}) return { attr: translations.get(language, translations.get("en", attr)) for attr, translations in attribute_labels.items() } def get_model_label(model_name: str, language: str = "en") -> str: """ Get the label for a model in the specified language. Args: model_name: Name of the model class language: Language code (default: "en") Returns: Model label in the specified language, or model name if no label exists """ model_data = MODEL_LABELS.get(model_name, {}) model_label = model_data.get("model", {}) return model_label.get(language, model_label.get("en", model_name)) def getModelAttributeDefinitions( modelClass: Type[BaseModel] = None, userLanguage: str = "en" ) -> Dict[str, Any]: """ Get attribute definitions for a model class. Args: modelClass: The model class to get attributes for userLanguage: Language code for translations (default: "en") Returns: Dictionary containing model label and attribute definitions """ if not modelClass: return {} attributes = [] model_name = modelClass.__name__ labels = get_model_labels(model_name, userLanguage) model_label = get_model_label(model_name, userLanguage) # Handle both Pydantic v1 and v2 if hasattr(modelClass, "model_fields"): # Pydantic v2 fields = modelClass.model_fields for name, field in fields.items(): # Extract frontend metadata from field info field_info = field.field_info if hasattr(field, "field_info") else None # Check both direct attributes and extra field for frontend metadata frontend_type = None frontend_readonly = False frontend_required = field.is_required() frontend_options = None if field_info: # Try direct attributes first frontend_type = getattr(field_info, "frontend_type", None) frontend_readonly = getattr(field_info, "frontend_readonly", False) frontend_required = getattr( field_info, "frontend_required", frontend_required ) frontend_options = getattr(field_info, "frontend_options", None) # If not found, check extra field if hasattr(field_info, "extra") and field_info.extra: if frontend_type is None: frontend_type = field_info.extra.get("frontend_type") if not frontend_readonly: frontend_readonly = field_info.extra.get( "frontend_readonly", False ) if ( frontend_required == field.is_required() ): # Only override if we didn't get it from direct attribute frontend_required = field_info.extra.get( "frontend_required", frontend_required ) if frontend_options is None: frontend_options = field_info.extra.get("frontend_options") # Use frontend type if available, otherwise fall back to Python type field_type = ( frontend_type if frontend_type else ( field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation) ) ) attributes.append( { "name": name, "type": field_type, "required": frontend_required, "description": field.description if hasattr(field, "description") else "", "label": labels.get(name, name), "placeholder": f"Please enter {labels.get(name, name)}", "editable": not frontend_readonly, "visible": True, "order": len(attributes), "readonly": frontend_readonly, "options": frontend_options, } ) else: # Pydantic v1 fields = modelClass.__fields__ for name, field in fields.items(): # Extract frontend metadata from field info field_info = field.field_info if hasattr(field, "field_info") else None # Check both direct attributes and extra field for frontend metadata frontend_type = None frontend_readonly = False frontend_required = field.required frontend_options = None if field_info: # Try direct attributes first frontend_type = getattr(field_info, "frontend_type", None) frontend_readonly = getattr(field_info, "frontend_readonly", False) frontend_required = getattr( field_info, "frontend_required", frontend_required ) frontend_options = getattr(field_info, "frontend_options", None) # If not found, check extra field if hasattr(field_info, "extra") and field_info.extra: if frontend_type is None: frontend_type = field_info.extra.get("frontend_type") if not frontend_readonly: frontend_readonly = field_info.extra.get( "frontend_readonly", False ) if ( frontend_required == field.required ): # Only override if we didn't get it from direct attribute frontend_required = field_info.extra.get( "frontend_required", frontend_required ) if frontend_options is None: frontend_options = field_info.extra.get("frontend_options") # Use frontend type if available, otherwise fall back to Python type field_type = ( frontend_type if frontend_type else ( field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_) ) ) attributes.append( { "name": name, "type": field_type, "required": frontend_required, "description": field.field_info.description if hasattr(field.field_info, "description") else "", "label": labels.get(name, name), "placeholder": f"Please enter {labels.get(name, name)}", "editable": not frontend_readonly, "visible": True, "order": len(attributes), "readonly": frontend_readonly, "options": frontend_options, } ) return {"model": model_label, "attributes": attributes} def getModelClasses() -> Dict[str, Type[BaseModel]]: """ Dynamically get all model classes from all model modules. Returns: Dict[str, Type[BaseModel]]: Dictionary of model class names to their classes """ modelClasses = {} # Get the interfaces directory path interfaces_dir = os.path.join( os.path.dirname(os.path.dirname(__file__)), "interfaces" ) # Find all model files for fileName in os.listdir(interfaces_dir): if fileName.endswith("Model.py"): # Convert fileName to module name (e.g., gatewayModel.py -> gatewayModel) module_name = fileName[:-3] # Import the module dynamically module = importlib.import_module(f"modules.interfaces.{module_name}") # Get all classes from the module for name, obj in inspect.getmembers(module): if ( inspect.isclass(obj) and issubclass(obj, BaseModel) and obj != BaseModel ): modelClasses[name] = obj return modelClasses class AttributeResponse(BaseModel): """Response model for entity attributes""" attributes: List[AttributeDefinition] model_config = ConfigDict( json_schema_extra={ "example": { "attributes": [ { "name": "username", "label": "Username", "type": "string", "required": True, "placeholder": "Please enter username", "editable": True, "visible": True, "order": 0, } ] } } )