mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-03-16 03:26:18 +00:00
fix: prevent exception when load prediction data is missing (#925)
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled
Validate solution prediction data before processing. If required prediction data is missing, the prediction is skipped instead of raising an exception. Introduce a new configuration file saving policy to improve loading robustness: - Exclude computed fields - Exclude fields set to their default values - Exclude fields with value None - Use field aliases - Recursively remove empty dictionaries and lists - Ensure general.version is always present and correctly set When loading older configuration files, computed fields are now stripped before migration. This further improves backward compatibility and loading robustness. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -129,11 +129,10 @@ class GeneralSettings(SettingsBaseModel):
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
version: str = Field(
|
||||
default=__version__,
|
||||
json_schema_extra={
|
||||
"description": "Configuration file version. Used to check compatibility."
|
||||
},
|
||||
version: Optional[str] = Field(
|
||||
default=None, # keep None here, will be set elsewhere
|
||||
json_schema_extra={"description": "Configuration file version."},
|
||||
examples=["0.0.0"],
|
||||
)
|
||||
|
||||
data_folder_path: Path = Field(
|
||||
@@ -195,18 +194,6 @@ class GeneralSettings(SettingsBaseModel):
|
||||
|
||||
compatible_versions: ClassVar[list[str]] = [__version__]
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def check_version(cls, v: str) -> str:
|
||||
if v not in cls.compatible_versions:
|
||||
error = (
|
||||
f"Incompatible configuration version '{v}'. "
|
||||
f"Expected: {', '.join(cls.compatible_versions)}."
|
||||
)
|
||||
logger.error(error)
|
||||
raise ValueError(error)
|
||||
return v
|
||||
|
||||
@field_validator("data_folder_path", mode="after")
|
||||
@classmethod
|
||||
def validate_data_folder_path(cls, value: Optional[Union[str, Path]]) -> Path:
|
||||
@@ -886,6 +873,73 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
||||
|
||||
return config_file_path
|
||||
|
||||
def to_config_json(self) -> str:
|
||||
"""Serialize the configuration to a normalized JSON string.
|
||||
|
||||
The serialization routine ensures that the resulting JSON:
|
||||
|
||||
- Excludes computed fields.
|
||||
- Excludes fields set to their default values.
|
||||
- Excludes fields with value ``None``.
|
||||
- Uses field aliases.
|
||||
- Recursively removes empty dictionaries and lists.
|
||||
- Ensures that ``general.version`` is always present and set
|
||||
to the current application version.
|
||||
|
||||
Returns:
|
||||
str: A normalized, human-readable JSON string representation
|
||||
of the configuration.
|
||||
|
||||
Raises:
|
||||
TypeError: If the serialized configuration root is not a dictionary.
|
||||
"""
|
||||
|
||||
def remove_empty(
|
||||
obj: Union[dict[str, Any], list[Any], Any],
|
||||
) -> Union[dict[str, Any], list[Any], Any]:
|
||||
"""Recursively remove empty dictionaries, lists, and None values."""
|
||||
if isinstance(obj, dict):
|
||||
cleaned: dict[str, Any] = {k: remove_empty(v) for k, v in obj.items()}
|
||||
return {k: v for k, v in cleaned.items() if v not in (None, {}, [])}
|
||||
elif isinstance(obj, list):
|
||||
cleaned_list: list[Any] = [remove_empty(v) for v in obj]
|
||||
return [v for v in cleaned_list if v not in (None, {}, [])]
|
||||
else:
|
||||
return obj
|
||||
|
||||
# Use model_dump_json to respect custom Pydantic serialization
|
||||
json_str = self.model_dump_json(
|
||||
exclude_computed_fields=True,
|
||||
exclude_defaults=True,
|
||||
exclude_none=True,
|
||||
by_alias=True,
|
||||
)
|
||||
|
||||
# Load as JSON
|
||||
root: Any = json.loads(json_str)
|
||||
|
||||
# Remove empty values recursively
|
||||
cleaned_root = remove_empty(root)
|
||||
|
||||
# Validate that root is a dictionary
|
||||
if not isinstance(cleaned_root, dict):
|
||||
raise TypeError(
|
||||
f"Configuration serialization error: root element must be a dictionary, "
|
||||
f"got {type(cleaned_root).__name__}"
|
||||
)
|
||||
|
||||
# Ensure version is present and correct
|
||||
cleaned_root.setdefault("general", {})
|
||||
cleaned_root["general"]["version"] = __version__
|
||||
|
||||
# Return pretty-printed JSON
|
||||
return json.dumps(
|
||||
cleaned_root,
|
||||
indent=4,
|
||||
sort_keys=True,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
def to_config_file(self) -> None:
|
||||
"""Saves the current configuration to the configuration file.
|
||||
|
||||
@@ -897,8 +951,7 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
||||
if not self.general.config_file_path:
|
||||
raise ValueError("Configuration file path unknown.")
|
||||
with self.general.config_file_path.open("w", encoding="utf-8", newline="\n") as f_out:
|
||||
json_str = super().model_dump_json(indent=4)
|
||||
f_out.write(json_str)
|
||||
f_out.write(self.to_config_json())
|
||||
|
||||
def update(self) -> None:
|
||||
"""Updates all configuration fields.
|
||||
|
||||
Reference in New Issue
Block a user