From 47ebda69daa34ea7992b6bbadf46de98dd17a390 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 11 Nov 2024 01:12:29 +0800 Subject: HALF WORK: 2024.1.9 --- tools/cru-py/cru/_error.py | 6 +- tools/cru-py/cru/service/__main__.py | 49 ++--------- tools/cru-py/cru/service/_app.py | 24 ++++++ tools/cru-py/cru/service/_base.py | 153 ++++++++++++++++++++++++++--------- tools/cru-py/cru/service/_config.py | 3 +- tools/cru-py/cru/service/_data.py | 8 +- tools/cru-py/cru/template.py | 11 +-- tools/manage.cmd | 15 ++++ 8 files changed, 174 insertions(+), 95 deletions(-) create mode 100644 tools/cru-py/cru/service/_app.py create mode 100644 tools/manage.cmd (limited to 'tools') diff --git a/tools/cru-py/cru/_error.py b/tools/cru-py/cru/_error.py index 0d2bf79..e4bf3d6 100644 --- a/tools/cru-py/cru/_error.py +++ b/tools/cru-py/cru/_error.py @@ -18,9 +18,11 @@ class CruInternalError(CruException): class CruUserFriendlyException(CruException): - def __init__(self, message: str, user_message: str, *args, **kwargs) -> None: + def __init__( + self, message: str, user_message: str | None = None, *args, **kwargs + ) -> None: super().__init__(message, *args, **kwargs) - self._user_message = user_message + self._user_message = user_message or message @property def user_message(self) -> str: diff --git a/tools/cru-py/cru/service/__main__.py b/tools/cru-py/cru/service/__main__.py index 923c25b..c218bc6 100644 --- a/tools/cru-py/cru/service/__main__.py +++ b/tools/cru-py/cru/service/__main__.py @@ -1,45 +1,6 @@ -from pathlib import Path +from cru import CruUserFriendlyException -from cru import CruException - -from ._base import AppBase, DATA_DIR_NAME, CommandDispatcher -from ._config import ConfigManager -from ._data import DataManager -from ._template import TemplateManager - - -class App(AppBase): - def __init__(self, root: str): - super().__init__("crupest-service", root) - self.add_feature(DataManager()) - self.add_feature(ConfigManager()) - self.add_feature(TemplateManager()) - self.add_feature(CommandDispatcher()) - - def setup(self): - for feature in self.features: - feature.setup() - - def run_command(self): - command_dispatcher = self.get_feature(CommandDispatcher) - command_dispatcher.run_command() - - -def _find_root() -> Path: - cwd = Path.cwd() - data_dir = cwd / DATA_DIR_NAME - if data_dir.is_dir(): - return data_dir - raise CruException( - "No valid data directory found. Please run 'init' to create one." - ) - - -def create_app() -> App: - root = _find_root() - app = App(str(root)) - app.setup() - return app +from ._app import create_app def main(): @@ -48,4 +9,8 @@ def main(): if __name__ == "__main__": - main() + try: + main() + except CruUserFriendlyException as e: + print(f"Error: {e.user_message}") + exit(1) diff --git a/tools/cru-py/cru/service/_app.py b/tools/cru-py/cru/service/_app.py new file mode 100644 index 0000000..a656e3b --- /dev/null +++ b/tools/cru-py/cru/service/_app.py @@ -0,0 +1,24 @@ +from ._base import AppBase, CommandDispatcher, AppInitializer, OWNER_NAME +from ._config import ConfigManager +from ._data import DataManager +from ._template import TemplateManager + + +class App(AppBase): + def __init__(self): + super().__init__(f"{OWNER_NAME}-service") + self.add_feature(AppInitializer()) + self.add_feature(DataManager()) + self.add_feature(ConfigManager()) + self.add_feature(TemplateManager()) + self.add_feature(CommandDispatcher()) + + def run_command(self): + command_dispatcher = self.get_feature(CommandDispatcher) + command_dispatcher.run_command() + + +def create_app() -> App: + app = App() + app.setup() + return app diff --git a/tools/cru-py/cru/service/_base.py b/tools/cru-py/cru/service/_base.py index 3b511a1..124acd5 100644 --- a/tools/cru-py/cru/service/_base.py +++ b/tools/cru-py/cru/service/_base.py @@ -3,14 +3,13 @@ from __future__ import annotations from argparse import ArgumentParser, Namespace from abc import ABC, abstractmethod import argparse -from collections.abc import Sequence import os from pathlib import Path from typing import TypeVar, overload -from cru import CruException, CruInternalError, CruPath +from cru import CruException, CruInternalError, CruPath, CruUserFriendlyException -_F = TypeVar("_F") +_Feature = TypeVar("_Feature", bound="AppFeatureProvider") OWNER_NAME = "crupest" @@ -30,27 +29,15 @@ class AppPathError(CruException): class AppPath(ABC): - def __init__( - self, - name: str, - is_dir: bool, - /, - id: str | None = None, - description: str = "", - ) -> None: - self._name = name + def __init__(self, id: str, is_dir: bool, description: str) -> None: self._is_dir = is_dir - self._id = id or name + self._id = id self._description = description @property @abstractmethod def parent(self) -> AppPath | None: ... - @property - def name(self) -> str: - return self._name - @property @abstractmethod def app(self) -> AppBase: ... @@ -68,11 +55,8 @@ class AppPath(ABC): return self._is_dir @property - def full_path(self) -> CruPath: - if self.parent is None: - return CruPath(self.name) - else: - return CruPath(self.parent.full_path, self.name) + @abstractmethod + def full_path(self) -> CruPath: ... @property def full_path_str(self) -> str: @@ -92,12 +76,12 @@ class AppPath(ABC): if not self.full_path.is_dir(): raise AppPathError("Should be a directory, but not.", self.full_path) else: - return False + return True else: if not self.full_path.is_file(): raise AppPathError("Should be a file, but not.", self.full_path) else: - return False + return True def ensure(self, create_file: bool = False) -> None: e = self.check_self(False) @@ -130,9 +114,14 @@ class AppFeaturePath(AppPath): id: str | None = None, description: str = "", ) -> None: - super().__init__(name, is_dir, id, description) + super().__init__(id or name, is_dir, description) + self._name = name self._parent = parent + @property + def name(self) -> str: + return self._name + @property def parent(self) -> AppPath: return self._parent @@ -141,11 +130,16 @@ class AppFeaturePath(AppPath): def app(self) -> AppBase: return self.parent.app + @property + def full_path(self) -> CruPath: + return CruPath(self.parent.full_path, self.name) + class AppRootPath(AppPath): - def __init__(self, app: AppBase, path: str): - super().__init__(path, True, "root", "Application root path.") + def __init__(self, app: AppBase): + super().__init__("root", True, "Application root path.") self._app = app + self._full_path: CruPath | None = None @property def parent(self) -> None: @@ -155,13 +149,23 @@ class AppRootPath(AppPath): def app(self) -> AppBase: return self._app + @property + def full_path(self) -> CruPath: + if self._full_path is None: + raise CruInternalError("App root path is not set yet.") + return self._full_path + + def setup(self, path: os.PathLike) -> None: + if self._full_path is not None: + raise CruInternalError("App root path is already set.") + self._full_path = CruPath(path) + class AppFeatureProvider(ABC): def __init__(self, name: str, /, app: AppBase | None = None): super().__init__() self._name = name self._app = app if app else AppBase.get_instance() - self.app.add_feature(self) @property def app(self) -> AppBase: @@ -192,10 +196,17 @@ DATA_DIR_NAME = "data" class CommandDispatcher(AppFeatureProvider): def __init__(self) -> None: super().__init__("command-dispatcher") + self._parsed_args: argparse.Namespace | None = None - def _setup_arg_parser(self) -> None: + def setup_arg_parser(self) -> None: self._map: dict[str, AppCommandFeatureProvider] = {} arg_parser = argparse.ArgumentParser(description="Service management") + arg_parser.add_argument( + "--project-dir", + help="The path of the project directory.", + required=True, + type=str, + ) subparsers = arg_parser.add_subparsers(dest="command") for feature in self.app.features: if isinstance(feature, AppCommandFeatureProvider): @@ -206,7 +217,7 @@ class CommandDispatcher(AppFeatureProvider): self._arg_parser = arg_parser def setup(self): - self._setup_arg_parser() + pass @property def arg_parser(self) -> argparse.ArgumentParser: @@ -216,9 +227,41 @@ class CommandDispatcher(AppFeatureProvider): def map(self) -> dict[str, AppCommandFeatureProvider]: return self._map - def run_command(self, _args: Sequence[str] | None = None) -> None: - args = self.arg_parser.parse_args(_args) - self.map[args.command].run_command(args) + def get_program_parsed_args(self) -> argparse.Namespace: + if self._parsed_args is None: + self._parsed_args = self.arg_parser.parse_args() + return self._parsed_args + + def run_command(self, args: argparse.Namespace | None = None) -> None: + real_args = args or self.get_program_parsed_args() + self.map[real_args.command].run_command(real_args) + + +class AppInitializer(AppCommandFeatureProvider): + def __init__(self) -> None: + super().__init__("app-initializer") + + def _init_app(self) -> bool: + if self.app.app_initialized: + return False + self.app.data_dir.ensure() + return True + + def setup(self): + pass + + def get_command_info(self): + return ("init", "Initialize the app.") + + def setup_arg_parser(self, arg_parser): + pass + + def run_command(self, args): + init = self._init_app() + if init: + print("App initialized successfully.") + else: + print("App is already initialized. Do nothing.") class AppBase: @@ -230,13 +273,24 @@ class AppBase: raise CruInternalError("App instance not initialized") return AppBase._instance - def __init__(self, name: str, root: str): + def __init__(self, name: str): AppBase._instance = self self._name = name - self._root = AppRootPath(self, root) + self._root = AppRootPath(self) self._paths: list[AppFeaturePath] = [] self._features: list[AppFeatureProvider] = [] + def setup(self) -> None: + command_dispatcher = self.get_feature(CommandDispatcher) + command_dispatcher.setup_arg_parser() + program_args = command_dispatcher.get_program_parsed_args() + self.setup_root(program_args.project_dir) + self._data_dir = self.add_path(DATA_DIR_NAME, True, id="data") + for feature in self.features: + feature.setup() + for path in self.paths: + path.check_self() + @property def name(self) -> str: return self._name @@ -245,6 +299,24 @@ class AppBase: def root(self) -> AppRootPath: return self._root + def setup_root(self, path: os.PathLike) -> None: + self._root.setup(path) + + @property + def data_dir(self) -> AppFeaturePath: + return self._data_dir + + @property + def app_initialized(self) -> bool: + return self.data_dir.check_self() + + def ensure_app_initialized(self) -> AppRootPath: + if not self.app_initialized: + raise CruUserFriendlyException( + "Root directory does not exist. Please run 'init' to create one." + ) + return self.root + @property def features(self) -> list[AppFeatureProvider]: return self._features @@ -253,7 +325,10 @@ class AppBase: def paths(self) -> list[AppFeaturePath]: return self._paths - def add_feature(self, feature: AppFeatureProvider) -> AppFeatureProvider: + def add_feature(self, feature: _Feature) -> _Feature: + for f in self.features: + if f.name == feature.name: + raise CruInternalError(f"Duplicate feature name: {feature.name}.") self._features.append(feature) return feature @@ -276,9 +351,11 @@ class AppBase: def get_feature(self, feature: str) -> AppFeatureProvider: ... @overload - def get_feature(self, feature: type[_F]) -> _F: ... + def get_feature(self, feature: type[_Feature]) -> _Feature: ... - def get_feature(self, feature: str | type[_F]) -> AppFeatureProvider | _F: + def get_feature( + self, feature: str | type[_Feature] + ) -> AppFeatureProvider | _Feature: if isinstance(feature, str): for f in self._features: if f.name == feature: diff --git a/tools/cru-py/cru/service/_config.py b/tools/cru-py/cru/service/_config.py index 1c3a571..3bfb6c9 100644 --- a/tools/cru-py/cru/service/_config.py +++ b/tools/cru-py/cru/service/_config.py @@ -1,5 +1,4 @@ from ._base import AppFeaturePath, AppFeatureProvider -from ._data import DataManager class ConfigManager(AppFeatureProvider): @@ -7,7 +6,7 @@ class ConfigManager(AppFeatureProvider): super().__init__("config-manager") def setup(self) -> None: - self._config_path = self.app.get_feature(DataManager).data_dir.add_subpath( + self._config_path = self.app.data_dir.add_subpath( "config", False, description="Configuration file path." ) diff --git a/tools/cru-py/cru/service/_data.py b/tools/cru-py/cru/service/_data.py index 79a1f64..885c8e8 100644 --- a/tools/cru-py/cru/service/_data.py +++ b/tools/cru-py/cru/service/_data.py @@ -1,4 +1,4 @@ -from ._base import AppFeaturePath, AppFeatureProvider +from ._base import AppFeatureProvider class DataManager(AppFeatureProvider): @@ -6,8 +6,4 @@ class DataManager(AppFeatureProvider): super().__init__("data-manager") def setup(self) -> None: - self._dir = self.app.add_path("data", True) - - @property - def data_dir(self) -> AppFeaturePath: - return self._dir + pass diff --git a/tools/cru-py/cru/template.py b/tools/cru-py/cru/template.py index a02ea0e..a07ca23 100644 --- a/tools/cru-py/cru/template.py +++ b/tools/cru-py/cru/template.py @@ -1,6 +1,7 @@ from collections.abc import Mapping import os import os.path +from pathlib import Path from string import Template from ._iter import CruIterator @@ -99,13 +100,13 @@ class TemplateTree: @staticmethod def _scan_files(root_path: str) -> list[str]: - files: list[str] = [] + result: list[str] = [] for root, _dirs, files in os.walk(root_path): for file in files: - path = os.path.join(root, file) - path = os.path.relpath(path, root_path) - files.append(path) - return files + path = Path(root, file) + path = path.relative_to(root_path) + result.append(str(path.as_posix())) + return result def _load(self) -> None: files = self._scan_files(self.source) diff --git a/tools/manage.cmd b/tools/manage.cmd new file mode 100644 index 0000000..fce913d --- /dev/null +++ b/tools/manage.cmd @@ -0,0 +1,15 @@ +@echo off + +set PYTHON=py -3 +%PYTHON% --version >NUL 2>&1 || ( + echo Error: failed to run Python with py -3 --version. + exit 1 +) + +set TOOLS_DIR=%~dp0 +set PROJECT_DIR=%TOOLS_DIR%.. + +cd /d "%PROJECT_DIR%" + +set PYTHONPATH=%PROJECT_DIR%\tools\cru-py;%PYTHONPATH% +%PYTHON% -m cru.service --project-dir "%PROJECT_DIR%" %* -- cgit v1.2.3