aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2024-11-11 01:12:29 +0800
committerYuqian Yang <crupest@crupest.life>2025-01-09 18:01:15 +0800
commit47ebda69daa34ea7992b6bbadf46de98dd17a390 (patch)
tree8d169d75c9bd9a48f47383f245e371e57ea2eb8f /tools
parent43892b892cfdc4e15f7ab191c42ccb32279fd7f6 (diff)
downloadcrupest-47ebda69daa34ea7992b6bbadf46de98dd17a390.tar.gz
crupest-47ebda69daa34ea7992b6bbadf46de98dd17a390.tar.bz2
crupest-47ebda69daa34ea7992b6bbadf46de98dd17a390.zip
HALF WORK: 2024.1.9
Diffstat (limited to 'tools')
-rw-r--r--tools/cru-py/cru/_error.py6
-rw-r--r--tools/cru-py/cru/service/__main__.py49
-rw-r--r--tools/cru-py/cru/service/_app.py24
-rw-r--r--tools/cru-py/cru/service/_base.py153
-rw-r--r--tools/cru-py/cru/service/_config.py3
-rw-r--r--tools/cru-py/cru/service/_data.py8
-rw-r--r--tools/cru-py/cru/template.py11
-rw-r--r--tools/manage.cmd15
8 files changed, 174 insertions, 95 deletions
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,17 +29,9 @@ 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
@@ -48,10 +39,6 @@ class AppPath(ABC):
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,10 +114,15 @@ 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%" %*