dothat 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dothat/VERSION +2 -0
- dothat/__init__.py +9 -0
- dothat/cli.py +97 -0
- dothat/default_tasks/__init__.py +3 -0
- dothat/default_tasks/_actions.py +8 -0
- dothat/default_tasks/_params.py +11 -0
- dothat/default_tasks/_tasks.py +19 -0
- dothat/di.py +27 -0
- dothat/initialization/__init__.py +45 -0
- dothat/initialization/templates/_tasks/__init__.py +3 -0
- dothat/initialization/templates/_tasks/_actions.py +38 -0
- dothat/initialization/templates/_tasks/_consts.py +5 -0
- dothat/initialization/templates/_tasks/_params.py +20 -0
- dothat/initialization/templates/_tasks/_tasks.py +39 -0
- dothat/logging.py +30 -0
- dothat/models/__init__.py +9 -0
- dothat/models/actions.py +54 -0
- dothat/models/params.py +42 -0
- dothat/project_config/__init__.py +46 -0
- dothat/py.typed +0 -0
- dothat/registry.py +80 -0
- dothat/task_builder.py +138 -0
- dothat/task_generator.py +32 -0
- dothat/tasks_core/default_tasks/__init__.py +3 -0
- dothat/tasks_core/default_tasks/_actions.py +8 -0
- dothat/tasks_core/default_tasks/_params.py +11 -0
- dothat/tasks_core/default_tasks/_tasks.py +19 -0
- dothat-1.0.0.dist-info/METADATA +13 -0
- dothat-1.0.0.dist-info/RECORD +31 -0
- dothat-1.0.0.dist-info/WHEEL +4 -0
- dothat-1.0.0.dist-info/entry_points.txt +2 -0
dothat/VERSION
ADDED
dothat/__init__.py
ADDED
dothat/cli.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import dataclasses
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import injector
|
|
7
|
+
|
|
8
|
+
from dothat.di import Module
|
|
9
|
+
from dothat.logging import logger as LOGGER
|
|
10
|
+
from dothat.registry import TaskRegistry
|
|
11
|
+
from dothat.task_builder import Task
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class DothatContext:
|
|
16
|
+
di: injector.Injector
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_registry(di: injector.Injector, module: str | None) -> dict[str, Task]:
|
|
20
|
+
if module:
|
|
21
|
+
return di.get(TaskRegistry).load_tasks_from_module(module)
|
|
22
|
+
return di.get(TaskRegistry).tasks
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
@click.option(
|
|
27
|
+
"--config",
|
|
28
|
+
type=pathlib.Path,
|
|
29
|
+
envvar="ENVIRONMENT_TOML_PATH",
|
|
30
|
+
default=pathlib.Path(
|
|
31
|
+
"environment.toml",
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def cli(ctx: click.Context, config: pathlib.Path):
|
|
36
|
+
ctx.obj = DothatContext(di=injector.Injector([Module(config_path=config)]))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command()
|
|
40
|
+
@click.option("--module", default=None)
|
|
41
|
+
@click.pass_obj
|
|
42
|
+
def list(dothat_context: DothatContext, module: str | None):
|
|
43
|
+
_registry = get_registry(dothat_context.di, module)
|
|
44
|
+
|
|
45
|
+
for key, task in _registry.items():
|
|
46
|
+
click.echo(f"R {key:<50} {task.doc}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@cli.command()
|
|
50
|
+
@click.argument("task_name")
|
|
51
|
+
@click.option("--module", default=None)
|
|
52
|
+
@click.pass_obj
|
|
53
|
+
def help(dothat_context: DothatContext, task_name: str, module: str):
|
|
54
|
+
_registry = get_registry(di=dothat_context.di, module=module)
|
|
55
|
+
|
|
56
|
+
task = _registry[task_name]
|
|
57
|
+
|
|
58
|
+
click.echo(f"{task_name} {task.doc}")
|
|
59
|
+
|
|
60
|
+
for param in task.params:
|
|
61
|
+
description: builtins.list[str] = [f"config: {param.name}"]
|
|
62
|
+
|
|
63
|
+
if param.env_var:
|
|
64
|
+
description.append(f"environ: {param.env_var}")
|
|
65
|
+
if param.type is bool and param.inverse:
|
|
66
|
+
description.append(f"opposite of {param.inverse}")
|
|
67
|
+
|
|
68
|
+
click.echo(f" --{param.long:<30} ({', '.join(description)})")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@cli.command(
|
|
72
|
+
context_settings={
|
|
73
|
+
"ignore_unknown_options": True,
|
|
74
|
+
"allow_extra_args": True,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
@click.argument("task_name")
|
|
78
|
+
@click.pass_context
|
|
79
|
+
@click.option("--module", default=None)
|
|
80
|
+
def run(ctx: click.Context, task_name: str, module: str | None):
|
|
81
|
+
_registry = get_registry(di=ctx.obj.di, module=module)
|
|
82
|
+
|
|
83
|
+
task = _registry.get(task_name)
|
|
84
|
+
|
|
85
|
+
if task is None:
|
|
86
|
+
raise click.ClickException(f"Unknown task: {task_name}")
|
|
87
|
+
task = task.model_copy(deep=True)
|
|
88
|
+
|
|
89
|
+
task.parse_task_params(args=ctx.args, di=ctx.obj.di)
|
|
90
|
+
|
|
91
|
+
LOGGER.debug(f"running task: {task.basename}")
|
|
92
|
+
|
|
93
|
+
task.exec_task(inj=ctx.obj.di)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
cli()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from dothat.initialization import TemplateGenerator
|
|
2
|
+
from dothat.models.actions import InteractivePythonAction
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InitializeDefaultProject(InteractivePythonAction):
|
|
6
|
+
def impl(self, *, force: bool, **_) -> None:
|
|
7
|
+
templategenerator = TemplateGenerator()
|
|
8
|
+
templategenerator.generate_project_structure(force=force)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from dothat.default_tasks import _actions, _params
|
|
4
|
+
from dothat.task_builder import Task, TaskBuilderImpl
|
|
5
|
+
from dothat.task_generator import TaskGeneratorImpl
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InitializeDefaultProject(TaskBuilderImpl):
|
|
9
|
+
_basename = "init"
|
|
10
|
+
|
|
11
|
+
def apply(self):
|
|
12
|
+
return self.with_actions((_actions.InitializeDefaultProject,)).with_params((_params.force(),))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CiTaskGenerator(TaskGeneratorImpl):
|
|
16
|
+
task_init = InitializeDefaultProject
|
|
17
|
+
|
|
18
|
+
def load_tasks(self) -> Iterator[Task]:
|
|
19
|
+
yield from super().load_tasks()
|
dothat/di.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import injector
|
|
4
|
+
|
|
5
|
+
from dothat.models.params import Params
|
|
6
|
+
from dothat.project_config import ProjectConfig
|
|
7
|
+
from dothat.registry import TaskRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigPath:
|
|
11
|
+
def __init__(self, path: pathlib.Path) -> None:
|
|
12
|
+
self.path = path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Module(injector.Module):
|
|
16
|
+
def __init__(self, config_path: pathlib.Path = pathlib.Path("environment.toml")) -> None:
|
|
17
|
+
self.config_path = config_path
|
|
18
|
+
|
|
19
|
+
def configure(self, binder: injector.Binder) -> None:
|
|
20
|
+
binder.bind(ConfigPath, to=ConfigPath(self.config_path))
|
|
21
|
+
binder.bind(TaskRegistry, scope=injector.singleton)
|
|
22
|
+
binder.bind(Params, scope=injector.singleton)
|
|
23
|
+
|
|
24
|
+
@injector.singleton
|
|
25
|
+
@injector.provider
|
|
26
|
+
def provide_config(self, cfg_path: ConfigPath) -> ProjectConfig:
|
|
27
|
+
return ProjectConfig.from_file(cfg_path.path)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import shutil
|
|
3
|
+
from importlib.resources import as_file, files
|
|
4
|
+
|
|
5
|
+
from dothat.logging import logger as LOGGER
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TemplateGenerator:
|
|
9
|
+
def generate_project_structure(
|
|
10
|
+
self,
|
|
11
|
+
destination: pathlib.Path = pathlib.Path("."),
|
|
12
|
+
force: bool = False,
|
|
13
|
+
) -> None:
|
|
14
|
+
template = files("dothat.initialization") / "templates"
|
|
15
|
+
|
|
16
|
+
LOGGER.info("Copying project template from {}", template)
|
|
17
|
+
|
|
18
|
+
with as_file(template) as template_path:
|
|
19
|
+
self._copy_template(
|
|
20
|
+
template_path,
|
|
21
|
+
destination,
|
|
22
|
+
force=force,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _copy_template(
|
|
27
|
+
source: pathlib.Path,
|
|
28
|
+
destination: pathlib.Path,
|
|
29
|
+
*,
|
|
30
|
+
force: bool,
|
|
31
|
+
) -> None:
|
|
32
|
+
for source_path in source.rglob("*"):
|
|
33
|
+
relative_path = source_path.relative_to(source)
|
|
34
|
+
destination_path = destination / relative_path
|
|
35
|
+
|
|
36
|
+
if source_path.is_dir():
|
|
37
|
+
destination_path.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
if destination_path.exists() and not force:
|
|
41
|
+
LOGGER.debug("Skipping existing file: {}", destination_path)
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
shutil.copy2(source_path, destination_path)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from dothat import InteractivePythonAction, InteractiveShlex
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SetupSsh(InteractivePythonAction):
|
|
8
|
+
def impl(self, **_):
|
|
9
|
+
ssh_private_key = "123"
|
|
10
|
+
|
|
11
|
+
ssh_dir = pathlib.Path("/root/.ssh")
|
|
12
|
+
ssh_dir.mkdir(exist_ok=True)
|
|
13
|
+
ssh_dir.chmod(0o600)
|
|
14
|
+
|
|
15
|
+
private_key_file = ssh_dir / "id_ci"
|
|
16
|
+
private_key_file.write_text(ssh_private_key)
|
|
17
|
+
private_key_file.chmod(0o600)
|
|
18
|
+
|
|
19
|
+
ssh_config_file = ssh_dir / "config"
|
|
20
|
+
ssh_config_data = [
|
|
21
|
+
"Host gl.pivlab.dev",
|
|
22
|
+
" IdentityFile /root/.ssh/id_ci",
|
|
23
|
+
" IdentitiesOnly yes",
|
|
24
|
+
" StrictHostKeyChecking no",
|
|
25
|
+
" UserKnownHostsFile /dev/null",
|
|
26
|
+
]
|
|
27
|
+
ssh_config_file.write_text("\n".join(ssh_config_data))
|
|
28
|
+
ssh_config_file.chmod(0o644)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SetupGitInsteadOf(InteractiveShlex):
|
|
32
|
+
def impl(self, **_: typing.Any) -> list[list[str]]:
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
["git", "config", "--global", "user.email", "ci@pivlab.space"],
|
|
36
|
+
["git", "config", "--global", "user.name", "CI"],
|
|
37
|
+
["git", "config", "--global", "init.defaultBranch", "master"],
|
|
38
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dothat import Param
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def flavor() -> Param:
|
|
5
|
+
return Param(
|
|
6
|
+
name="flavor",
|
|
7
|
+
long="flavor",
|
|
8
|
+
default="release",
|
|
9
|
+
type=str,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def insecure() -> Param:
|
|
14
|
+
return Param(
|
|
15
|
+
name="insecure",
|
|
16
|
+
long="insecure",
|
|
17
|
+
default=False,
|
|
18
|
+
type=bool,
|
|
19
|
+
inverse="secure",
|
|
20
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from _tasks import _actions, _params
|
|
4
|
+
from _tasks._consts import Tasks
|
|
5
|
+
from dothat.task_builder import Task, TaskBuilderImpl
|
|
6
|
+
from dothat.task_generator import TaskGeneratorImpl
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestBaseTask(TaskBuilderImpl):
|
|
10
|
+
_basename = Tasks.test_task
|
|
11
|
+
|
|
12
|
+
def apply(self):
|
|
13
|
+
self.model.doc = "test task"
|
|
14
|
+
self.with_actions((_actions.SetupSsh,))
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskGenerator(TaskGeneratorImpl):
|
|
19
|
+
task_generate_docs = TestBaseTask
|
|
20
|
+
|
|
21
|
+
def load_tasks(self) -> Iterator[Task]:
|
|
22
|
+
yield from super().load_tasks()
|
|
23
|
+
|
|
24
|
+
targets = self._config.extra.get("targets", [])
|
|
25
|
+
|
|
26
|
+
for target in targets:
|
|
27
|
+
yield (
|
|
28
|
+
self.builder(TestBaseTask)
|
|
29
|
+
.apply()
|
|
30
|
+
.with_name(target)
|
|
31
|
+
.with_params(
|
|
32
|
+
(
|
|
33
|
+
self._params.ci(),
|
|
34
|
+
_params.flavor(),
|
|
35
|
+
_params.insecure(),
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
.build()
|
|
39
|
+
)
|
dothat/logging.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_logger(debug: bool = False) -> None:
|
|
7
|
+
logger.remove()
|
|
8
|
+
|
|
9
|
+
level = "DEBUG" if debug else "INFO"
|
|
10
|
+
|
|
11
|
+
logger.add(
|
|
12
|
+
sys.stdout,
|
|
13
|
+
level=level,
|
|
14
|
+
colorize=True,
|
|
15
|
+
format=(
|
|
16
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
17
|
+
"<level>{level: <8}</level> | "
|
|
18
|
+
"<cyan>{name}</cyan>:"
|
|
19
|
+
"<cyan>{function}</cyan>:"
|
|
20
|
+
"<cyan>{line}</cyan> - "
|
|
21
|
+
"<level>{message}</level>"
|
|
22
|
+
),
|
|
23
|
+
backtrace=True,
|
|
24
|
+
diagnose=debug,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
setup_logger(debug=True)
|
|
29
|
+
|
|
30
|
+
__all__ = ["logger"]
|
dothat/models/actions.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import shlex
|
|
3
|
+
import subprocess
|
|
4
|
+
import typing
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
import injector
|
|
9
|
+
|
|
10
|
+
from dothat.logging import logger as LOGGER
|
|
11
|
+
from dothat.project_config import ProjectConfig
|
|
12
|
+
|
|
13
|
+
ActionResult = typing.Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@injector.inject
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class ActionBuilderImpl(ABC):
|
|
19
|
+
_di: injector.Injector
|
|
20
|
+
_config: ProjectConfig
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def impl(self, **kwargs: typing.Any) -> ActionResult: ...
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def execute(self, **kwargs: typing.Any): ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InteractivePythonAction(ActionBuilderImpl):
|
|
30
|
+
@typing.override
|
|
31
|
+
def impl(self, **kwargs: typing.Any) -> None:
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
@typing.override
|
|
35
|
+
def execute(self, **kwargs: typing.Any) -> None:
|
|
36
|
+
LOGGER.debug(f"running InteractivePythonAction action: {self.__class__.__name__}")
|
|
37
|
+
self.impl(**kwargs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InteractiveShlex(ActionBuilderImpl):
|
|
41
|
+
@typing.override
|
|
42
|
+
def impl(self, **kwargs: typing.Any) -> Iterable[list[str]]:
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
@typing.override
|
|
46
|
+
def execute(self, **kwargs: typing.Any) -> None:
|
|
47
|
+
LOGGER.debug(f"running InteractiveShlex action: {self.__class__.__name__}")
|
|
48
|
+
|
|
49
|
+
commands = [shlex.join(cmd) for cmd in self.impl(**kwargs)]
|
|
50
|
+
|
|
51
|
+
script = " && ".join(commands)
|
|
52
|
+
|
|
53
|
+
LOGGER.info("running: {}", script)
|
|
54
|
+
subprocess.run(script, shell=True, check=True)
|
dothat/models/params.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import injector
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
from dothat.project_config import ProjectConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Param(pydantic.BaseModel):
|
|
11
|
+
name: str
|
|
12
|
+
default: typing.Any = None
|
|
13
|
+
long: str
|
|
14
|
+
|
|
15
|
+
short: typing.Optional[str] = ""
|
|
16
|
+
type: typing.Callable[[typing.Any], typing.Any] = lambda x: x
|
|
17
|
+
help: typing.Optional[str] = None
|
|
18
|
+
env_var: typing.Optional[str] = None
|
|
19
|
+
inverse: typing.Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@injector.inject
|
|
23
|
+
@dataclasses.dataclass
|
|
24
|
+
class Params:
|
|
25
|
+
config: ProjectConfig
|
|
26
|
+
|
|
27
|
+
def ci(self):
|
|
28
|
+
return Param(
|
|
29
|
+
name="ci",
|
|
30
|
+
long="ci",
|
|
31
|
+
type=bool,
|
|
32
|
+
default=False,
|
|
33
|
+
env_var="CI",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def from_config(self, key: str, **kwargs):
|
|
37
|
+
return Param(
|
|
38
|
+
name=f"extra__{key}",
|
|
39
|
+
long=key.replace("_", "-"),
|
|
40
|
+
default=(self.config.extra.model_extra or {}).get(key),
|
|
41
|
+
**kwargs,
|
|
42
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import tomllib
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from dothat.logging import logger as LOGGER
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class System(BaseModel):
|
|
11
|
+
tasks_module: typing.Optional[str] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Extra(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="allow")
|
|
16
|
+
|
|
17
|
+
system: System = Field(default_factory=System)
|
|
18
|
+
|
|
19
|
+
def __getitem__(self, key: str) -> typing.Any:
|
|
20
|
+
|
|
21
|
+
if self.model_extra is None:
|
|
22
|
+
raise KeyError(key)
|
|
23
|
+
|
|
24
|
+
return self.model_extra[key]
|
|
25
|
+
|
|
26
|
+
def get(self, key: str, default: typing.Any = None) -> typing.Any:
|
|
27
|
+
return (self.model_extra or {}).get(key, default)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProjectConfig(BaseModel):
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_file(
|
|
33
|
+
cls,
|
|
34
|
+
path: pathlib.Path = pathlib.Path("environment.toml"),
|
|
35
|
+
) -> "ProjectConfig":
|
|
36
|
+
if not path.exists():
|
|
37
|
+
LOGGER.debug("ProjectConfig is not provided: file is not exists")
|
|
38
|
+
data = {}
|
|
39
|
+
else:
|
|
40
|
+
data = tomllib.loads(path.read_text())
|
|
41
|
+
|
|
42
|
+
_cfg = cls.model_validate(data)
|
|
43
|
+
|
|
44
|
+
return _cfg
|
|
45
|
+
|
|
46
|
+
extra: Extra = Field(default_factory=Extra)
|
dothat/py.typed
ADDED
|
File without changes
|
dothat/registry.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import injector
|
|
6
|
+
|
|
7
|
+
from dothat.default_tasks import CiTaskGenerator
|
|
8
|
+
from dothat.logging import logger as LOGGER
|
|
9
|
+
from dothat.project_config import ProjectConfig
|
|
10
|
+
from dothat.task_builder import Task
|
|
11
|
+
from dothat.task_generator import TaskGenerator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskRegistry:
|
|
15
|
+
@injector.inject
|
|
16
|
+
def __init__(self, config: ProjectConfig, di: injector.Injector) -> None:
|
|
17
|
+
self._di = di
|
|
18
|
+
self._config = config
|
|
19
|
+
|
|
20
|
+
self._tasks: dict[str, Task] | None = None
|
|
21
|
+
|
|
22
|
+
def _load_module(self, module: str):
|
|
23
|
+
|
|
24
|
+
module_path = pathlib.Path(module)
|
|
25
|
+
|
|
26
|
+
LOGGER.debug(f"Tasks loaded from module: {module_path}")
|
|
27
|
+
|
|
28
|
+
if module_path.is_file():
|
|
29
|
+
module_name = module_path.stem
|
|
30
|
+
|
|
31
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
32
|
+
if spec is None or spec.loader is None:
|
|
33
|
+
raise ImportError(f"Could not load module from {module_path}")
|
|
34
|
+
|
|
35
|
+
module_obj = importlib.util.module_from_spec(spec)
|
|
36
|
+
sys.modules[module_name] = module_obj
|
|
37
|
+
spec.loader.exec_module(module_obj)
|
|
38
|
+
return module_obj
|
|
39
|
+
|
|
40
|
+
sys.path.insert(0, str(module_path.parent))
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
module_obj = __import__(module, fromlist=["TaskGenerator"])
|
|
44
|
+
finally:
|
|
45
|
+
sys.path.remove(str(module_path.parent))
|
|
46
|
+
|
|
47
|
+
return module_obj
|
|
48
|
+
|
|
49
|
+
def _load_tasks_from_generator(self, generator: TaskGenerator) -> dict[str, Task]:
|
|
50
|
+
registry: dict[str, Task] = {}
|
|
51
|
+
|
|
52
|
+
for task in generator.load_tasks():
|
|
53
|
+
if not task.basename:
|
|
54
|
+
raise ValueError("Task basename is not set.")
|
|
55
|
+
taskname = task.basename if not task.name else ":".join([task.basename, task.name])
|
|
56
|
+
registry[taskname] = task
|
|
57
|
+
|
|
58
|
+
return registry
|
|
59
|
+
|
|
60
|
+
def load_tasks_from_module(self, module: str | None) -> dict[str, Task]:
|
|
61
|
+
if not module:
|
|
62
|
+
LOGGER.warning("module with tasks not found")
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
module_obj = self._load_module(module)
|
|
66
|
+
|
|
67
|
+
generator: TaskGenerator = self._di.create_object(module_obj.TaskGenerator)
|
|
68
|
+
|
|
69
|
+
registry: dict[str, Task] = self._load_tasks_from_generator(generator)
|
|
70
|
+
|
|
71
|
+
return registry
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def tasks(self) -> dict[str, Task]:
|
|
75
|
+
if self._tasks is None:
|
|
76
|
+
self._tasks = {
|
|
77
|
+
**self.load_tasks_from_module(self._config.extra.system.tasks_module),
|
|
78
|
+
**self._load_tasks_from_generator(self._di.create_object(CiTaskGenerator)),
|
|
79
|
+
}
|
|
80
|
+
return self._tasks
|
dothat/task_builder.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import os
|
|
3
|
+
import typing
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import injector
|
|
8
|
+
import pydantic
|
|
9
|
+
|
|
10
|
+
from dothat.models.actions import ActionBuilderImpl
|
|
11
|
+
from dothat.models.params import Param, Params
|
|
12
|
+
from dothat.project_config import ProjectConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Task(pydantic.BaseModel):
|
|
16
|
+
basename: typing.Optional[str] = None
|
|
17
|
+
|
|
18
|
+
name: typing.Optional[str] = None
|
|
19
|
+
|
|
20
|
+
doc: str = ""
|
|
21
|
+
|
|
22
|
+
actions: typing.Optional[list[type["ActionBuilderImpl"]]] = None
|
|
23
|
+
|
|
24
|
+
parsed_params: dict[str, typing.Any] = {}
|
|
25
|
+
|
|
26
|
+
params: list[Param] = pydantic.Field(default_factory=lambda: [])
|
|
27
|
+
|
|
28
|
+
def parse_task_params(self, args: list[str], di: injector.Injector):
|
|
29
|
+
_config = di.get(ProjectConfig)
|
|
30
|
+
|
|
31
|
+
if _config.extra.model_extra:
|
|
32
|
+
for key, val in _config.extra.model_extra.items():
|
|
33
|
+
self.parsed_params[f"extra__{key}"] = val
|
|
34
|
+
|
|
35
|
+
args_iter = iter(args)
|
|
36
|
+
|
|
37
|
+
for arg in args_iter:
|
|
38
|
+
if arg.startswith("--"):
|
|
39
|
+
key = arg.removeprefix("--")
|
|
40
|
+
param = next(
|
|
41
|
+
(p for p in self.params if key in [p.long, p.inverse]),
|
|
42
|
+
None,
|
|
43
|
+
)
|
|
44
|
+
elif arg.startswith("-"):
|
|
45
|
+
key = arg.removeprefix("-")
|
|
46
|
+
param = next(
|
|
47
|
+
(p for p in self.params if key in [p.short, p.inverse]),
|
|
48
|
+
None,
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if not param:
|
|
54
|
+
raise ValueError(f"Unknown parameter: --{key}")
|
|
55
|
+
|
|
56
|
+
if param.type is bool:
|
|
57
|
+
self.parsed_params[param.name] = True if key == param.long or key == param.short else False
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
value = next(args_iter)
|
|
62
|
+
except StopIteration as err:
|
|
63
|
+
raise StopIteration from err
|
|
64
|
+
|
|
65
|
+
self.parsed_params[param.name] = param.type(value)
|
|
66
|
+
|
|
67
|
+
def exec_task(self, inj: injector.Injector):
|
|
68
|
+
if not self.actions:
|
|
69
|
+
raise RuntimeError("not found actions to execute.")
|
|
70
|
+
for action_cls in self.actions:
|
|
71
|
+
action = inj.create_object(action_cls)
|
|
72
|
+
action.execute(**self.parsed_params)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TaskBulder:
|
|
76
|
+
def build(self) -> Task:
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@injector.inject
|
|
81
|
+
@dataclasses.dataclass
|
|
82
|
+
class TaskBuilderImpl(TaskBulder):
|
|
83
|
+
_basename: typing.ClassVar[str | Enum | None] = None
|
|
84
|
+
|
|
85
|
+
_params: Params
|
|
86
|
+
_config: ProjectConfig
|
|
87
|
+
|
|
88
|
+
model: Task = dataclasses.field(init=False, default_factory=Task)
|
|
89
|
+
|
|
90
|
+
def __post_init__(self):
|
|
91
|
+
"extension point for childs"
|
|
92
|
+
|
|
93
|
+
def apply(self):
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def build(self) -> Task:
|
|
97
|
+
if not self._basename:
|
|
98
|
+
raise ValueError("task name not set.")
|
|
99
|
+
|
|
100
|
+
if not self.model.doc:
|
|
101
|
+
self.model.doc = ".".join(
|
|
102
|
+
[
|
|
103
|
+
self.__class__.__module__,
|
|
104
|
+
self.__class__.__name__,
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if isinstance(self._basename, str):
|
|
109
|
+
self.model.basename = self._basename
|
|
110
|
+
else:
|
|
111
|
+
self.model.basename = self._basename.value
|
|
112
|
+
|
|
113
|
+
return self.model.model_copy(deep=True)
|
|
114
|
+
|
|
115
|
+
def with_actions(self, actions: Iterable[type[ActionBuilderImpl]]):
|
|
116
|
+
self.model.actions = list(actions)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def with_params(self: typing.Self, params: Iterable[Param]) -> typing.Self:
|
|
120
|
+
self.model.params = list(params)
|
|
121
|
+
|
|
122
|
+
params_dict = {}
|
|
123
|
+
for param in params:
|
|
124
|
+
params_dict[param.name] = os.getenv(param.env_var, param.default) if param.env_var else param.default
|
|
125
|
+
self.model.parsed_params = params_dict
|
|
126
|
+
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def replace_param(self, param_name: str, param: str, default: str):
|
|
130
|
+
self.model.parsed_params[param_name] = param or default
|
|
131
|
+
|
|
132
|
+
def with_name(self, name: str):
|
|
133
|
+
self.model.name = name
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def with_doc(self, doc: str):
|
|
137
|
+
self.model.doc = doc
|
|
138
|
+
return self
|
dothat/task_generator.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
|
|
4
|
+
import injector
|
|
5
|
+
|
|
6
|
+
from dothat.models.params import Params
|
|
7
|
+
from dothat.project_config import ProjectConfig
|
|
8
|
+
from dothat.task_builder import Task, TaskBuilderImpl
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskGenerator:
|
|
12
|
+
def load_tasks(self) -> Iterator[Task]:
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@injector.inject
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class TaskGeneratorImpl(TaskGenerator):
|
|
19
|
+
_config: ProjectConfig
|
|
20
|
+
_di: injector.Injector
|
|
21
|
+
_params: Params
|
|
22
|
+
|
|
23
|
+
def _auto_load_tasks(self) -> Iterator[Task]:
|
|
24
|
+
for name, cls in vars(type(self)).items():
|
|
25
|
+
if name.startswith("task_") and isinstance(cls, type) and issubclass(cls, TaskBuilderImpl):
|
|
26
|
+
yield self._di.create_object(cls).apply().build()
|
|
27
|
+
|
|
28
|
+
def builder(self, task_class: type[TaskBuilderImpl]) -> TaskBuilderImpl:
|
|
29
|
+
return self._di.create_object(task_class)
|
|
30
|
+
|
|
31
|
+
def load_tasks(self) -> Iterator[Task]:
|
|
32
|
+
yield from self._auto_load_tasks()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from dothat.initialization import TemplateGenerator
|
|
2
|
+
from dothat.models.actions import InteractivePythonAction
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InitializeDefaultProject(InteractivePythonAction):
|
|
6
|
+
def impl(self, *, force: bool, **_) -> None:
|
|
7
|
+
templategenerator = TemplateGenerator()
|
|
8
|
+
templategenerator.generate_project_structure(force=force)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from dothat.default_tasks import _actions, _params
|
|
4
|
+
from dothat.task_builder import Task, TaskBuilderImpl
|
|
5
|
+
from dothat.task_generator import TaskGeneratorImpl
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InitializeDefaultProject(TaskBuilderImpl):
|
|
9
|
+
_basename = "init"
|
|
10
|
+
|
|
11
|
+
def apply(self):
|
|
12
|
+
return self.with_actions((_actions.InitializeDefaultProject,)).with_params((_params.force(),))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CiTaskGenerator(TaskGeneratorImpl):
|
|
16
|
+
task_init = InitializeDefaultProject
|
|
17
|
+
|
|
18
|
+
def load_tasks(self) -> Iterator[Task]:
|
|
19
|
+
yield from super().load_tasks()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dothat
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: framework for python tasks and actions management
|
|
5
|
+
Author: PivLab Dev
|
|
6
|
+
Author-email: Arsenii Nikulin <a.nikulin@pivlab.dev>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: <4.0,>=3.11
|
|
9
|
+
Requires-Dist: click<9.0.0,>=8.4.1
|
|
10
|
+
Requires-Dist: gitpython<4.0.0,>=3.1.57
|
|
11
|
+
Requires-Dist: injector>=0.24.0
|
|
12
|
+
Requires-Dist: loguru<1.0.0,>=0.7.3
|
|
13
|
+
Requires-Dist: pydantic>=2.13.5
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
dothat/VERSION,sha256=kCdyz2boWiSkwz3wg3LS5QBAw2Xv-U-KfPDjGD_hFA8,7
|
|
2
|
+
dothat/__init__.py,sha256=WEq5tua40pi2jkROw7gdy6wfCuUwbkwaTdCakzoDcJc,220
|
|
3
|
+
dothat/cli.py,sha256=JiFVlL9kXuT5BWzyURhG3N5ylRtStga7k_sRG8yXz04,2470
|
|
4
|
+
dothat/di.py,sha256=Bc5XhQnmwhAezBNRA1T048fIlvQLGSY0mLxAb36kiwk,834
|
|
5
|
+
dothat/logging.py,sha256=TB-wD9c-9BE68TjKWIsqAD-t0_5tLNJVGIfh5N2uBWQ,614
|
|
6
|
+
dothat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
dothat/registry.py,sha256=CRB8xwkJEEmr0OmvRxmt3k83HhnSff5MQj_X1XtwUeY,2572
|
|
8
|
+
dothat/task_builder.py,sha256=ocCn06IUHtCgx093HEZDIGMUdMrpvsULL3lXe4V0LR8,3986
|
|
9
|
+
dothat/task_generator.py,sha256=uXD8mEuh9W7H2V8DJqO2uZRjwOt3xWoU8YVU9QKgrps,961
|
|
10
|
+
dothat/default_tasks/__init__.py,sha256=6P4Tc1g2VfGMgda0Mnp8XW3rnNHGPZFkqF0Zk1pVYKU,88
|
|
11
|
+
dothat/default_tasks/_actions.py,sha256=FsgiPLgqsDPjmSa21SpFp83cahyhF8lAEXTfyTyHcQA,332
|
|
12
|
+
dothat/default_tasks/_params.py,sha256=5HC5tcUMuL8MfIhhqsVy7t1ywuJHtQUbJEqXhP2oF8w,192
|
|
13
|
+
dothat/default_tasks/_tasks.py,sha256=OJNB5ETwgczz-RAmTPs2IoKSpuY3-BFRwnP3qdRDiyQ,564
|
|
14
|
+
dothat/initialization/__init__.py,sha256=pLi5z1N7HvNBHz3ALgeoni7Ygc2vProVAKxQN-HU3LU,1352
|
|
15
|
+
dothat/initialization/templates/_tasks/__init__.py,sha256=CmrPc4x2IIMOMf40IaOvRasn_XNv-9-Z0f7XaEssj5s,70
|
|
16
|
+
dothat/initialization/templates/_tasks/_actions.py,sha256=oD7IXDXYUhOgllEH-gFRugQtpdi3nUBZLxjfErCecaQ,1179
|
|
17
|
+
dothat/initialization/templates/_tasks/_consts.py,sha256=OkA0lWfASFIy_zGMz7B4C3e8pbjwI1o9XICD7xWIH8M,71
|
|
18
|
+
dothat/initialization/templates/_tasks/_params.py,sha256=6wdaki-Qf4W00zptnp4MiH5KLby4M7pDzMISTu1qeOE,334
|
|
19
|
+
dothat/initialization/templates/_tasks/_tasks.py,sha256=F2wg0Msq_ttR98UIneyJa5Yyx94WKlpE4HHtly-j0dc,1049
|
|
20
|
+
dothat/models/__init__.py,sha256=WEq5tua40pi2jkROw7gdy6wfCuUwbkwaTdCakzoDcJc,220
|
|
21
|
+
dothat/models/actions.py,sha256=HHPSyZ7hDBpDLCHHTLk1FM75L8DLzPrYctBegpB8LsU,1436
|
|
22
|
+
dothat/models/params.py,sha256=QEpPIF55Um7kT-rCZTIZEGdrklqB96LzeSosxZ911Hc,932
|
|
23
|
+
dothat/project_config/__init__.py,sha256=SKOoOGwVdguF-qMpjt78zj2vKuJ2Z9cgp_5jbMNhang,1104
|
|
24
|
+
dothat/tasks_core/default_tasks/__init__.py,sha256=hgIPX6gdJ8hK7-r_A_zVL3E7uywXiX6DqCc2PNi3QvU,99
|
|
25
|
+
dothat/tasks_core/default_tasks/_actions.py,sha256=FsgiPLgqsDPjmSa21SpFp83cahyhF8lAEXTfyTyHcQA,332
|
|
26
|
+
dothat/tasks_core/default_tasks/_params.py,sha256=5HC5tcUMuL8MfIhhqsVy7t1ywuJHtQUbJEqXhP2oF8w,192
|
|
27
|
+
dothat/tasks_core/default_tasks/_tasks.py,sha256=OJNB5ETwgczz-RAmTPs2IoKSpuY3-BFRwnP3qdRDiyQ,564
|
|
28
|
+
dothat-1.0.0.dist-info/METADATA,sha256=YNasljhwwo4Wh6AEDK7wTSRn2AtYN2ndyrgUP18fAog,409
|
|
29
|
+
dothat-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
30
|
+
dothat-1.0.0.dist-info/entry_points.txt,sha256=W0NBUdOfdU2VlnszHrvxUG_Zb1zYgF5iZxnZEy_a1Z8,42
|
|
31
|
+
dothat-1.0.0.dist-info/RECORD,,
|