309 lines
12 KiB
Python
309 lines
12 KiB
Python
"""
|
|
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
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Define the AttributeDefinition class here instead of importing it
|
|
class AttributeDefinition(BaseModel):
|
|
"""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[Union[str, List[Any]]] = None # Can be a string reference (e.g., "user.role") or a list of options
|
|
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 registerModelLabels(modelName: str, modelLabel: Dict[str, str], labels: Dict[str, Dict[str, str]]):
|
|
"""
|
|
Register labels for a model's attributes and the model itself.
|
|
|
|
Args:
|
|
modelName: Name of the model class
|
|
modelLabel: 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[modelName] = {"model": modelLabel, "attributes": labels}
|
|
|
|
|
|
def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
|
|
"""
|
|
Get labels for a model's attributes in the specified language.
|
|
|
|
Args:
|
|
modelName: Name of the model class
|
|
language: Language code (default: "en")
|
|
|
|
Returns:
|
|
Dictionary mapping attribute names to their labels in the specified language
|
|
"""
|
|
modelData = MODEL_LABELS.get(modelName, {})
|
|
attributeLabels = modelData.get("attributes", {})
|
|
|
|
return {
|
|
attr: translations.get(language, translations.get("en", attr))
|
|
for attr, translations in attributeLabels.items()
|
|
}
|
|
|
|
|
|
def getModelLabel(modelName: str, language: str = "en") -> str:
|
|
"""
|
|
Get the label for a model in the specified language.
|
|
|
|
Args:
|
|
modelName: Name of the model class
|
|
language: Language code (default: "en")
|
|
|
|
Returns:
|
|
Model label in the specified language, or model name if no label exists
|
|
"""
|
|
modelData = MODEL_LABELS.get(modelName, {})
|
|
modelLabel = modelData.get("model", {})
|
|
return modelLabel.get(language, modelLabel.get("en", modelName))
|
|
|
|
|
|
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 = getModelLabels(model_name, userLanguage)
|
|
model_label = getModelLabel(model_name, userLanguage)
|
|
|
|
# Pydantic v2 only
|
|
fields = modelClass.model_fields
|
|
for name, field in fields.items():
|
|
# Extract frontend metadata from field info
|
|
# In Pydantic v2, the field from model_fields.items() IS the FieldInfo object
|
|
# FieldInfo objects have the 'extra' dict directly on them
|
|
field_info = field
|
|
# 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 (though these won't exist for custom kwargs)
|
|
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 json_schema_extra (Pydantic v2 stores custom kwargs here)
|
|
if frontend_type is None and hasattr(field_info, "json_schema_extra"):
|
|
json_extra = field_info.json_schema_extra
|
|
if isinstance(json_extra, dict):
|
|
frontend_type = json_extra.get("frontend_type")
|
|
elif callable(json_extra):
|
|
# If it's a callable, we can't easily extract from it, skip
|
|
pass
|
|
|
|
# Check extra field (Pydantic v2 stores custom kwargs here)
|
|
# This is the main location where frontend_type is stored
|
|
if hasattr(field_info, "extra"):
|
|
extra_dict = field_info.extra
|
|
if isinstance(extra_dict, dict):
|
|
if frontend_type is None:
|
|
frontend_type = extra_dict.get("frontend_type")
|
|
|
|
# Also extract other fields from extra if it exists
|
|
if hasattr(field_info, "extra") and isinstance(field_info.extra, dict):
|
|
extra_dict = field_info.extra
|
|
if not frontend_readonly:
|
|
frontend_readonly = extra_dict.get("frontend_readonly", False)
|
|
if frontend_required == field.is_required():
|
|
frontend_required = extra_dict.get("frontend_required", frontend_required)
|
|
if frontend_options is None:
|
|
frontend_options = extra_dict.get("frontend_options")
|
|
|
|
# Also check json_schema_extra for other fields
|
|
if hasattr(field_info, "json_schema_extra"):
|
|
json_extra = field_info.json_schema_extra
|
|
if isinstance(json_extra, dict):
|
|
if not frontend_readonly and "frontend_readonly" in json_extra:
|
|
frontend_readonly = json_extra.get("frontend_readonly", False)
|
|
if frontend_required == field.is_required() and "frontend_required" in json_extra:
|
|
frontend_required = json_extra.get("frontend_required", frontend_required)
|
|
if frontend_options is None and "frontend_options" in json_extra:
|
|
frontend_options = json_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)
|
|
)
|
|
)
|
|
|
|
# Extract default value from field
|
|
# In Pydantic v2, FieldInfo has a 'default' attribute
|
|
field_default = None
|
|
if hasattr(field_info, 'default'):
|
|
default_value = field_info.default
|
|
# Handle default_factory (callable that generates default)
|
|
if hasattr(field_info, 'default_factory') and callable(field_info.default_factory):
|
|
# Don't call it here - it's meant to be called per-instance
|
|
# Instead, store a marker that indicates it exists
|
|
field_default = None # Frontend should use first option or specific logic
|
|
elif default_value is not ...: # Ellipsis means no default
|
|
# Convert enum to its value if it's an enum
|
|
if hasattr(default_value, 'value'):
|
|
field_default = default_value.value
|
|
else:
|
|
field_default = default_value
|
|
|
|
# Safely get description
|
|
description = ""
|
|
try:
|
|
if hasattr(field_info, "description") and field_info.description:
|
|
description = str(field_info.description)
|
|
except Exception:
|
|
pass
|
|
|
|
attributes.append(
|
|
{
|
|
"name": name,
|
|
"type": field_type,
|
|
"required": frontend_required,
|
|
"description": description,
|
|
"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,
|
|
"default": field_default,
|
|
}
|
|
)
|
|
|
|
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 in interfaces directory
|
|
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
|
|
|
|
# Also get models from datamodels directory
|
|
datamodels_dir = os.path.join(
|
|
os.path.dirname(os.path.dirname(__file__)), "datamodels"
|
|
)
|
|
|
|
# Find all model files in datamodels directory
|
|
for fileName in os.listdir(datamodels_dir):
|
|
if fileName.startswith("datamodel") and fileName.endswith(".py"):
|
|
# Convert fileName to module name (e.g., datamodelUtils.py -> datamodelUtils)
|
|
module_name = fileName[:-3]
|
|
|
|
try:
|
|
# Import the module dynamically
|
|
module = importlib.import_module(f"modules.datamodels.{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
|
|
except Exception as e:
|
|
logger.warning(f"Error importing module {module_name}: {str(e)}", exc_info=True)
|
|
# Continue with other modules even if one fails
|
|
|
|
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,
|
|
}
|
|
]
|
|
}
|
|
}
|
|
)
|