diff options
| author | crupest <crupest@outlook.com> | 2024-11-11 01:12:29 +0800 | 
|---|---|---|
| committer | Yuqian Yang <crupest@crupest.life> | 2025-01-14 22:36:08 +0800 | 
| commit | 5a4898d8fe48de877e3a9d35db3774fefd4a5be9 (patch) | |
| tree | 0cb89834a1ed32bdf20597309c78c3a5cbca3c88 | |
| parent | 9ca818450a89b98c1de063b15ffbd3e7c6615f50 (diff) | |
| download | crupest-5a4898d8fe48de877e3a9d35db3774fefd4a5be9.tar.gz crupest-5a4898d8fe48de877e3a9d35db3774fefd4a5be9.tar.bz2 crupest-5a4898d8fe48de877e3a9d35db3774fefd4a5be9.zip | |
HALF WORK: 2024.1.14
| -rw-r--r-- | tools/cru-py/cru/config.py | 16 | ||||
| -rw-r--r-- | tools/cru-py/cru/list.py | 4 | ||||
| -rw-r--r-- | tools/cru-py/cru/parsing.py | 14 | ||||
| -rw-r--r-- | tools/cru-py/cru/service/_config.py | 247 | 
4 files changed, 174 insertions, 107 deletions
| diff --git a/tools/cru-py/cru/config.py b/tools/cru-py/cru/config.py index 497eb01..bd8eaf9 100644 --- a/tools/cru-py/cru/config.py +++ b/tools/cru-py/cru/config.py @@ -1,6 +1,6 @@  from __future__ import annotations -from typing import TypeVar, Generic +from typing import Any, TypeVar, Generic  from ._error import CruInternalError, CruException  from .list import CruUniqueKeyList @@ -124,7 +124,7 @@ class ConfigItem(Generic[_T]):          ) -class Configuration(CruUniqueKeyList[ConfigItem, str]): +class Configuration(CruUniqueKeyList[ConfigItem[Any], str]):      def __init__(self):          super().__init__(lambda c: c.name) @@ -134,7 +134,7 @@ class Configuration(CruUniqueKeyList[ConfigItem, str]):          description: str,          value: str | None = None,          default: ValueGeneratorBase[str] | str | None = None, -    ) -> ConfigItem: +    ) -> ConfigItem[str]:          item = ConfigItem(name, description, TEXT_VALUE_TYPE, value, default)          self.add(item)          return item @@ -145,7 +145,7 @@ class Configuration(CruUniqueKeyList[ConfigItem, str]):          description: str,          value: int | None = None,          default: ValueGeneratorBase[int] | int | None = None, -    ) -> ConfigItem: +    ) -> ConfigItem[int]:          item = ConfigItem(name, description, INTEGER_VALUE_TYPE, value, default)          self.add(item)          return item @@ -153,3 +153,11 @@ class Configuration(CruUniqueKeyList[ConfigItem, str]):      def reset_all(self, clear_default_cache=False) -> None:          for item in self:              item.reset(clear_default_cache) + +    def to_dict(self) -> dict[str, Any]: +        return {item.name: item.value for item in self} + +    def to_str_dict(self) -> dict[str, str]: +        return { +            item.name: item.value_type.convert_value_to_str(item.value) for item in self +        } diff --git a/tools/cru-py/cru/list.py b/tools/cru-py/cru/list.py index c65c793..53c3c77 100644 --- a/tools/cru-py/cru/list.py +++ b/tools/cru-py/cru/list.py @@ -93,6 +93,10 @@ class CruUniqueKeyList(Generic[_T, _K]):              raise KeyError(f"Key {key} not found!")          return value  # type: ignore +    @property +    def keys(self) -> Iterable[_K]: +        return self._list.as_cru_iterator().map(self._key_getter) +      def has_key(self, key: _K) -> bool:          return self.get_or(key) != CruNotFound.VALUE diff --git a/tools/cru-py/cru/parsing.py b/tools/cru-py/cru/parsing.py index 598a2bb..1d2fa7f 100644 --- a/tools/cru-py/cru/parsing.py +++ b/tools/cru-py/cru/parsing.py @@ -9,7 +9,7 @@ from ._iter import CruIterable  _T = TypeVar("_T") -class ParseException(CruException, Generic[_T]): +class ParseError(CruException, Generic[_T]):      def __init__(          self,          message, @@ -53,16 +53,16 @@ class Parser(Generic[_T], metaclass=ABCMeta):          self, text: str, line_number: int | None = None      ) -> NoReturn:          a = line_number and f" at line {line_number}" or "" -        raise ParseException(f"Parser {self.name} failed{a}.", self, text, line_number) +        raise ParseError(f"Parser {self.name} failed{a}.", self, text, line_number) -class SimpleLineConfigParserItem(NamedTuple): +class SimpleLineConfigParserEntry(NamedTuple):      key: str      value: str      line_number: int | None = None -class SimpleLineConfigParserResult(CruIterable.IterList[SimpleLineConfigParserItem]): +class SimpleLineConfigParserResult(CruIterable.IterList[SimpleLineConfigParserEntry]):      pass @@ -71,13 +71,13 @@ class SimpleLineConfigParser(Parser[SimpleLineConfigParserResult]):      The parsing result is a list of tuples (key, value, line number).      """ -    Item: TypeAlias = SimpleLineConfigParserItem +    Entry: TypeAlias = SimpleLineConfigParserEntry      Result: TypeAlias = SimpleLineConfigParserResult      def __init__(self) -> None:          super().__init__(type(self).__name__) -    def _parse(self, text: str, callback: Callable[[Item], None]) -> None: +    def _parse(self, text: str, callback: Callable[[Entry], None]) -> None:          for ln, line in enumerate(text.splitlines()):              line_number = ln + 1              # check if it's a comment @@ -90,7 +90,7 @@ class SimpleLineConfigParser(Parser[SimpleLineConfigParserResult]):              key, value = line.split("=", 1)              key = key.strip()              value = value.strip() -            callback(SimpleLineConfigParserItem(key, value, line_number)) +            callback(SimpleLineConfigParserEntry(key, value, line_number))      def parse(self, text: str) -> Result:          result = SimpleLineConfigParserResult() diff --git a/tools/cru-py/cru/service/_config.py b/tools/cru-py/cru/service/_config.py index 6e1edda..12784c5 100644 --- a/tools/cru-py/cru/service/_config.py +++ b/tools/cru-py/cru/service/_config.py @@ -6,7 +6,7 @@ from cru.value import (      RandomStringValueGenerator,      UuidValueGenerator,  ) -from cru.parsing import SimpleLineConfigParser +from cru.parsing import ParseError, SimpleLineConfigParser  from ._base import AppFeaturePath, AppCommandFeatureProvider, OWNER_NAME @@ -15,160 +15,215 @@ class AppConfigError(CruException):      pass -class AppConfigDuplicateItemsError(AppConfigError): +class AppConfigDuplicateEntryError(AppConfigError):      def __init__( -        self, message: str, items: list[SimpleLineConfigParser.Item], *args, **kwargs +        self, message: str, entries: list[SimpleLineConfigParser.Entry], *args, **kwargs      ) -> None:          super().__init__(message, *args, **kwargs) -        self._items = items +        self._entries = entries      @property -    def duplicate_items(self) -> list[SimpleLineConfigParser.Item]: -        return self._items +    def duplicate_entries(self) -> list[SimpleLineConfigParser.Entry]: +        return self._entries      @staticmethod -    def duplicate_items_to_friendly_message( -        items: list[SimpleLineConfigParser.Item], +    def duplicate_entries_to_friendly_message( +        entries: list[SimpleLineConfigParser.Entry],      ) -> str:          return "".join( -            f"line {item.line_number}: {item.key}={item.value}\n" for item in items +            f"line {entry.line_number}: {entry.key}={entry.value}\n" +            for entry in entries          )      def to_friendly_error(self) -> CruUserFriendlyException:          e = CruUserFriendlyException( -            f"Duplicate configuration items detected:\n" -            f"{self.duplicate_items_to_friendly_message(self.duplicate_items)}" +            f"Duplicate entries found in config file:\n" +            f"{self.duplicate_entries_to_friendly_message(self.duplicate_entries)}"          ) -        e.__cause__ = self          return e +class AppConfigFileNotFoundError(AppConfigError): +    def __init__(self, message: str, file_path: str, *args, **kwargs) -> None: +        super().__init__(message, *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 + + +class AppConfigItemNotSetError(AppConfigError): +    def __init__( +        self, +        message: str, +        items: list[ConfigItem], +        *args, +        **kwargs, +    ) -> None: +        super().__init__(message, *args, **kwargs) +        self._items = items + + +class AppConfigItemNotDefinedError(AppConfigError): +    def __init__( +        self, +        message: str, +        undefined_names: list[str], +        configuration: Configuration, +        *args, +        **kwargs, +    ) -> None: +        super().__init__(message, *args, **kwargs) +        self._undefined_names = undefined_names +        self._configuration = configuration + +    @property +    def undefined_names(self) -> list[str]: +        return self._undefined_names + +    @property +    def configuration(self) -> Configuration: +        return self._configuration + +  class ConfigManager(AppCommandFeatureProvider):      def __init__(self) -> None:          super().__init__("config-manager")          configuration = Configuration()          self._configuration = configuration -        self._add_text_item("DOMAIN", "domain name") -        self._add_text_item("EMAIL", "admin email address") -        self._add_text_item( +        self._init_app_defined_items() + +    def _init_app_defined_items(self) -> None: +        def _add_text(name: str, description: str) -> None: +            self.configuration.add( +                ConfigItem(f"{OWNER_NAME}_{name}", description, TEXT_VALUE_TYPE) +            ) + +        def _add_uuid(name: str, description: str) -> None: +            self.configuration.add( +                ConfigItem( +                    f"{OWNER_NAME}_{name}", +                    description, +                    TEXT_VALUE_TYPE, +                    default=UuidValueGenerator(), +                ) +            ) + +        def _add_random_string( +            name: str, description: str, length: int = 32, secure: bool = True +        ) -> None: +            self.configuration.add( +                ConfigItem( +                    f"{OWNER_NAME}_{name}", +                    description, +                    TEXT_VALUE_TYPE, +                    default=RandomStringValueGenerator(length, secure), +                ) +            ) + +        def _add_int(name: str, description: str) -> None: +            self.configuration.add( +                ConfigItem(f"{OWNER_NAME}_{name}", description, INTEGER_VALUE_TYPE) +            ) + +        _add_text("DOMAIN", "domain name") +        _add_text("EMAIL", "admin email address") +        _add_text(              "AUTO_BACKUP_COS_SECRET_ID",              "access key id for Tencent COS, used for auto backup",          ) -        self._add_text_item( +        _add_text(              "AUTO_BACKUP_COS_SECRET_KEY",              "access key secret for Tencent COS, used for auto backup",          ) -        self._add_text_item( +        _add_text(              "AUTO_BACKUP_COS_REGION", "region for Tencent COS, used for auto backup"          ) -        self._add_text_item( +        _add_text(              "AUTO_BACKUP_BUCKET_NAME",              "bucket name for Tencent COS, used for auto backup",          ) -        self._add_text_item("GITHUB_USERNAME", "github username for fetching todos") -        self._add_int_item( -            "GITHUB_PROJECT_NUMBER", "github project number for fetching todos" -        ) -        self._add_text_item("GITHUB_TOKEN", "github token for fetching todos") -        self._add_text_item("GITHUB_TODO_COUNT", "github todo count") -        self._add_uuid_item("V2RAY_TOKEN", "v2ray user id") -        self._add_uuid_item("V2RAY_PATH", "v2ray path, which will be prefixed by _") -        self._add_text_item("FORGEJO_MAILER_USER", "Forgejo SMTP user") -        self._add_text_item("FORGEJO_MAILER_PASSWD", "Forgejo SMTP password") -        self._add_random_string_item("2FAUTH_APP_KEY", "2FAuth App Key") -        self._add_text_item("2FAUTH_MAIL_USERNAME", "2FAuth SMTP user") -        self._add_text_item("2FAUTH_MAIL_PASSWORD", "2FAuth SMTP password") - -    def _add_text_item(self, name: str, description: str) -> None: -        self.configuration.add( -            ConfigItem(f"{OWNER_NAME}_{name}", description, TEXT_VALUE_TYPE) -        ) - -    def _add_uuid_item(self, name: str, description: str) -> None: -        self.configuration.add( -            ConfigItem( -                f"{OWNER_NAME}_{name}", -                description, -                TEXT_VALUE_TYPE, -                default=UuidValueGenerator(), -            ) -        ) - -    def _add_random_string_item( -        self, name: str, description: str, length: int = 32, secure: bool = True -    ) -> None: -        self.configuration.add( -            ConfigItem( -                f"{OWNER_NAME}_{name}", -                description, -                TEXT_VALUE_TYPE, -                default=RandomStringValueGenerator(length, secure), -            ) -        ) - -    def _add_int_item(self, name: str, description: str) -> None: -        self.configuration.add( -            ConfigItem(f"{OWNER_NAME}_{name}", description, INTEGER_VALUE_TYPE) -        ) +        _add_text("GITHUB_USERNAME", "github username for fetching todos") +        _add_int("GITHUB_PROJECT_NUMBER", "github project number for fetching todos") +        _add_text("GITHUB_TOKEN", "github token for fetching todos") +        _add_text("GITHUB_TODO_COUNT", "github todo count") +        _add_uuid("V2RAY_TOKEN", "v2ray user id") +        _add_uuid("V2RAY_PATH", "v2ray path, which will be prefixed by _") +        _add_text("FORGEJO_MAILER_USER", "Forgejo SMTP user") +        _add_text("FORGEJO_MAILER_PASSWD", "Forgejo SMTP password") +        _add_random_string("2FAUTH_APP_KEY", "2FAuth App Key") +        _add_text("2FAUTH_MAIL_USERNAME", "2FAuth SMTP user") +        _add_text("2FAUTH_MAIL_PASSWORD", "2FAuth SMTP password")      def setup(self) -> None: -        self._config_path = self.app.data_dir.add_subpath( +        self._config_file_path = self.app.data_dir.add_subpath(              "config", False, description="Configuration file path."          )      @property -    def config_file_path(self) -> AppFeaturePath: -        return self._config_path - -    @property      def configuration(self) -> Configuration:          return self._configuration      @property -    def config_keys(self) -> list[str]: -        return [item.name for item in self.configuration] - -    @property -    def config_map(self) -> dict[str, str]: -        raise NotImplementedError() +    def config_file_path(self) -> AppFeaturePath: +        return self._config_file_path -    def _parse_config_file(self) -> SimpleLineConfigParser.Result | None: +    def _parse_config_file(self) -> SimpleLineConfigParser.Result:          if not self.config_file_path.check_self(): -            return None +            raise AppConfigFileNotFoundError( +                "Config file not found.", self.config_file_path.full_path_str +            )          parser = SimpleLineConfigParser()          return parser.parse(self.config_file_path.full_path.read_text())      def _check_duplicate(          self, -        result: SimpleLineConfigParser.Result -        | dict[str, list[SimpleLineConfigParser.Item]], +        parse_result: dict[str, list[SimpleLineConfigParser.Entry]],      ) -> dict[str, str]: -        if isinstance(result, SimpleLineConfigParser.Result): -            result = result.cru_iter().group_by(lambda i: i.key) - -        config = {} -        error_items = [] -        for key, items in result.items(): -            config[key] = items[0].value -            for item in items[1:]: -                error_items.append(item) +        config_dict = {} +        duplicate_entries = [] +        for key, entries in parse_result.items(): +            config_dict[key] = entries[0].value +            for entry in entries[1:]: +                duplicate_entries.append(entry) -        if len(error_items) > 0: -            raise AppConfigDuplicateItemsError("Duplicate items found.", error_items) +        if len(duplicate_entries) > 0: +            raise AppConfigDuplicateEntryError( +                "Duplicate entries found.", duplicate_entries +            ) -        return config +        return config_dict -    def _check_config_file(self) -> dict[str, str]: +    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]: +        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 +      def reload_config_file(self) -> bool:          self.configuration.reset_all() -        if not self.config_file_path.check_self(): -            return False -        parser = SimpleLineConfigParser() -        parse_result = parser.parse(self.config_file_path.full_path.read_text()) -        config_dict = parse_result.cru_iter().group_by(lambda i: i.key) +        config_dict = self._check_config_file() +        for key, value in config_dict.items(): +            self.configuration.set(key, value)          return True      def print_app_config_info(self): | 
