From 98e9fa7f31eed615eefe65e72d315717a87f39d4 Mon Sep 17 00:00:00 2001 From: coder Date: Sat, 3 Jan 2026 21:05:02 -0800 Subject: [PATCH 1/3] add to_xml --- .gitignore | 1 + src/mdreader/fmi2.py | 560 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 561 insertions(+) diff --git a/.gitignore b/.gitignore index fc5e5ad..4769a68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ reference_fmus/ +.tmp/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/src/mdreader/fmi2.py b/src/mdreader/fmi2.py index a68ea30..7022d52 100644 --- a/src/mdreader/fmi2.py +++ b/src/mdreader/fmi2.py @@ -163,6 +163,31 @@ class BaseUnit(BaseModel): Field(default=0.0, alias="offset", description="Offset for unit conversion"), ] = 0.0 + def to_xml(self) -> Element: + """Convert BaseUnit to XML Element""" + element = Element("BaseUnit") + if self.kg is not None and self.kg != 0: + element.set("kg", str(self.kg)) + if self.m is not None and self.m != 0: + element.set("m", str(self.m)) + if self.s is not None and self.s != 0: + element.set("s", str(self.s)) + if self.a is not None and self.a != 0: + element.set("A", str(self.a)) + if self.k is not None and self.k != 0: + element.set("K", str(self.k)) + if self.mol is not None and self.mol != 0: + element.set("mol", str(self.mol)) + if self.cd is not None and self.cd != 0: + element.set("cd", str(self.cd)) + if self.rad is not None and self.rad != 0: + element.set("rad", str(self.rad)) + if self.factor is not None and self.factor != 1.0: + element.set("factor", str(self.factor)) + if self.offset is not None and self.offset != 0.0: + element.set("offset", str(self.offset)) + return element + class DisplayUnit(BaseModel): """Display unit definition""" @@ -194,6 +219,16 @@ class DisplayUnit(BaseModel): ), ] = 0.0 + def to_xml(self) -> Element: + """Convert DisplayUnit to XML Element""" + element = Element("DisplayUnit") + element.set("name", self.name) + if self.factor is not None and self.factor != 1.0: + element.set("factor", str(self.factor)) + if self.offset is not None and self.offset != 0.0: + element.set("offset", str(self.offset)) + return element + class Unit(BaseModel): """Unit definition with base unit and display units""" @@ -225,6 +260,17 @@ class Unit(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert Unit to XML Element""" + element = Element("Unit") + element.set("name", self.name) + if self.base_unit is not None: + element.append(self.base_unit.to_xml()) + if self.display_units is not None: + for display_unit in self.display_units: + element.append(display_unit.to_xml()) + return element + class Item(BaseModel): """Enumeration item""" @@ -251,6 +297,15 @@ class Item(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert Item to XML Element""" + element = Element("Item") + element.set("name", self.name) + element.set("value", str(self.value)) + if self.description is not None: + element.set("description", self.description) + return element + class RealSimpleType(BaseModel): """Real simple type definition""" @@ -328,6 +383,27 @@ def check_min_max(self): ) return self + def to_xml(self) -> Element: + """Convert RealSimpleType to XML Element""" + element = Element("Real") + if self.quantity is not None: + element.set("quantity", self.quantity) + if self.unit is not None: + element.set("unit", self.unit) + if self.display_unit is not None: + element.set("displayUnit", self.display_unit) + if self.relative_quantity is not None and self.relative_quantity: + element.set("relativeQuantity", str(self.relative_quantity).lower()) + if self.min_value is not None: + element.set("min", str(self.min_value)) + if self.max_value is not None: + element.set("max", str(self.max_value)) + if self.nominal is not None: + element.set("nominal", str(self.nominal)) + if self.unbounded is not None and self.unbounded: + element.set("unbounded", str(self.unbounded).lower()) + return element + class IntegerSimpleType(BaseModel): """Integer simple type definition""" @@ -369,6 +445,17 @@ def check_min_max(self): ) return self + def to_xml(self) -> Element: + """Convert IntegerSimpleType to XML Element""" + element = Element("Integer") + if self.quantity is not None: + element.set("quantity", self.quantity) + if self.min_value is not None: + element.set("min", str(self.min_value)) + if self.max_value is not None: + element.set("max", str(self.max_value)) + return element + class BooleanSimpleType(BaseModel): """Boolean simple type definition""" @@ -376,6 +463,11 @@ class BooleanSimpleType(BaseModel): model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) pass # Boolean types have no specific attributes + def to_xml(self) -> Element: + """Convert BooleanSimpleType to XML Element""" + element = Element("Boolean") + return element + class StringSimpleType(BaseModel): """String simple type definition""" @@ -383,6 +475,11 @@ class StringSimpleType(BaseModel): model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) pass # String types have no specific attributes + def to_xml(self) -> Element: + """Convert StringSimpleType to XML Element""" + element = Element("String") + return element + class EnumerationSimpleType(BaseModel): """Enumeration simple type definition""" @@ -401,6 +498,16 @@ class EnumerationSimpleType(BaseModel): list[Item], Field(..., alias="Item", description="List of enumeration items") ] + def to_xml(self) -> Element: + """Convert EnumerationSimpleType to XML Element""" + element = Element("Enumeration") + if self.quantity is not None: + element.set("quantity", self.quantity) + if self.items is not None: + for item in self.items: + element.append(item.to_xml()) + return element + class SimpleType(BaseModel): """Simple type definition""" @@ -456,6 +563,27 @@ def get_type_category(self): return "Enumeration" return None + def to_xml(self) -> Element: + """Convert SimpleType to XML Element""" + element = Element("SimpleType") + element.set("name", self.name) + if self.description is not None: + element.set("description", self.description) + + # Add the appropriate type element + if self.real is not None: + element.append(self.real.to_xml()) + elif self.integer is not None: + element.append(self.integer.to_xml()) + elif self.boolean is not None: + element.append(self.boolean.to_xml()) + elif self.string is not None: + element.append(self.string.to_xml()) + elif self.enumeration is not None: + element.append(self.enumeration.to_xml()) + + return element + class File(BaseModel): """Source file definition""" @@ -464,6 +592,12 @@ class File(BaseModel): name: Annotated[str, Field(..., alias="name")] + def to_xml(self) -> Element: + """Convert File to XML Element""" + element = Element("File") + element.set("name", self.name) + return element + class SourceFiles(BaseModel): """List of source files""" @@ -472,6 +606,14 @@ class SourceFiles(BaseModel): files: Annotated[list[File], Field(..., alias="File")] + def to_xml(self) -> Element: + """Convert SourceFiles to XML Element""" + element = Element("SourceFiles") + if self.files is not None: + for file in self.files: + element.append(file.to_xml()) + return element + class ModelExchange(BaseModel): """Model Exchange interface definition""" @@ -505,6 +647,59 @@ class ModelExchange(BaseModel): SourceFiles | None, Field(default=None, alias="SourceFiles") ] = None + def to_xml(self) -> Element: + """Convert ModelExchange to XML Element""" + element = Element("ModelExchange") + element.set("modelIdentifier", self.model_identifier) + if self.needs_execution_tool is not None and self.needs_execution_tool: + element.set("needsExecutionTool", str(self.needs_execution_tool).lower()) + if ( + self.completed_integrator_step_not_needed is not None + and self.completed_integrator_step_not_needed + ): + element.set( + "completedIntegratorStepNotNeeded", + str(self.completed_integrator_step_not_needed).lower(), + ) + if ( + self.can_be_instantiated_only_once_per_process is not None + and self.can_be_instantiated_only_once_per_process + ): + element.set( + "canBeInstantiatedOnlyOncePerProcess", + str(self.can_be_instantiated_only_once_per_process).lower(), + ) + if ( + self.can_not_use_memory_management_functions is not None + and self.can_not_use_memory_management_functions + ): + element.set( + "canNotUseMemoryManagementFunctions", + str(self.can_not_use_memory_management_functions).lower(), + ) + if ( + self.can_get_and_set_fmu_state is not None + and self.can_get_and_set_fmu_state + ): + element.set( + "canGetAndSetFMUstate", str(self.can_get_and_set_fmu_state).lower() + ) + if self.can_serialize_fmu_state is not None and self.can_serialize_fmu_state: + element.set( + "canSerializeFMUstate", str(self.can_serialize_fmu_state).lower() + ) + if ( + self.provides_directional_derivative is not None + and self.provides_directional_derivative + ): + element.set( + "providesDirectionalDerivative", + str(self.provides_directional_derivative).lower(), + ) + if self.source_files is not None: + element.append(self.source_files.to_xml()) + return element + class CoSimulation(BaseModel): """Co-Simulation interface definition""" @@ -548,6 +743,74 @@ class CoSimulation(BaseModel): SourceFiles | None, Field(default=None, alias="SourceFiles") ] = None + def to_xml(self) -> Element: + """Convert CoSimulation to XML Element""" + element = Element("CoSimulation") + element.set("modelIdentifier", self.model_identifier) + if self.needs_execution_tool is not None and self.needs_execution_tool: + element.set("needsExecutionTool", str(self.needs_execution_tool).lower()) + if ( + self.can_handle_variable_communication_step_size is not None + and self.can_handle_variable_communication_step_size + ): + element.set( + "canHandleVariableCommunicationStepSize", + str(self.can_handle_variable_communication_step_size).lower(), + ) + if self.can_interpolate_inputs is not None and self.can_interpolate_inputs: + element.set( + "canInterpolateInputs", str(self.can_interpolate_inputs).lower() + ) + if ( + self.max_output_derivative_order is not None + and self.max_output_derivative_order != 0 + ): + element.set( + "maxOutputDerivativeOrder", str(self.max_output_derivative_order) + ) + if self.can_run_asynchronuously is not None and self.can_run_asynchronuously: + element.set( + "canRunAsynchronuously", str(self.can_run_asynchronuously).lower() + ) + if ( + self.can_be_instantiated_only_once_per_process is not None + and self.can_be_instantiated_only_once_per_process + ): + element.set( + "canBeInstantiatedOnlyOncePerProcess", + str(self.can_be_instantiated_only_once_per_process).lower(), + ) + if ( + self.can_not_use_memory_management_functions is not None + and self.can_not_use_memory_management_functions + ): + element.set( + "canNotUseMemoryManagementFunctions", + str(self.can_not_use_memory_management_functions).lower(), + ) + if ( + self.can_get_and_set_fmu_state is not None + and self.can_get_and_set_fmu_state + ): + element.set( + "canGetAndSetFMUstate", str(self.can_get_and_set_fmu_state).lower() + ) + if self.can_serialize_fmu_state is not None and self.can_serialize_fmu_state: + element.set( + "canSerializeFMUstate", str(self.can_serialize_fmu_state).lower() + ) + if ( + self.provides_directional_derivative is not None + and self.provides_directional_derivative + ): + element.set( + "providesDirectionalDerivative", + str(self.provides_directional_derivative).lower(), + ) + if self.source_files is not None: + element.append(self.source_files.to_xml()) + return element + class Category(BaseModel): """Log category definition""" @@ -557,6 +820,14 @@ class Category(BaseModel): name: Annotated[str, Field(..., alias="name")] description: Annotated[str | None, Field(default=None, alias="description")] = None + def to_xml(self) -> Element: + """Convert Category to XML Element""" + element = Element("Category") + element.set("name", self.name) + if self.description is not None: + element.set("description", self.description) + return element + class LogCategories(BaseModel): """Log categories list""" @@ -565,6 +836,14 @@ class LogCategories(BaseModel): categories: Annotated[list[Category], Field(..., alias="Category")] + def to_xml(self) -> Element: + """Convert LogCategories to XML Element""" + element = Element("LogCategories") + if self.categories is not None: + for category in self.categories: + element.append(category.to_xml()) + return element + class DefaultExperiment(BaseModel): """Default experiment configuration""" @@ -576,6 +855,19 @@ class DefaultExperiment(BaseModel): tolerance: Annotated[float | None, Field(default=None, alias="tolerance")] = None step_size: Annotated[float | None, Field(default=None, alias="stepSize")] = None + def to_xml(self) -> Element: + """Convert DefaultExperiment to XML Element""" + element = Element("DefaultExperiment") + if self.start_time is not None: + element.set("startTime", str(self.start_time)) + if self.stop_time is not None: + element.set("stopTime", str(self.stop_time)) + if self.tolerance is not None: + element.set("tolerance", str(self.tolerance)) + if self.step_size is not None: + element.set("stepSize", str(self.step_size)) + return element + class Tool(BaseModel): """Tool-specific annotation""" @@ -587,6 +879,13 @@ class Tool(BaseModel): # In a more complete implementation, this could be more structured content: Annotated[dict | None, Field(default=None)] = None + def to_xml(self) -> Element: + """Convert Tool to XML Element""" + element = Element("Tool") + element.set("name", self.name) + # Note: content is not currently handled in XML conversion + return element + class Annotation(BaseModel): """Vendor annotations""" @@ -595,6 +894,14 @@ class Annotation(BaseModel): tools: Annotated[list[Tool], Field(..., alias="Tool")] + def to_xml(self) -> Element: + """Convert Annotation to XML Element""" + element = Element("Annotation") + if self.tools is not None: + for tool in self.tools: + element.append(tool.to_xml()) + return element + class UnknownDependency(BaseModel): """Dependency definition for unknown variables""" @@ -610,6 +917,19 @@ class UnknownDependency(BaseModel): Field(default=None, alias="dependenciesKind"), ] = None + def to_xml(self) -> Element: + """Convert UnknownDependency to XML Element""" + element = Element("Unknown") + element.set("index", str(self.index)) + if self.dependencies is not None: + element.set("dependencies", " ".join(map(str, self.dependencies))) + if self.dependencies_kind is not None: + element.set( + "dependenciesKind", + " ".join([kind.value for kind in self.dependencies_kind]), + ) + return element + class VariableDependency(BaseModel): """Variable dependency definition""" @@ -618,6 +938,14 @@ class VariableDependency(BaseModel): unknowns: Annotated[list[UnknownDependency], Field(..., alias="Unknown")] + def to_xml(self) -> Element: + """Convert VariableDependency to XML Element""" + element = Element("VariableDependency") + if self.unknowns is not None: + for unknown in self.unknowns: + element.append(unknown.to_xml()) + return element + class InitialUnknown(BaseModel): """Initial unknown variable""" @@ -633,6 +961,19 @@ class InitialUnknown(BaseModel): Field(default=None, alias="dependenciesKind"), ] = None + def to_xml(self) -> Element: + """Convert InitialUnknown to XML Element""" + element = Element("Unknown") + element.set("index", str(self.index)) + if self.dependencies is not None: + element.set("dependencies", " ".join(map(str, self.dependencies))) + if self.dependencies_kind is not None: + element.set( + "dependenciesKind", + " ".join([kind.value for kind in self.dependencies_kind]), + ) + return element + class InitialUnknowns(BaseModel): """List of initial unknown variables""" @@ -641,6 +982,14 @@ class InitialUnknowns(BaseModel): unknowns: Annotated[list[InitialUnknown], Field(..., alias="Unknown")] + def to_xml(self) -> Element: + """Convert InitialUnknowns to XML Element""" + element = Element("InitialUnknowns") + if self.unknowns is not None: + for unknown in self.unknowns: + element.append(unknown.to_xml()) + return element + class ModelStructure(BaseModel): """Model structure definition""" @@ -657,6 +1006,25 @@ class ModelStructure(BaseModel): InitialUnknowns | None, Field(default=None, alias="InitialUnknowns") ] = None + def to_xml(self) -> Element: + """Convert ModelStructure to XML Element""" + element = Element("ModelStructure") + if self.outputs is not None: + outputs_element = self.outputs.to_xml() + outputs_element.tag = ( + "Outputs" # Change tag from VariableDependency to Outputs + ) + element.append(outputs_element) + if self.derivatives is not None: + derivatives_element = self.derivatives.to_xml() + derivatives_element.tag = ( + "Derivatives" # Change tag from VariableDependency to Derivatives + ) + element.append(derivatives_element) + if self.initial_unknowns is not None: + element.append(self.initial_unknowns.to_xml()) + return element + class RealVariable(BaseModel): """Real variable definition""" @@ -756,6 +1124,35 @@ class RealVariable(BaseModel): ), ] = False + def to_xml(self) -> Element: + """Convert RealVariable to XML Element""" + element = Element("Real") + if self.declared_type is not None: + element.set("declaredType", self.declared_type) + if self.quantity is not None: + element.set("quantity", self.quantity) + if self.unit is not None: + element.set("unit", self.unit) + if self.display_unit is not None: + element.set("displayUnit", self.display_unit) + if self.relative_quantity is not None and self.relative_quantity: + element.set("relativeQuantity", str(self.relative_quantity).lower()) + if self.min_value is not None: + element.set("min", str(self.min_value)) + if self.max_value is not None: + element.set("max", str(self.max_value)) + if self.nominal is not None: + element.set("nominal", str(self.nominal)) + if self.unbounded is not None and self.unbounded: + element.set("unbounded", str(self.unbounded).lower()) + if self.start is not None: + element.set("start", str(self.start)) + if self.derivative is not None: + element.set("derivative", str(self.derivative)) + if self.reinit is not None and self.reinit: + element.set("reinit", str(self.reinit).lower()) + return element + class IntegerVariable(BaseModel): """Integer variable definition""" @@ -803,6 +1200,21 @@ class IntegerVariable(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert IntegerVariable to XML Element""" + element = Element("Integer") + if self.declared_type is not None: + element.set("declaredType", self.declared_type) + if self.quantity is not None: + element.set("quantity", self.quantity) + if self.min_value is not None: + element.set("min", str(self.min_value)) + if self.max_value is not None: + element.set("max", str(self.max_value)) + if self.start is not None: + element.set("start", str(self.start)) + return element + class BooleanVariable(BaseModel): """Boolean variable definition""" @@ -826,6 +1238,15 @@ class BooleanVariable(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert BooleanVariable to XML Element""" + element = Element("Boolean") + if self.declared_type is not None: + element.set("declaredType", self.declared_type) + if self.start is not None: + element.set("start", str(self.start).lower()) + return element + class StringVariable(BaseModel): """String variable definition""" @@ -849,6 +1270,15 @@ class StringVariable(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert StringVariable to XML Element""" + element = Element("String") + if self.declared_type is not None: + element.set("declaredType", self.declared_type) + if self.start is not None: + element.set("start", self.start) + return element + class EnumerationVariable(BaseModel): """Enumeration variable definition""" @@ -896,6 +1326,20 @@ class EnumerationVariable(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert EnumerationVariable to XML Element""" + element = Element("Enumeration") + element.set("declaredType", self.declared_type) + if self.quantity is not None: + element.set("quantity", self.quantity) + if self.min_value is not None: + element.set("min", str(self.min_value)) + if self.max_value is not None: + element.set("max", str(self.max_value)) + if self.start is not None: + element.set("start", str(self.start)) + return element + class ScalarVariable(BaseModel): """Scalar variable definition""" @@ -1007,6 +1451,46 @@ def get_variable_type(self): return "Enumeration" return None + def to_xml(self) -> Element: + """Convert ScalarVariable to XML Element""" + element = Element("ScalarVariable") + element.set("name", self.name) + element.set("valueReference", str(self.value_reference)) + if self.description is not None: + element.set("description", self.description) + if self.causality is not None and self.causality != CausalityEnum.local: + element.set("causality", self.causality.value) + if ( + self.variability is not None + and self.variability != VariabilityEnum.continuous + ): + element.set("variability", self.variability.value) + if self.initial is not None: + element.set("initial", self.initial.value) + if self.can_handle_multiple_set_per_time_instant is not None: + element.set( + "canHandleMultipleSetPerTimeInstant", + str(self.can_handle_multiple_set_per_time_instant).lower(), + ) + + # Add the appropriate variable type element + if self.real is not None: + element.append(self.real.to_xml()) + elif self.integer is not None: + element.append(self.integer.to_xml()) + elif self.boolean is not None: + element.append(self.boolean.to_xml()) + elif self.string is not None: + element.append(self.string.to_xml()) + elif self.enumeration is not None: + element.append(self.enumeration.to_xml()) + + # Add annotations if present + if self.annotations is not None: + element.append(self.annotations.to_xml()) + + return element + class ModelVariables(BaseModel): """Model variables list""" @@ -1015,6 +1499,14 @@ class ModelVariables(BaseModel): variables: Annotated[list[ScalarVariable], Field(..., alias="ScalarVariable")] + def to_xml(self) -> Element: + """Convert ModelVariables to XML Element""" + element = Element("ModelVariables") + if self.variables is not None: + for variable in self.variables: + element.append(variable.to_xml()) + return element + class UnitDefinitions(BaseModel): """Unit definitions list""" @@ -1030,6 +1522,14 @@ class UnitDefinitions(BaseModel): ), ] + def to_xml(self) -> Element: + """Convert UnitDefinitions to XML Element""" + element = Element("UnitDefinitions") + if self.units is not None: + for unit in self.units: + element.append(unit.to_xml()) + return element + class TypeDefinitions(BaseModel): """Type definitions list""" @@ -1038,6 +1538,14 @@ class TypeDefinitions(BaseModel): simple_types: Annotated[list[SimpleType], Field(..., alias="SimpleType")] + def to_xml(self) -> Element: + """Convert TypeDefinitions to XML Element""" + element = Element("TypeDefinitions") + if self.simple_types is not None: + for simple_type in self.simple_types: + element.append(simple_type.to_xml()) + return element + class FmiModelDescription(BaseModel): """Main FMI model description""" @@ -1207,6 +1715,58 @@ class FmiModelDescription(BaseModel): ), ] = None + def to_xml(self) -> Element: + """Convert FmiModelDescription to XML Element""" + element = Element("fmiModelDescription") + element.set("fmiVersion", self.fmi_version) + element.set("modelName", self.model_name) + element.set("guid", self.guid) + if self.description is not None: + element.set("description", self.description) + if self.author is not None: + element.set("author", self.author) + if self.version is not None: + element.set("version", self.version) + if self.copyright is not None: + element.set("copyright", self.copyright) + if self.license is not None: + element.set("license", self.license) + if self.generation_tool is not None: + element.set("generationTool", self.generation_tool) + if self.generation_date_and_time is not None: + element.set("generationDateAndTime", self.generation_date_and_time) + if ( + self.variable_naming_convention is not None + and self.variable_naming_convention != VariableNamingConventionEnum.flat + ): + element.set( + "variableNamingConvention", self.variable_naming_convention.value + ) + if self.number_of_event_indicators is not None: + element.set("numberOfEventIndicators", str(self.number_of_event_indicators)) + + # Add optional components + if self.model_exchange is not None: + element.append(self.model_exchange.to_xml()) + if self.co_simulation is not None: + element.append(self.co_simulation.to_xml()) + if self.unit_definitions is not None: + element.append(self.unit_definitions.to_xml()) + if self.type_definitions is not None: + element.append(self.type_definitions.to_xml()) + if self.log_categories is not None: + element.append(self.log_categories.to_xml()) + if self.default_experiment is not None: + element.append(self.default_experiment.to_xml()) + if self.vendor_annotations is not None: + element.append(self.vendor_annotations.to_xml()) + if self.model_variables is not None: + element.append(self.model_variables.to_xml()) + if self.model_structure is not None: + element.append(self.model_structure.to_xml()) + + return element + def _parse_xml_to_model(xml_content: str | Element) -> FmiModelDescription: """ From 28bacc781436d259da192ae5011bab699a1f5a0a Mon Sep 17 00:00:00 2001 From: coder Date: Sat, 3 Jan 2026 21:34:18 -0800 Subject: [PATCH 2/3] add to_xml and unit tests --- src/mdreader/fmi2.py | 7 +- tests/test_fmi2.py | 555 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 557 insertions(+), 5 deletions(-) diff --git a/src/mdreader/fmi2.py b/src/mdreader/fmi2.py index 7022d52..3b3d28c 100644 --- a/src/mdreader/fmi2.py +++ b/src/mdreader/fmi2.py @@ -1458,12 +1458,9 @@ def to_xml(self) -> Element: element.set("valueReference", str(self.value_reference)) if self.description is not None: element.set("description", self.description) - if self.causality is not None and self.causality != CausalityEnum.local: + if self.causality is not None: element.set("causality", self.causality.value) - if ( - self.variability is not None - and self.variability != VariabilityEnum.continuous - ): + if self.variability is not None: element.set("variability", self.variability.value) if self.initial is not None: element.set("initial", self.initial.value) diff --git a/tests/test_fmi2.py b/tests/test_fmi2.py index f3663fb..d0f8a5f 100644 --- a/tests/test_fmi2.py +++ b/tests/test_fmi2.py @@ -871,3 +871,558 @@ def test_edge_cases_and_validators(): assert _str_to_bool(None) is None assert _str_to_bool(False) is False assert _str_to_bool(True) is True + + +def test_xml_serialization(): + """Test the to_xml() methods for all model classes""" + from mdreader.fmi2 import ( + FmiModelDescription, + ModelVariables, + ScalarVariable, + RealVariable, + CausalityEnum, + VariabilityEnum, + BaseUnit, + DisplayUnit, + Unit, + Item, + RealSimpleType, + SimpleType, + ModelExchange, + CoSimulation, + SourceFiles, + File, + Category, + LogCategories, + DefaultExperiment, + Annotation, + Tool, + UnknownDependency, + VariableDependency, + InitialUnknown, + InitialUnknowns, + ModelStructure, + IntegerVariable, + BooleanVariable, + StringVariable, + EnumerationVariable, + IntegerSimpleType, + BooleanSimpleType, + StringSimpleType, + EnumerationSimpleType, + DependenciesKindEnum, + ) + import xml.etree.ElementTree as ET + + # Test BaseUnit to_xml + base_unit = BaseUnit(kg=1, m=1, s=-2) # Force non-default values + base_unit_xml = base_unit.to_xml() + assert base_unit_xml.tag == "BaseUnit" + assert base_unit_xml.get("kg") == "1" + assert base_unit_xml.get("m") == "1" + assert base_unit_xml.get("s") == "-2" + + # Test DisplayUnit to_xml + display_unit = DisplayUnit(name="deg", factor=57.29577951308232) + display_unit_xml = display_unit.to_xml() + assert display_unit_xml.tag == "DisplayUnit" + assert display_unit_xml.get("name") == "deg" + assert display_unit_xml.get("factor") == "57.29577951308232" + + # Test Unit to_xml + unit = Unit(name="rad", base_unit=base_unit, display_units=[display_unit]) + unit_xml = unit.to_xml() + assert unit_xml.tag == "Unit" + assert unit_xml.get("name") == "rad" + assert len(unit_xml) == 2 # base_unit and display_unit + + # Test Item to_xml + item = Item(name="Option1", value=1, description="First option") + item_xml = item.to_xml() + assert item_xml.tag == "Item" + assert item_xml.get("name") == "Option1" + assert item_xml.get("value") == "1" + assert item_xml.get("description") == "First option" + + # Test RealSimpleType to_xml + real_simple = RealSimpleType( + quantity="Length", unit="m", min_value=0.0, max_value=10.0 + ) + real_simple_xml = real_simple.to_xml() + assert real_simple_xml.tag == "Real" + assert real_simple_xml.get("quantity") == "Length" + assert real_simple_xml.get("unit") == "m" + assert real_simple_xml.get("min") == "0.0" + assert real_simple_xml.get("max") == "10.0" + + # Test IntegerSimpleType to_xml + int_simple = IntegerSimpleType(quantity="Count", min_value=0, max_value=100) + int_simple_xml = int_simple.to_xml() + assert int_simple_xml.tag == "Integer" + assert int_simple_xml.get("quantity") == "Count" + assert int_simple_xml.get("min") == "0" + assert int_simple_xml.get("max") == "100" + + # Test BooleanSimpleType to_xml + bool_simple = BooleanSimpleType() + bool_simple_xml = bool_simple.to_xml() + assert bool_simple_xml.tag == "Boolean" + + # Test StringSimpleType to_xml + str_simple = StringSimpleType() + str_simple_xml = str_simple.to_xml() + assert str_simple_xml.tag == "String" + + # Test EnumerationSimpleType to_xml + enum_simple = EnumerationSimpleType(quantity="Options", items=[item]) + enum_simple_xml = enum_simple.to_xml() + assert enum_simple_xml.tag == "Enumeration" + assert enum_simple_xml.get("quantity") == "Options" + assert len(enum_simple_xml) == 1 + + # Test SimpleType to_xml with Real + simple_type = SimpleType( + name="LengthType", description="A length type", real=real_simple + ) + simple_type_xml = simple_type.to_xml() + assert simple_type_xml.tag == "SimpleType" + assert simple_type_xml.get("name") == "LengthType" + assert simple_type_xml.get("description") == "A length type" + assert len(simple_type_xml) == 1 + assert simple_type_xml[0].tag == "Real" + + # Test File to_xml + file_obj = File(name="source.c") + file_xml = file_obj.to_xml() + assert file_xml.tag == "File" + assert file_xml.get("name") == "source.c" + + # Test SourceFiles to_xml + source_files = SourceFiles(files=[file_obj]) + source_files_xml = source_files.to_xml() + assert source_files_xml.tag == "SourceFiles" + assert len(source_files_xml) == 1 + + # Test ModelExchange to_xml + model_exchange = ModelExchange( + model_identifier="TestModel", + needs_execution_tool=True, + source_files=source_files, + ) + model_exchange_xml = model_exchange.to_xml() + assert model_exchange_xml.tag == "ModelExchange" + assert model_exchange_xml.get("modelIdentifier") == "TestModel" + assert model_exchange_xml.get("needsExecutionTool") == "true" + assert len(model_exchange_xml) == 1 + + # Test CoSimulation to_xml + co_simulation = CoSimulation( + model_identifier="TestModel", can_handle_variable_communication_step_size=True + ) + co_simulation_xml = co_simulation.to_xml() + assert co_simulation_xml.tag == "CoSimulation" + assert co_simulation_xml.get("modelIdentifier") == "TestModel" + assert co_simulation_xml.get("canHandleVariableCommunicationStepSize") == "true" + + # Test Category to_xml + category = Category(name="logEvents", description="Log events") + category_xml = category.to_xml() + assert category_xml.tag == "Category" + assert category_xml.get("name") == "logEvents" + assert category_xml.get("description") == "Log events" + + # Test LogCategories to_xml + log_categories = LogCategories(categories=[category]) + log_categories_xml = log_categories.to_xml() + assert log_categories_xml.tag == "LogCategories" + assert len(log_categories_xml) == 1 + + # Test DefaultExperiment to_xml + default_exp = DefaultExperiment(start_time=0.0, stop_time=10.0, step_size=0.01) + default_exp_xml = default_exp.to_xml() + assert default_exp_xml.tag == "DefaultExperiment" + assert default_exp_xml.get("startTime") == "0.0" + assert default_exp_xml.get("stopTime") == "10.0" + assert default_exp_xml.get("stepSize") == "0.01" + + # Test Tool to_xml + tool = Tool(name="TestTool") + tool_xml = tool.to_xml() + assert tool_xml.tag == "Tool" + assert tool_xml.get("name") == "TestTool" + + # Test Annotation to_xml + annotation = Annotation(tools=[tool]) + annotation_xml = annotation.to_xml() + assert annotation_xml.tag == "Annotation" + assert len(annotation_xml) == 1 + + # Test UnknownDependency to_xml + unknown_dep = UnknownDependency( + index=1, + dependencies=[2, 3], + dependencies_kind=[ + DependenciesKindEnum.dependent, + DependenciesKindEnum.constant, + ], + ) + unknown_dep_xml = unknown_dep.to_xml() + assert unknown_dep_xml.tag == "Unknown" + assert unknown_dep_xml.get("index") == "1" + assert unknown_dep_xml.get("dependencies") == "2 3" + assert unknown_dep_xml.get("dependenciesKind") == "dependent constant" + + # Test VariableDependency to_xml + var_dep = VariableDependency(unknowns=[unknown_dep]) + var_dep_xml = var_dep.to_xml() + assert var_dep_xml.tag == "VariableDependency" + assert len(var_dep_xml) == 1 + + # Test InitialUnknown to_xml + initial_unknown = InitialUnknown( + index=1, dependencies=[2, 3], dependencies_kind=[DependenciesKindEnum.dependent] + ) + initial_unknown_xml = initial_unknown.to_xml() + assert initial_unknown_xml.tag == "Unknown" # Same tag as UnknownDependency + assert initial_unknown_xml.get("index") == "1" + + # Test InitialUnknowns to_xml + initial_unknowns = InitialUnknowns(unknowns=[initial_unknown]) + initial_unknowns_xml = initial_unknowns.to_xml() + assert initial_unknowns_xml.tag == "InitialUnknowns" + assert len(initial_unknowns_xml) == 1 + + # Test ModelStructure to_xml + model_structure = ModelStructure(outputs=var_dep, initial_unknowns=initial_unknowns) + model_structure_xml = model_structure.to_xml() + assert model_structure_xml.tag == "ModelStructure" + assert len(model_structure_xml) == 2 # outputs and initial_unknowns + + # Test RealVariable to_xml + real_var = RealVariable( + declared_type="LengthType", unit="m", min_value=0.0, max_value=10.0, start=5.0 + ) + real_var_xml = real_var.to_xml() + assert real_var_xml.tag == "Real" + assert real_var_xml.get("declaredType") == "LengthType" + assert real_var_xml.get("unit") == "m" + assert real_var_xml.get("min") == "0.0" + assert real_var_xml.get("max") == "10.0" + assert real_var_xml.get("start") == "5.0" + + # Test IntegerVariable to_xml + int_var = IntegerVariable( + declared_type="CountType", min_value=0, max_value=100, start=50 + ) + int_var_xml = int_var.to_xml() + assert int_var_xml.tag == "Integer" + assert int_var_xml.get("declaredType") == "CountType" + assert int_var_xml.get("min") == "0" + assert int_var_xml.get("max") == "100" + assert int_var_xml.get("start") == "50" + + # Test BooleanVariable to_xml + bool_var = BooleanVariable(declared_type="FlagType", start=True) + bool_var_xml = bool_var.to_xml() + assert bool_var_xml.tag == "Boolean" + assert bool_var_xml.get("declaredType") == "FlagType" + assert bool_var_xml.get("start") == "true" + + # Test StringVariable to_xml + str_var = StringVariable(declared_type="TextType", start="Hello") + str_var_xml = str_var.to_xml() + assert str_var_xml.tag == "String" + assert str_var_xml.get("declaredType") == "TextType" + assert str_var_xml.get("start") == "Hello" + + # Test EnumerationVariable to_xml + enum_var = EnumerationVariable( + declared_type="OptionType", min_value=1, max_value=3, start=2 + ) + enum_var_xml = enum_var.to_xml() + assert enum_var_xml.tag == "Enumeration" + assert enum_var_xml.get("declaredType") == "OptionType" + assert enum_var_xml.get("min") == "1" + assert enum_var_xml.get("max") == "3" + assert enum_var_xml.get("start") == "2" + + # Test ScalarVariable to_xml + scalar_var = ScalarVariable( + name="test_var", + value_reference=1, + description="A test variable", + causality=CausalityEnum.output, + variability=VariabilityEnum.continuous, + real=real_var, + annotations=annotation, + ) + scalar_var_xml = scalar_var.to_xml() + assert scalar_var_xml.tag == "ScalarVariable" + assert scalar_var_xml.get("name") == "test_var" + assert scalar_var_xml.get("valueReference") == "1" + assert scalar_var_xml.get("description") == "A test variable" + assert scalar_var_xml.get("causality") == "output" + assert scalar_var_xml.get("variability") == "continuous" + assert len(scalar_var_xml) == 2 # Real element and Annotations element + + # Test ModelVariables to_xml + model_vars = ModelVariables(variables=[scalar_var]) + model_vars_xml = model_vars.to_xml() + assert model_vars_xml.tag == "ModelVariables" + assert len(model_vars_xml) == 1 + + # Test UnitDefinitions to_xml + from mdreader.fmi2 import UnitDefinitions + + unit_defs = UnitDefinitions(units=[unit]) + unit_defs_xml = unit_defs.to_xml() + assert unit_defs_xml.tag == "UnitDefinitions" + assert len(unit_defs_xml) == 1 + + # Test TypeDefinitions to_xml + from mdreader.fmi2 import TypeDefinitions + + type_defs = TypeDefinitions(simple_types=[simple_type]) + type_defs_xml = type_defs.to_xml() + assert type_defs_xml.tag == "TypeDefinitions" + assert len(type_defs_xml) == 1 + + # Test FmiModelDescription to_xml + model_desc = FmiModelDescription( + fmi_version="2.0", + model_name="TestModel", + guid="{12345678-1234-5678-9012-123456789012}", + description="A test model", + model_exchange=model_exchange, + unit_definitions=unit_defs, + type_definitions=type_defs, + log_categories=log_categories, + default_experiment=default_exp, + vendor_annotations=annotation, + model_variables=model_vars, + model_structure=model_structure, + ) + model_desc_xml = model_desc.to_xml() + assert model_desc_xml.tag == "fmiModelDescription" + assert model_desc_xml.get("fmiVersion") == "2.0" + assert model_desc_xml.get("modelName") == "TestModel" + assert model_desc_xml.get("guid") == "{12345678-1234-5678-9012-123456789012}" + assert model_desc_xml.get("description") == "A test model" + assert len(model_desc_xml) == 8 # All optional components + + # Test that the generated XML can be parsed back to a string + xml_string = ET.tostring(model_desc_xml, encoding="unicode") + assert "fmiModelDescription" in xml_string + assert "TestModel" in xml_string + assert "{12345678-1234-5678-9012-123456789012}" in xml_string + + +def test_xml_serialization_roundtrip(): + """Test that XML serialization and deserialization work correctly""" + from mdreader.fmi2 import ( + FmiModelDescription, + ModelVariables, + ScalarVariable, + RealVariable, + CausalityEnum, + VariabilityEnum, + ) + import xml.etree.ElementTree as ET + + # Create a simple model + real_var = RealVariable(unit="m", min_value=0.0, max_value=10.0, start=5.0) + scalar_var = ScalarVariable( + name="test_var", + value_reference=1, + description="A test variable", + causality=CausalityEnum.output, + variability=VariabilityEnum.continuous, + real=real_var, + ) + model_vars = ModelVariables(variables=[scalar_var]) + model_desc = FmiModelDescription( + fmi_version="2.0", + model_name="TestModel", + guid="{12345678-1234-5678-9012-123456789012}", + model_variables=model_vars, + ) + + # Convert to XML + xml_element = model_desc.to_xml() + xml_string = ET.tostring(xml_element, encoding="unicode") + + # Verify that the XML string contains expected elements + assert 'fmiVersion="2.0"' in xml_string + assert 'modelName="TestModel"' in xml_string + assert 'guid="{12345678-1234-5678-9012-123456789012}"' in xml_string + assert 'name="test_var"' in xml_string + assert 'valueReference="1"' in xml_string + assert 'causality="output"' in xml_string + + +@pytest.mark.parametrize( + "reference_fmu", + [ + "2.0/Feedthrough.fmu", + "2.0/BouncingBall.fmu", + "2.0/VanDerPol.fmu", + "2.0/Dahlquist.fmu", + "2.0/Stair.fmu", + "2.0/Resource.fmu", + ], +) +def test_xml_serialization_roundtrip_with_reference_fmus( + reference_fmu, reference_fmus_dir +): + """Test XML serialization round-trip with reference FMUs""" + import xml.etree.ElementTree as ET + from mdreader.fmi2 import read_model_description, _parse_xml_to_model + + filename = (reference_fmus_dir / reference_fmu).absolute() + + # Read the original model description + original_md = read_model_description(filename) + + # Convert to XML using to_xml() method + xml_element = original_md.to_xml() + + # Convert XML element back to string + xml_string = ET.tostring(xml_element, encoding="unicode") + + # Parse the XML string back to a model description + parsed_element = ET.fromstring(xml_string) + reconstructed_md = _parse_xml_to_model(parsed_element) + + # Compare key attributes between original and reconstructed + assert original_md.fmi_version == reconstructed_md.fmi_version + assert original_md.model_name == reconstructed_md.model_name + assert original_md.guid == reconstructed_md.guid + assert original_md.description == reconstructed_md.description + assert original_md.author == reconstructed_md.author + assert original_md.version == reconstructed_md.version + assert original_md.copyright == reconstructed_md.copyright + assert original_md.license == reconstructed_md.license + assert original_md.generation_tool == reconstructed_md.generation_tool + assert ( + original_md.number_of_event_indicators + == reconstructed_md.number_of_event_indicators + ) + + # Compare variable counts + assert len(original_md.model_variables.variables) == len( + reconstructed_md.model_variables.variables + ) + + # Compare some variable properties + for orig_var, recon_var in zip( + original_md.model_variables.variables, + reconstructed_md.model_variables.variables, + ): + assert orig_var.name == recon_var.name + assert orig_var.value_reference == recon_var.value_reference + assert orig_var.description == recon_var.description + assert orig_var.causality == recon_var.causality + assert orig_var.variability == recon_var.variability + assert orig_var.initial == recon_var.initial + + # Compare variable type-specific properties + orig_type = orig_var.get_variable_type() + recon_type = recon_var.get_variable_type() + assert orig_type == recon_type + + if orig_type == "Real" and orig_var.real and recon_var.real: + assert orig_var.real.declared_type == recon_var.real.declared_type + assert orig_var.real.unit == recon_var.real.unit + assert orig_var.real.min_value == recon_var.real.min_value + assert orig_var.real.max_value == recon_var.real.max_value + assert orig_var.real.start == recon_var.real.start + elif orig_type == "Integer" and orig_var.integer and recon_var.integer: + assert orig_var.integer.declared_type == recon_var.integer.declared_type + assert orig_var.integer.min_value == recon_var.integer.min_value + assert orig_var.integer.max_value == recon_var.integer.max_value + assert orig_var.integer.start == recon_var.integer.start + elif orig_type == "Boolean" and orig_var.boolean and recon_var.boolean: + assert orig_var.boolean.declared_type == recon_var.boolean.declared_type + assert orig_var.boolean.start == recon_var.boolean.start + elif orig_type == "String" and orig_var.string and recon_var.string: + assert orig_var.string.declared_type == recon_var.string.declared_type + assert orig_var.string.start == recon_var.string.start + elif ( + orig_type == "Enumeration" + and orig_var.enumeration + and recon_var.enumeration + ): + assert ( + orig_var.enumeration.declared_type + == recon_var.enumeration.declared_type + ) + assert orig_var.enumeration.min_value == recon_var.enumeration.min_value + assert orig_var.enumeration.max_value == recon_var.enumeration.max_value + assert orig_var.enumeration.start == recon_var.enumeration.start + + +@pytest.mark.parametrize( + "reference_fmu", + [ + "2.0/Feedthrough.fmu", + "2.0/BouncingBall.fmu", + ], +) +def test_xml_serialization_with_optional_components(reference_fmu, reference_fmus_dir): + """Test XML serialization preserves optional components like ModelExchange, CoSimulation, etc.""" + import xml.etree.ElementTree as ET + from mdreader.fmi2 import read_model_description + + filename = (reference_fmus_dir / reference_fmu).absolute() + + # Read the original model description + original_md = read_model_description(filename) + + # Convert to XML using to_xml() method + xml_element = original_md.to_xml() + + # Convert XML element back to string to ensure it's well-formed + xml_string = ET.tostring(xml_element, encoding="unicode") + + # Verify that the XML string contains expected root attributes + assert f'fmiVersion="{original_md.fmi_version}"' in xml_string + assert f'modelName="{original_md.model_name}"' in xml_string + assert f'guid="{original_md.guid}"' in xml_string + + # Check for optional components if they exist in the original + if original_md.model_exchange: + assert " Date: Sat, 3 Jan 2026 22:11:06 -0800 Subject: [PATCH 3/3] update version --- .bumpversion.toml | 31 ++++++- Makefile | 8 ++ pyproject.toml | 3 +- src/mdreader/__init__.py | 2 +- uv.lock | 176 +-------------------------------------- 5 files changed, 42 insertions(+), 178 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 550e005..7f83fda 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,12 +1,37 @@ [tool.bumpversion] -current_version = "0.1.1" -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" -serialize = ["{major}.{minor}.{patch}"] +current_version = "0.1.2rc0" +parse = """(?x) + (?P0|[1-9]\\d*)\\. + (?P0|[1-9]\\d*)\\. + (?P0|[1-9]\\d*) + (?: + (?P[a-zA-Z-]+) # pre-release label + (?P0|[1-9]\\d*) # pre-release version number + )? # pre-release section is optional +""" +serialize = [ + "{major}.{minor}.{patch}{pre_l}{pre_n}", + "{major}.{minor}.{patch}", +] search = "{current_version}" replace = "{new_version}" +ignore_missing_version = false +regex = false +tag = false +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = false +commit = false +message = "Bump version: {current_version} → {new_version}" +commit_args = "" [[tool.bumpversion.files]] filename = "pyproject.toml" [[tool.bumpversion.files]] filename = "src/mdreader/__init__.py" + +[tool.bumpversion.parts.pre_l] +optional_value = "final" +values = ["rc", "final"] diff --git a/Makefile b/Makefile index 7b4e278..6fabd06 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,14 @@ major: ## Bump version major @uvx bump-my-version bump major uv lock --upgrade +pre-release: ## Bump version pre-release + @uvx bump-my-version bump pre_l + uv lock --upgrade + +pre-release-num: ## Bump version pre-release + @uvx bump-my-version bump pre_n + uv lock --upgrade + build: install ## Build the package @echo "🚀 Building the package" @uv build diff --git a/pyproject.toml b/pyproject.toml index d2e3a57..9121f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mdreader" -version = "0.1.1" +version = "0.1.2rc0" description = "A Python library for reading and parsing Functional Mock-up Interface model description XML file." readme = "README.md" authors = [ @@ -21,7 +21,6 @@ build-backend = "uv_build" [dependency-groups] dev = [ - "bump-my-version>=1.2.6", "httpx>=0.28.1", "pyright>=1.1.407", "pytest>=9.0.2", diff --git a/src/mdreader/__init__.py b/src/mdreader/__init__.py index 485f44a..af0774b 100644 --- a/src/mdreader/__init__.py +++ b/src/mdreader/__init__.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2rc0" diff --git a/uv.lock b/uv.lock index 760f352..98efee6 100644 --- a/uv.lock +++ b/uv.lock @@ -24,54 +24,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] -[[package]] -name = "bracex" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, -] - -[[package]] -name = "bump-my-version" -version = "1.2.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "questionary" }, - { name = "rich" }, - { name = "rich-click" }, - { name = "tomlkit" }, - { name = "wcmatch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/d3/43acec2ec4a477d6c6191faebe5f2e79facd80936ab3e93b6f9d18d11593/bump_my_version-1.2.6.tar.gz", hash = "sha256:1f2f0daa5d699904e9739be8efb51c4c945461bad83cd4da4c89d324d9a18343", size = 1195328, upload-time = "2025-12-29T11:59:30.389Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/8e/39de3356f72327dd0bf569540a858723f3fc4f11f3c5bfae85b3dadac5c3/bump_my_version-1.2.6-py3-none-any.whl", hash = "sha256:a2f567c10574a374b81a9bd6d2bd3cb2ca74befe5c24c3021123773635431659", size = 59791, upload-time = "2025-12-29T11:59:27.873Z" }, -] - [[package]] name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -230,21 +189,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - [[package]] name = "mdreader" -version = "0.1.1" +version = "0.1.2rc0" source = { editable = "." } dependencies = [ { name = "pydantic" }, @@ -252,7 +199,6 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "bump-my-version" }, { name = "httpx" }, { name = "pyright" }, { name = "pytest" }, @@ -264,22 +210,12 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.11.5,<3.0.0" }] [package.metadata.requires-dev] dev = [ - { name = "bump-my-version", specifier = ">=1.2.6" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "pyright", specifier = ">=1.1.407" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, ] -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - [[package]] name = "nodeenv" version = "1.10.0" @@ -307,18 +243,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -431,20 +355,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -497,54 +407,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "questionary" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "prompt-toolkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, -] - -[[package]] -name = "rich" -version = "14.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, -] - -[[package]] -name = "rich-click" -version = "1.9.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/d1/b60ca6a8745e76800b50c7ee246fd73f08a3be5d8e0b551fc93c19fa1203/rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6", size = 73927, upload-time = "2025-12-21T14:49:44.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/d865895e1e5d88a60baee0fc3703eb111c502ee10c8c107516bc7623abf8/rich_click-1.9.5-py3-none-any.whl", hash = "sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a", size = 70580, upload-time = "2025-12-21T14:49:42.905Z" }, -] - [[package]] name = "tomli" version = "2.3.0" @@ -594,15 +456,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] -[[package]] -name = "tomlkit" -version = "0.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -623,24 +476,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] - -[[package]] -name = "wcmatch" -version = "10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bracex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, -] - -[[package]] -name = "wcwidth" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, -]