Source code for belso.translator.serialization.xml_format

from os import PathLike
from pathlib import Path
from typing import Optional
import xml.dom.minidom as minidom
import xml.etree.ElementTree as ET
from typing import Any, Type, Union

from belso.schemas import Schema, Field
from belso.utils.logging import get_logger

# Get a module-specific logger
logger = get_logger(__name__)

[docs] def schema_to_xml( schema: Type[Schema], file_path: Optional[Union[str, Path, PathLike]] = None ) -> str: """ Convert a belso Schema to XML format and optionally save to a file.\n --- ### Args - `schema` (`Type[Schema]`): the schema to convert.\n - `file_path` (`Optional[Union[str, Path, PathLike]]`): path to save the XML to a file.\n --- ### Returns - `str`: the schema in XML format. """ try: schema_name = schema.name if hasattr(schema, "name") else "unnamed" logger.debug(f"Starting conversion of schema '{schema_name}' to XML format...") # Create root element root = ET.Element("schema") root.set("name", schema.name) logger.debug(f"Created root element with name: {schema.name}.") # Add fields fields_elem = ET.SubElement(root, "fields") logger.debug(f"Processing {len(schema.fields)} fields...") for field in schema.fields: logger.debug(f"Processing field '{field.name}'...") field_elem = ET.SubElement(fields_elem, "field") field_elem.set("name", field.name) # Convert Python type to string representation type_str = field.type.__name__ if hasattr(field.type, "__name__") else str(field.type) field_elem.set("type", type_str) logger.debug(f"Field '{field.name}' has type: {type_str}.") field_elem.set("required", str(field.required).lower()) required_status = "required" if field.required else "optional" logger.debug(f"Field '{field.name}' is {required_status}") # Add description as a child element if field.description: desc_elem = ET.SubElement(field_elem, "description") desc_elem.text = field.description logger.debug(f"Field '{field.name}' has description: '{field.description}'.") # Add default value if it exists if field.default is not None: default_elem = ET.SubElement(field_elem, "default") default_elem.text = str(field.default) logger.debug(f"Field '{field.name}' has default value: {field.default}.") # Convert to string with pretty formatting logger.debug("Converting XML to string with pretty formatting...") rough_string = ET.tostring(root, 'utf-8') reparsed = minidom.parseString(rough_string) xml_str = reparsed.toprettyxml(indent=" ") # Save to file if path is provided if file_path: logger.debug(f"Saving XML schema to file: {file_path}.") try: with open(file_path, 'w', encoding='utf-8') as f: f.write(xml_str) logger.debug(f"Successfully saved XML schema to {file_path}.") except Exception as e: logger.error(f"Failed to save XML schema to file: {e}") logger.debug("File saving error details", exc_info=True) logger.debug("Successfully converted schema to XML format.") return xml_str except Exception as e: logger.error(f"Error converting schema to XML format: {e}") logger.debug("Conversion error details", exc_info=True) return "<schema><fields></fields></schema>"
[docs] def xml_to_schema(xml_input: Union[str, ET.Element]) -> Type[Schema]: """ Convert XML data or an XML file to a belso Schema.\n --- ### Args - `xml_input`: either an XML string, Element, or a file path to an XML file.\n --- ### Returns - `Type[Schema]`: the belso Schema. """ try: logger.debug("Starting conversion from XML to belso Schema...") # Parse input if isinstance(xml_input, str): # Check if it's a file path if "<" not in xml_input: # Simple heuristic to check if it's XML content logger.debug(f"Attempting to load XML from file: {xml_input}.") try: tree = ET.parse(xml_input) root = tree.getroot() logger.debug(f"Successfully loaded XML from file: {xml_input}.") except (FileNotFoundError, ET.ParseError) as e: logger.error(f"Failed to load XML from file: {e}") logger.debug("File loading error details", exc_info=True) raise ValueError(f"Failed to load XML from file: {e}") else: # It's an XML string logger.debug("Parsing XML from string...") try: root = ET.fromstring(xml_input) logger.debug("Successfully parsed XML string.") except ET.ParseError as e: logger.error(f"Failed to parse XML string: {e}") logger.debug("XML parsing error details", exc_info=True) raise ValueError(f"Failed to parse XML string: {e}") else: # Assume it's an ElementTree Element logger.debug("Using provided ElementTree Element...") root = xml_input # Create a new Schema class schema_name = root.get("name", "LoadedSchema") logger.debug(f"Creating new Schema class with name: {schema_name}.") class LoadedSchema(Schema): name = schema_name fields = [] # Type mapping from string to Python types type_mapping = { "str": str, "int": int, "float": float, "bool": bool, "list": list, "dict": dict, "any": Any } # Process each field fields_elem = root.find("fields") if fields_elem is not None: fields_count = len(fields_elem.findall("field")) logger.debug(f"Found {fields_count} fields in XML...") for field_elem in fields_elem.findall("field"): name = field_elem.get("name", "") field_type_str = field_elem.get("type", "str") field_type = type_mapping.get(field_type_str.lower(), str) logger.debug(f"Processing field '{name}' with type '{field_type_str}'...") # Get required attribute (default to True) required_str = field_elem.get("required", "true") required = required_str.lower() == "true" required_status = "required" if required else "optional" logger.debug(f"Field '{name}' is {required_status}.") # Get description desc_elem = field_elem.find("description") description = desc_elem.text if desc_elem is not None and desc_elem.text else "" if description: logger.debug(f"Field '{name}' has description: '{description}'.") # Get default value default = None default_elem = field_elem.find("default") if default_elem is not None and default_elem.text: # Convert default value to the appropriate type if field_type == bool: default = default_elem.text.lower() == "true" elif field_type == int: default = int(default_elem.text) elif field_type == float: default = float(default_elem.text) else: default = default_elem.text logger.debug(f"Field '{name}' has default value: {default}.") field = Field( name=name, type=field_type, description=description, required=required, default=default ) LoadedSchema.fields.append(field) logger.debug(f"Successfully created Schema with {len(LoadedSchema.fields)} fields.") return LoadedSchema except Exception as e: logger.error(f"Error converting XML to schema: {e}") logger.debug("Conversion error details", exc_info=True) # Return a minimal schema if conversion fails logger.warning("Returning fallback schema due to conversion error.") class FallbackSchema(Schema): name = "FallbackSchema" fields = [Field(name="text", type=str, description="Fallback field", required=True)] return FallbackSchema