gateway/modules/shared/attributeUtils.py
2025-10-24 23:57:17 +02:00

255 lines
8.5 KiB
Python

"""
Shared utilities for model attributes and labels.
"""
from pydantic import BaseModel, Field, ConfigDict
from typing import Dict, Any, List, Type, Optional
import inspect
import importlib
import os
# 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[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 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)
# Pydantic v2 only
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,
}
)
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]
# 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
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,
}
]
}
}
)