diff options
author | crupest <crupest@outlook.com> | 2024-11-11 01:12:29 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-01-15 15:55:51 +0800 |
commit | 7f2e4107e7f469d6747350487e9441d9b987de47 (patch) | |
tree | 245a543316e9f1291acebe0aefd510e7064e92a2 | |
parent | a5c2f62b0b3c4ec6ce46c57bcd99319a85900798 (diff) | |
download | crupest-7f2e4107e7f469d6747350487e9441d9b987de47.tar.gz crupest-7f2e4107e7f469d6747350487e9441d9b987de47.tar.bz2 crupest-7f2e4107e7f469d6747350487e9441d9b987de47.zip |
HALF WORK: 2024.1.15
-rw-r--r-- | tools/cru-py/cru/config.py | 15 | ||||
-rw-r--r-- | tools/cru-py/cru/service/_config.py | 243 | ||||
-rw-r--r-- | tools/cru-py/pyproject.toml | 5 |
3 files changed, 197 insertions, 66 deletions
diff --git a/tools/cru-py/cru/config.py b/tools/cru-py/cru/config.py index bd8eaf9..b71f042 100644 --- a/tools/cru-py/cru/config.py +++ b/tools/cru-py/cru/config.py @@ -161,3 +161,18 @@ class Configuration(CruUniqueKeyList[ConfigItem[Any], str]): return { item.name: item.value_type.convert_value_to_str(item.value) for item in self } + + def set_value_dict( + self, + value_dict: dict[str, Any], + *, + empty_is_default: bool = True, + allow_convert_from_str: bool = True, + ) -> None: + for name, value in value_dict.items(): + item = self.get(name) + item.set_value( + value, + empty_is_default=empty_is_default, + allow_convert_from_str=allow_convert_from_str, + ) diff --git a/tools/cru-py/cru/service/_config.py b/tools/cru-py/cru/service/_config.py index 12784c5..f2beeb2 100644 --- a/tools/cru-py/cru/service/_config.py +++ b/tools/cru-py/cru/service/_config.py @@ -1,8 +1,12 @@ +from collections.abc import Iterable +from typing import Any, NoReturn + from cru import CruException, CruUserFriendlyException from cru.config import Configuration, ConfigItem from cru.value import ( INTEGER_VALUE_TYPE, TEXT_VALUE_TYPE, + CruValueTypeError, RandomStringValueGenerator, UuidValueGenerator, ) @@ -12,85 +16,149 @@ from ._base import AppFeaturePath, AppCommandFeatureProvider, OWNER_NAME class AppConfigError(CruException): - pass - - -class AppConfigDuplicateEntryError(AppConfigError): def __init__( - self, message: str, entries: list[SimpleLineConfigParser.Entry], *args, **kwargs + self, message: str, configuration: Configuration, *args, **kwargs ) -> None: super().__init__(message, *args, **kwargs) - self._entries = entries + self._configuration = configuration @property - def duplicate_entries(self) -> list[SimpleLineConfigParser.Entry]: - return self._entries + def configuration(self) -> Configuration: + return self._configuration - @staticmethod - def duplicate_entries_to_friendly_message( - entries: list[SimpleLineConfigParser.Entry], - ) -> str: - return "".join( - f"line {entry.line_number}: {entry.key}={entry.value}\n" - for entry in entries - ) + @property + def friendly_error_message(self) -> str: + raise NotImplementedError("Subclasses must implement this method.") def to_friendly_error(self) -> CruUserFriendlyException: - e = CruUserFriendlyException( - f"Duplicate entries found in config file:\n" - f"{self.duplicate_entries_to_friendly_message(self.duplicate_entries)}" - ) - return e + return CruUserFriendlyException(self.friendly_error_message) + def raise_friendly_error(self) -> NoReturn: + raise self.to_friendly_error() from self -class AppConfigFileNotFoundError(AppConfigError): - def __init__(self, message: str, file_path: str, *args, **kwargs) -> None: - super().__init__(message, *args, **kwargs) + +class AppConfigFileError(AppConfigError): + def __init__( + self, + message: str, + configuration: Configuration, + *args, + **kwargs, + ) -> None: + super().__init__(message, configuration, *args, **kwargs) + + +class AppConfigFileNotFoundError(AppConfigFileError): + def __init__( + self, + message: str, + configuration: Configuration, + file_path: str, + *args, + **kwargs, + ) -> None: + super().__init__(message, configuration, *args, **kwargs) self._file_path = file_path @property def file_path(self) -> str: return self._file_path - def to_friendly_error(self) -> CruUserFriendlyException: - e = CruUserFriendlyException( - f"Config file not found at {self.file_path}. You may need to create one." - ) - return e + @property + def friendly_error_message(self) -> str: + return f"Config file not found at {self.file_path}. You may need to create one." -class AppConfigItemNotSetError(AppConfigError): +class AppConfigFileParseError(AppConfigFileError): def __init__( self, message: str, - items: list[ConfigItem], + configuration: Configuration, + file_content: str, *args, + cause: ParseError | None = None, **kwargs, ) -> None: - super().__init__(message, *args, **kwargs) - self._items = items + super().__init__(message, configuration, *args, **kwargs) + self._file_content = file_content + if cause is not None: + self._cause = cause + self._cause = self.__cause__ # type: ignore + @property + def file_content(self) -> str: + return self._file_content + + @property + def friendly_error_message(self) -> str: + return f"Error while parsing config file at line {self._cause.line_number}." -class AppConfigItemNotDefinedError(AppConfigError): + +class AppConfigFileEntryError(AppConfigFileError): def __init__( self, message: str, - undefined_names: list[str], configuration: Configuration, + entries: Iterable[SimpleLineConfigParser.Entry], *args, **kwargs, ) -> None: - super().__init__(message, *args, **kwargs) - self._undefined_names = undefined_names - self._configuration = configuration + super().__init__(message, configuration, *args, **kwargs) + self._entries = list(entries) @property - def undefined_names(self) -> list[str]: - return self._undefined_names + def error_entries(self) -> list[SimpleLineConfigParser.Entry]: + return self._entries + + @staticmethod + def entries_to_friendly_message( + entries: Iterable[SimpleLineConfigParser.Entry], + ) -> str: + return "".join( + f"line {entry.line_number}: {entry.key}={entry.value}\n" + for entry in entries + ) @property - def configuration(self) -> Configuration: - return self._configuration + def friendly_message_head(self) -> str: + return "Error entries found in config file" + + @property + def friendly_error_message(self) -> str: + return ( + f"{self.friendly_message_head}:\n" + f"{self.entries_to_friendly_message(self.error_entries)}" + ) + + +class AppConfigDuplicateEntryError(AppConfigFileEntryError): + @property + def friendly_message_head(self) -> str: + return "Duplicate entries found in config file" + + +class AppConfigEntryKeyNotDefinedError(AppConfigFileEntryError): + @property + def friendly_message_head(self) -> str: + return "Entry key not defined in app config" + + +class AppConfigEntryValueFormatError(AppConfigFileEntryError): + @property + def friendly_message_head(self) -> str: + return "Invalid value format for entries" + + +class AppConfigItemNotSetError(AppConfigError): + def __init__( + self, + message: str, + items: list[ConfigItem], + *args, + **kwargs, + ) -> None: + super().__init__(message, *args, **kwargs) + self._items = items class ConfigManager(AppCommandFeatureProvider): @@ -178,50 +246,93 @@ class ConfigManager(AppCommandFeatureProvider): def _parse_config_file(self) -> SimpleLineConfigParser.Result: if not self.config_file_path.check_self(): raise AppConfigFileNotFoundError( - "Config file not found.", self.config_file_path.full_path_str + "Config file not found.", + self.configuration, + self.config_file_path.full_path_str, ) - parser = SimpleLineConfigParser() - return parser.parse(self.config_file_path.full_path.read_text()) + + text = self.config_file_path.full_path.read_text() + try: + parser = SimpleLineConfigParser() + return parser.parse(text) + except ParseError as e: + raise AppConfigFileParseError( + "Failed to parse config file.", self.configuration, text + ) from e def _check_duplicate( self, parse_result: dict[str, list[SimpleLineConfigParser.Entry]], - ) -> dict[str, str]: - config_dict = {} - duplicate_entries = [] + ) -> dict[str, SimpleLineConfigParser.Entry]: + entry_dict: dict[str, SimpleLineConfigParser.Entry] = {} + duplicate_entries: list[SimpleLineConfigParser.Entry] = [] for key, entries in parse_result.items(): - config_dict[key] = entries[0].value + entry_dict[key] = entries[0] for entry in entries[1:]: duplicate_entries.append(entry) if len(duplicate_entries) > 0: raise AppConfigDuplicateEntryError( - "Duplicate entries found.", duplicate_entries + "Duplicate entries found.", self.configuration, duplicate_entries ) - return config_dict + return entry_dict def _check_defined( - self, - config_dict: dict[str, str], - allow_extra: bool = True, - ) -> dict[str, str]: - # TODO: Continue here! - raise NotImplementedError() - - def _check_config_file(self) -> dict[str, str]: + self, entry_dict: dict[str, SimpleLineConfigParser.Entry] + ) -> dict[str, SimpleLineConfigParser.Entry]: + undefined: list[SimpleLineConfigParser.Entry] = [] + for key, entry in entry_dict.items(): + if not self.configuration.has_key(key): + undefined.append(entry) + if len(undefined) > 0: + raise AppConfigEntryKeyNotDefinedError( + "Entry keys are not defined in app config.", + self.configuration, + undefined, + ) + return entry_dict + + def _check_type( + self, entry_dict: dict[str, SimpleLineConfigParser.Entry] + ) -> dict[str, Any]: + value_dict: dict[str, Any] = {} + error_entries: list[SimpleLineConfigParser.Entry] = [] + errors: list[CruValueTypeError] = [] + for key, entry in entry_dict.items(): + config_item = self.configuration.get(key) + try: + value_dict[key] = config_item.value_type.convert_str_to_value( + entry.value + ) + except CruValueTypeError as e: + error_entries.append(entry) + errors.append(e) + if len(error_entries) > 0: + raise AppConfigEntryValueFormatError( + "Entry value format is not correct.", + self.configuration, + error_entries, + ) from ExceptionGroup("Multiple format errors occurred.", errors) + return value_dict + + def _read_config_file(self, friendly: bool = False) -> dict[str, Any]: try: parsed = self._parse_config_file() - config = self._check_duplicate(parsed) - return config - except ParseError as e: - raise CruUserFriendlyException("Failed to parse config file.") from e - except AppConfigDuplicateEntryError as e: - raise e.to_friendly_error() from e + entry_groups = parsed.cru_iter().group_by(lambda e: e.key) + entry_dict = self._check_duplicate(entry_groups) + entry_dict = self._check_defined(entry_dict) + value_dict = self._check_type(entry_dict) + return value_dict + except AppConfigError as e: + if friendly: + e.raise_friendly_error() + raise def reload_config_file(self) -> bool: self.configuration.reset_all() - config_dict = self._check_config_file() + value_dict = self._read_config_file() + # TODO: Continue here! for key, value in config_dict.items(): self.configuration.set(key, value) return True diff --git a/tools/cru-py/pyproject.toml b/tools/cru-py/pyproject.toml index 15da910..e5e7f09 100644 --- a/tools/cru-py/pyproject.toml +++ b/tools/cru-py/pyproject.toml @@ -1,3 +1,8 @@ +[project] +name = "cru-py" +version = "0.1.0" +requires-python = ">=3.11" + [tool.poetry] package-mode = false name = "cru" |