import html import logging from collections import defaultdict from enum import Enum from types import NoneType from typing import Dict, List, Any from envipy_additional_information import Interval, EnviPyModel from envipy_additional_information import NAME_MAPPING from pydantic import BaseModel, HttpUrl logger = logging.getLogger(__name__) class HTMLGenerator: registry = {x.__name__: x for x in NAME_MAPPING.values()} @staticmethod def generate_html(additional_information: 'EnviPyModel', prefix='') -> str: from typing import get_origin, get_args, Union if isinstance(additional_information, type): clz_name = additional_information.__name__ else: clz_name = additional_information.__class__.__name__ widget = f'

{clz_name}

' if hasattr(additional_information, 'uuid'): uuid = additional_information.uuid widget += f'' for name, field in additional_information.model_fields.items(): value = getattr(additional_information, name, None) full_name = f"{clz_name}__{prefix}__{name}" annotation = field.annotation base_type = get_origin(annotation) or annotation # Optional[Interval[float]] alias for Union[X, None] if base_type is Union: for arg in get_args(annotation): if arg is not NoneType: field_type = arg break else: field_type = base_type is_interval_float = ( field_type == Interval[float] or str(field_type) == str(Interval[float]) or 'Interval[float]' in str(field_type) ) if is_interval_float: widget += f"""
""" elif issubclass(field_type, Enum): options: str = '' for e in field_type: options += f'' widget += f"""
""" else: if field_type == str or field_type == HttpUrl: input_type = 'text' elif field_type == float or field_type == int: input_type = 'number' elif field_type == bool: input_type = 'checkbox' else: raise ValueError(f"Could not parse field type {field_type} for {name}") value_to_use = value if value and field_type != bool else '' widget += f"""
""" return widget + "
" @staticmethod def build_models(params) -> Dict[str, List['EnviPyModel']]: def has_non_none(d): """ Recursively checks if any value in a (possibly nested) dict is not None. """ for value in d.values(): if isinstance(value, dict): if has_non_none(value): # recursive check return True elif value is not None: return True return False """ Build Pydantic model instances from flattened HTML parameters. Args: params: dict of {param_name: value}, e.g. form data model_registry: mapping of class names (strings) to Pydantic model classes Returns: dict: {ClassName: [list of model instances]} """ grouped: Dict[str, Dict[str, Dict[str, Any]]] = {} # Step 1: group fields by ClassName and Number for key, value in params.items(): if value == '': value = None parts = key.split("__") if len(parts) < 3: continue # skip invalid keys class_name, number, *field_parts = parts grouped.setdefault(class_name, {}).setdefault(number, {}) # handle nested fields like interval__start target = grouped[class_name][number] current = target for p in field_parts[:-1]: current = current.setdefault(p, {}) current[field_parts[-1]] = value # Step 2: instantiate Pydantic models instances: Dict[str, List[BaseModel]] = defaultdict(list) for class_name, number_dict in grouped.items(): model_cls = HTMLGenerator.registry.get(class_name) if not model_cls: logger.info(f"Could not find model class for {class_name}") continue for number, fields in number_dict.items(): if not has_non_none(fields): print(f"Skipping empty {class_name} {number} {fields}") continue uuid = fields.pop('uuid', None) instance = model_cls(**fields) if uuid: instance.__dict__['uuid'] = uuid instances[class_name].append(instance) return instances