mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-10-11 20:06:18 +00:00
Improve Configuration and Prediction Usability (#220)
* Update utilities in utils submodule. * Add base configuration modules. * Add server base configuration modules. * Add devices base configuration modules. * Add optimization base configuration modules. * Add utils base configuration modules. * Add prediction abstract and base classes plus tests. * Add PV forecast to prediction submodule. The PV forecast modules are adapted from the class_pvforecast module and replace it. * Add weather forecast to prediction submodule. The modules provide classes and methods to retrieve, manage, and process weather forecast data from various sources. Includes are structured representations of weather data and utilities for fetching forecasts for specific locations and time ranges. BrightSky and ClearOutside are currently supported. * Add electricity price forecast to prediction submodule. * Adapt fastapi server to base config and add fasthtml server. * Add ems to core submodule. * Adapt genetic to config. * Adapt visualize to config. * Adapt common test fixtures to config. * Add load forecast to prediction submodule. * Add core abstract and base classes. * Adapt single test optimization to config. * Adapt devices to config. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
226
src/akkudoktoreos/core/pydantic.py
Normal file
226
src/akkudoktoreos/core/pydantic.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""Module for managing and serializing Pydantic-based models with custom support.
|
||||
|
||||
This module introduces the `PydanticBaseModel` class, which extends Pydantic’s `BaseModel` to facilitate
|
||||
custom serialization and deserialization for `pendulum.DateTime` objects. The main features include
|
||||
automatic handling of `pendulum.DateTime` fields, custom serialization to ISO 8601 format, and utility
|
||||
methods for converting model instances to and from dictionary and JSON formats.
|
||||
|
||||
Key Classes:
|
||||
- PendulumDateTime: A custom type adapter that provides serialization and deserialization
|
||||
functionality for `pendulum.DateTime` objects, converting them to ISO 8601 strings and back.
|
||||
- PydanticBaseModel: A base model class for handling prediction records or configuration data
|
||||
with automatic Pendulum DateTime handling and additional methods for JSON and dictionary
|
||||
conversion.
|
||||
|
||||
Classes:
|
||||
PendulumDateTime(TypeAdapter[pendulum.DateTime]): Type adapter for `pendulum.DateTime` fields
|
||||
with ISO 8601 serialization. Includes:
|
||||
- serialize: Converts `pendulum.DateTime` instances to ISO 8601 string.
|
||||
- deserialize: Converts ISO 8601 strings to `pendulum.DateTime` instances.
|
||||
- is_iso8601: Validates if a string matches the ISO 8601 date format.
|
||||
|
||||
PydanticBaseModel(BaseModel): Extends `pydantic.BaseModel` to handle `pendulum.DateTime` fields
|
||||
and adds convenience methods for dictionary and JSON serialization. Key methods:
|
||||
- model_dump: Dumps the model, converting `pendulum.DateTime` fields to ISO 8601.
|
||||
- model_construct: Constructs a model instance with automatic deserialization of
|
||||
`pendulum.DateTime` fields from ISO 8601.
|
||||
- to_dict: Serializes the model instance to a dictionary.
|
||||
- from_dict: Constructs a model instance from a dictionary.
|
||||
- to_json: Converts the model instance to a JSON string.
|
||||
- from_json: Creates a model instance from a JSON string.
|
||||
|
||||
Usage Example:
|
||||
# Define custom settings in a model using PydanticBaseModel
|
||||
class PredictionCommonSettings(PydanticBaseModel):
|
||||
prediction_start: pendulum.DateTime = Field(...)
|
||||
|
||||
# Serialize a model instance to a dictionary or JSON
|
||||
config = PredictionCommonSettings(prediction_start=pendulum.now())
|
||||
config_dict = config.to_dict()
|
||||
config_json = config.to_json()
|
||||
|
||||
# Deserialize from dictionary or JSON
|
||||
new_config = PredictionCommonSettings.from_dict(config_dict)
|
||||
restored_config = PredictionCommonSettings.from_json(config_json)
|
||||
|
||||
Dependencies:
|
||||
- `pendulum`: Required for handling timezone-aware datetime fields.
|
||||
- `pydantic`: Required for model and validation functionality.
|
||||
|
||||
Notes:
|
||||
- This module enables custom handling of Pendulum DateTime fields within Pydantic models,
|
||||
which is particularly useful for applications requiring consistent ISO 8601 datetime formatting
|
||||
and robust timezone-aware datetime support.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Type
|
||||
|
||||
import pendulum
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
||||
|
||||
|
||||
# Custom type adapter for Pendulum DateTime fields
|
||||
class PendulumDateTime(TypeAdapter[pendulum.DateTime]):
|
||||
@classmethod
|
||||
def serialize(cls, value: Any) -> str:
|
||||
"""Convert pendulum.DateTime to ISO 8601 string."""
|
||||
if isinstance(value, pendulum.DateTime):
|
||||
return value.to_iso8601_string()
|
||||
raise ValueError(f"Expected pendulum.DateTime, got {type(value)}")
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, value: Any) -> pendulum.DateTime:
|
||||
"""Convert ISO 8601 string to pendulum.DateTime."""
|
||||
if isinstance(value, str) and cls.is_iso8601(value):
|
||||
try:
|
||||
return pendulum.parse(value)
|
||||
except pendulum.parsing.exceptions.ParserError as e:
|
||||
raise ValueError(f"Invalid date format: {value}") from e
|
||||
elif isinstance(value, pendulum.DateTime):
|
||||
return value
|
||||
raise ValueError(f"Expected ISO 8601 string or pendulum.DateTime, got {type(value)}")
|
||||
|
||||
@staticmethod
|
||||
def is_iso8601(value: str) -> bool:
|
||||
"""Check if the string is a valid ISO 8601 date string."""
|
||||
iso8601_pattern = (
|
||||
r"^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})?)$"
|
||||
)
|
||||
return bool(re.match(iso8601_pattern, value))
|
||||
|
||||
|
||||
class PydanticBaseModel(BaseModel):
|
||||
"""Base model class with automatic serialization and deserialization of `pendulum.DateTime` fields.
|
||||
|
||||
This model serializes pendulum.DateTime objects to ISO 8601 strings and
|
||||
deserializes ISO 8601 strings to pendulum.DateTime objects.
|
||||
"""
|
||||
|
||||
# Enable custom serialization globally in config
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
use_enum_values=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
|
||||
# Override Pydantic’s serialization for all DateTime fields
|
||||
def model_dump(self, *args: Any, **kwargs: Any) -> dict:
|
||||
"""Custom dump method to handle serialization for DateTime fields."""
|
||||
result = super().model_dump(*args, **kwargs)
|
||||
for key, value in result.items():
|
||||
if isinstance(value, pendulum.DateTime):
|
||||
result[key] = PendulumDateTime.serialize(value)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def model_construct(cls, data: dict) -> "PydanticBaseModel":
|
||||
"""Custom constructor to handle deserialization for DateTime fields."""
|
||||
for key, value in data.items():
|
||||
if isinstance(value, str) and PendulumDateTime.is_iso8601(value):
|
||||
data[key] = PendulumDateTime.deserialize(value)
|
||||
return super().model_construct(data)
|
||||
|
||||
def reset_optional(self) -> "PydanticBaseModel":
|
||||
"""Resets all optional fields in the model to None.
|
||||
|
||||
Iterates through all model fields and sets any optional (non-required)
|
||||
fields to None. The modification is done in-place on the current instance.
|
||||
|
||||
Returns:
|
||||
PydanticBaseModel: The current instance with all optional fields
|
||||
reset to None.
|
||||
|
||||
Example:
|
||||
>>> settings = PydanticBaseModel(name="test", optional_field="value")
|
||||
>>> settings.reset_optional()
|
||||
>>> assert settings.optional_field is None
|
||||
"""
|
||||
for field_name, field in self.model_fields.items():
|
||||
if field.is_required is False: # Check if field is optional
|
||||
setattr(self, field_name, None)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert this PredictionRecord instance to a dictionary representation.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where the keys are the field names of the PydanticBaseModel,
|
||||
and the values are the corresponding field values.
|
||||
"""
|
||||
return self.model_dump()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type["PydanticBaseModel"], data: dict) -> "PydanticBaseModel":
|
||||
"""Create a PydanticBaseModel instance from a dictionary.
|
||||
|
||||
Args:
|
||||
data (dict): A dictionary containing data to initialize the PydanticBaseModel.
|
||||
Keys should match the field names defined in the model.
|
||||
|
||||
Returns:
|
||||
PydanticBaseModel: An instance of the PydanticBaseModel populated with the data.
|
||||
|
||||
Notes:
|
||||
Works with derived classes by ensuring the `cls` argument is used to instantiate the object.
|
||||
"""
|
||||
return cls.model_validate(data)
|
||||
|
||||
@classmethod
|
||||
def from_dict_with_reset(cls, data: dict | None = None) -> "PydanticBaseModel":
|
||||
"""Creates a new instance with reset optional fields, then updates from dict.
|
||||
|
||||
First creates an instance with default values, resets all optional fields
|
||||
to None, then updates the instance with the provided dictionary data if any.
|
||||
|
||||
Args:
|
||||
data (dict | None): Dictionary containing field values to initialize
|
||||
the instance with. Defaults to None.
|
||||
|
||||
Returns:
|
||||
PydanticBaseModel: A new instance with all optional fields initially
|
||||
reset to None and then updated with provided data.
|
||||
|
||||
Example:
|
||||
>>> data = {"name": "test", "optional_field": "value"}
|
||||
>>> settings = PydanticBaseModel.from_dict_with_reset(data)
|
||||
>>> # All non-specified optional fields will be None
|
||||
"""
|
||||
# Create instance with model defaults
|
||||
instance = cls()
|
||||
|
||||
# Reset all optional fields to None
|
||||
instance.reset_optional()
|
||||
|
||||
# Update with provided data if any
|
||||
if data:
|
||||
# Use model_validate to ensure proper type conversion and validation
|
||||
updated_instance = instance.model_validate({**instance.model_dump(), **data})
|
||||
return updated_instance
|
||||
|
||||
return instance
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert the PydanticBaseModel instance to a JSON string.
|
||||
|
||||
Returns:
|
||||
str: The JSON representation of the instance.
|
||||
"""
|
||||
return self.model_dump_json()
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type["PydanticBaseModel"], json_str: str) -> "PydanticBaseModel":
|
||||
"""Create an instance of the PydanticBaseModel class or its subclass from a JSON string.
|
||||
|
||||
Args:
|
||||
json_str (str): JSON string to parse and convert into a PydanticBaseModel instance.
|
||||
|
||||
Returns:
|
||||
PydanticBaseModel: A new instance of the class, populated with data from the JSON string.
|
||||
|
||||
Notes:
|
||||
Works with derived classes by ensuring the `cls` argument is used to instantiate the object.
|
||||
"""
|
||||
data = json.loads(json_str)
|
||||
return cls.model_validate(data)
|
Reference in New Issue
Block a user