rats-apps 0.1.2.dev14__py3-none-any.whl → 0.1.3__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.
- rats/annotations/__init__.py +25 -0
- rats/annotations/_functions.py +145 -0
- rats/annotations/py.typed +0 -0
- rats/apps/__init__.py +33 -10
- rats/apps/_annotations.py +46 -136
- rats/apps/_composite_container.py +3 -1
- rats/apps/_container.py +104 -8
- rats/apps/_executables.py +34 -0
- rats/apps/_ids.py +3 -7
- rats/apps/_plugin_container.py +17 -2
- rats/apps/_plugins.py +23 -0
- rats/apps/_runtimes.py +41 -0
- rats/apps/_scoping.py +6 -2
- rats/apps/_simple_apps.py +163 -0
- rats/cli/__init__.py +22 -0
- rats/cli/_annotations.py +40 -0
- rats/cli/_click.py +38 -0
- rats/cli/_executable.py +23 -0
- rats/cli/_plugin.py +68 -0
- rats/cli/_plugins.py +81 -0
- rats/cli/py.typed +0 -0
- rats/logs/__init__.py +8 -0
- rats/logs/_plugin.py +63 -0
- {rats_apps-0.1.2.dev14.dist-info → rats_apps-0.1.3.dist-info}/METADATA +3 -1
- rats_apps-0.1.3.dist-info/RECORD +29 -0
- rats_apps-0.1.3.dist-info/entry_points.txt +4 -0
- rats_apps-0.1.2.dev14.dist-info/RECORD +0 -13
- rats_apps-0.1.2.dev14.dist-info/entry_points.txt +0 -3
- {rats_apps-0.1.2.dev14.dist-info → rats_apps-0.1.3.dist-info}/WHEEL +0 -0
rats/apps/_plugin_container.py
CHANGED
@@ -7,12 +7,24 @@ from ._ids import ServiceId, T_ServiceType
|
|
7
7
|
|
8
8
|
|
9
9
|
class PluginContainers(Container):
|
10
|
+
"""
|
11
|
+
A container that loads plugins using importlib.metadata.entry_points.
|
12
|
+
|
13
|
+
When looking for groups, the container loads the specified entry_points and defers the lookups
|
14
|
+
to the plugins. Plugin containers are expected to be Callable[[Container], Container] objects,
|
15
|
+
where the input container is typically the root application container.
|
16
|
+
|
17
|
+
TODO: How do we better specify the API for plugins without relying on documentation?
|
18
|
+
"""
|
19
|
+
|
10
20
|
_app: Container
|
11
21
|
_group: str
|
22
|
+
_names: tuple[str, ...]
|
12
23
|
|
13
|
-
def __init__(self, app: Container, group: str) -> None:
|
24
|
+
def __init__(self, app: Container, group: str, *names: str) -> None:
|
14
25
|
self._app = app
|
15
26
|
self._group = group
|
27
|
+
self._names = names
|
16
28
|
|
17
29
|
def get_namespaced_group(
|
18
30
|
self,
|
@@ -25,4 +37,7 @@ class PluginContainers(Container):
|
|
25
37
|
@cache # noqa: B019
|
26
38
|
def _load_containers(self) -> Iterable[Container]:
|
27
39
|
entries = entry_points(group=self._group)
|
28
|
-
return tuple(entry.load()(self._app) for entry in entries)
|
40
|
+
return tuple(entry.load()(self._app) for entry in entries if self._is_enabled(entry.name))
|
41
|
+
|
42
|
+
def _is_enabled(self, plugin_name: str) -> bool:
|
43
|
+
return len(self._names) == 0 or plugin_name in self._names
|
rats/apps/_plugins.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import warnings
|
2
|
+
from collections.abc import Callable, Iterator
|
3
|
+
from typing import Generic, TypeVar
|
4
|
+
|
5
|
+
T_PluginType = TypeVar("T_PluginType")
|
6
|
+
|
7
|
+
|
8
|
+
class PluginRunner(Generic[T_PluginType]):
|
9
|
+
"""Client to apply a function to a list of plugins."""
|
10
|
+
|
11
|
+
_plugins: Iterator[T_PluginType]
|
12
|
+
|
13
|
+
def __init__(self, plugins: Iterator[T_PluginType]) -> None:
|
14
|
+
self._plugins = plugins
|
15
|
+
|
16
|
+
def apply(self, handler: Callable[[T_PluginType], None]) -> None:
|
17
|
+
warnings.warn(
|
18
|
+
"PluginRunner is deprecated. Use PluginContainer instances instead.",
|
19
|
+
DeprecationWarning,
|
20
|
+
stacklevel=2,
|
21
|
+
)
|
22
|
+
for plugin in self._plugins:
|
23
|
+
handler(plugin)
|
rats/apps/_runtimes.py
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
from abc import abstractmethod
|
2
|
+
from collections.abc import Callable
|
3
|
+
from typing import Protocol, final
|
4
|
+
|
5
|
+
from ._ids import ServiceId, T_ExecutableType
|
6
|
+
|
7
|
+
|
8
|
+
class Runtime(Protocol):
|
9
|
+
@abstractmethod
|
10
|
+
def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
|
11
|
+
"""Execute a list of executables sequentially."""
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
|
15
|
+
"""
|
16
|
+
Execute one or more groups of executables sequentially.
|
17
|
+
|
18
|
+
Although each group is expected to be executed sequentially, the groups themselves are not
|
19
|
+
executed in a deterministic order. Runtime implementations are free to execute groups in
|
20
|
+
parallel or in any order that is convenient.
|
21
|
+
"""
|
22
|
+
|
23
|
+
@abstractmethod
|
24
|
+
def execute_callable(self, *callables: Callable[[], None]) -> None:
|
25
|
+
"""
|
26
|
+
Execute provided callables by automatically turning them into apps.Executable objects.
|
27
|
+
|
28
|
+
The used ServiceId is determined by the Runtime implementation.
|
29
|
+
"""
|
30
|
+
|
31
|
+
|
32
|
+
@final
|
33
|
+
class NullRuntime(Runtime):
|
34
|
+
def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
|
35
|
+
raise NotImplementedError("NullRuntime does not support execution.")
|
36
|
+
|
37
|
+
def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
|
38
|
+
raise NotImplementedError("NullRuntime does not support execution.")
|
39
|
+
|
40
|
+
def execute_callable(self, *callables: Callable[[], None]) -> None:
|
41
|
+
raise NotImplementedError("NullRuntime does not support execution.")
|
rats/apps/_scoping.py
CHANGED
@@ -22,7 +22,7 @@ def autoscope(cls: type[T]) -> type[T]:
|
|
22
22
|
if not isinstance(result, ServiceId):
|
23
23
|
return result
|
24
24
|
|
25
|
-
return ServiceId[Any](
|
25
|
+
return ServiceId[Any](scope_service_name(cls.__module__, cls.__name__, result.name))
|
26
26
|
|
27
27
|
return cast(FunctionType, wrapper)
|
28
28
|
|
@@ -37,7 +37,11 @@ def autoscope(cls: type[T]) -> type[T]:
|
|
37
37
|
if not isinstance(non_ns, ServiceId):
|
38
38
|
continue
|
39
39
|
|
40
|
-
prop = ServiceId[Any](
|
40
|
+
prop = ServiceId[Any](scope_service_name(cls.__module__, cls.__name__, non_ns.name))
|
41
41
|
setattr(cls, prop_name, prop)
|
42
42
|
|
43
43
|
return cls
|
44
|
+
|
45
|
+
|
46
|
+
def scope_service_name(module_name: str, cls_name: str, name: str) -> str:
|
47
|
+
return f"{module_name}:{cls_name}[{name}]"
|
@@ -0,0 +1,163 @@
|
|
1
|
+
from collections.abc import Callable, Iterable, Iterator
|
2
|
+
from contextlib import contextmanager
|
3
|
+
from typing import final
|
4
|
+
|
5
|
+
from ._annotations import fallback_service, service
|
6
|
+
from ._composite_container import CompositeContainer
|
7
|
+
from ._container import Container, container
|
8
|
+
from ._executables import App, Executable
|
9
|
+
from ._ids import ServiceId
|
10
|
+
from ._plugin_container import PluginContainers
|
11
|
+
from ._runtimes import Runtime, T_ExecutableType
|
12
|
+
from ._scoping import autoscope
|
13
|
+
|
14
|
+
|
15
|
+
class ExecutableCallableContext(Executable):
|
16
|
+
"""
|
17
|
+
An executable that can be set dynamically with a callable.
|
18
|
+
|
19
|
+
We use this class to support the use of `rats.apps` with a plain callable, like is expected of
|
20
|
+
standard python scripts. We give this class a service id, and set the callable before using
|
21
|
+
`apps.Runtime` to execute the chosen service id.
|
22
|
+
"""
|
23
|
+
|
24
|
+
_exe_id: ServiceId[Executable]
|
25
|
+
_callables: list[Callable[[], None]]
|
26
|
+
|
27
|
+
def __init__(self, exe_id: ServiceId[Executable]) -> None:
|
28
|
+
self._exe_id = exe_id
|
29
|
+
self._callables = []
|
30
|
+
|
31
|
+
def execute(self) -> None:
|
32
|
+
if len(self._callables) == 0:
|
33
|
+
raise RuntimeError("No active executable found.")
|
34
|
+
|
35
|
+
self._callables[-1]()
|
36
|
+
|
37
|
+
@contextmanager
|
38
|
+
def open_callable(
|
39
|
+
self,
|
40
|
+
callable: Callable[[], None],
|
41
|
+
) -> Iterator[Executable]:
|
42
|
+
self._callables.append(callable)
|
43
|
+
try:
|
44
|
+
yield App(callable)
|
45
|
+
finally:
|
46
|
+
self._callables.pop()
|
47
|
+
|
48
|
+
|
49
|
+
@autoscope
|
50
|
+
class AppServices:
|
51
|
+
"""
|
52
|
+
Services used by simple apps that can generally be used anywhere.
|
53
|
+
|
54
|
+
Owners of applications can decide not to use or not to make these services available to plugin
|
55
|
+
authors.
|
56
|
+
"""
|
57
|
+
|
58
|
+
RUNTIME = ServiceId[Runtime]("app-runtime")
|
59
|
+
STANDARD_RUNTIME = ServiceId[Runtime]("standard-runtime")
|
60
|
+
CONTAINER = ServiceId[Container]("app-container")
|
61
|
+
CALLABLE_EXE_CTX = ServiceId[ExecutableCallableContext]("callable-exe-ctx")
|
62
|
+
CALLABLE_EXE = ServiceId[Executable]("callable-exe")
|
63
|
+
|
64
|
+
|
65
|
+
@final
|
66
|
+
class StandardRuntime(Runtime):
|
67
|
+
"""A simple runtime that executes sequentially and in a single thread."""
|
68
|
+
|
69
|
+
_app: Container
|
70
|
+
|
71
|
+
def __init__(self, app: Container) -> None:
|
72
|
+
self._app = app
|
73
|
+
|
74
|
+
def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
|
75
|
+
for exe_id in exe_ids:
|
76
|
+
self._app.get(exe_id).execute()
|
77
|
+
|
78
|
+
def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
|
79
|
+
for exe_group_id in exe_group_ids:
|
80
|
+
for exe in self._app.get_group(exe_group_id):
|
81
|
+
exe.execute()
|
82
|
+
|
83
|
+
def execute_callable(self, *callables: Callable[[], None]) -> None:
|
84
|
+
ctx: ExecutableCallableContext = self._app.get(AppServices.CALLABLE_EXE_CTX)
|
85
|
+
for cb in callables:
|
86
|
+
with ctx.open_callable(cb) as exe:
|
87
|
+
exe.execute()
|
88
|
+
|
89
|
+
|
90
|
+
@final
|
91
|
+
class SimpleApplication(Runtime, Container):
|
92
|
+
"""An application without anything fancy."""
|
93
|
+
|
94
|
+
_plugin_groups: Iterable[str]
|
95
|
+
_runtime_plugin: Callable[[Container], Container] | None
|
96
|
+
|
97
|
+
def __init__(
|
98
|
+
self,
|
99
|
+
*plugin_groups: str,
|
100
|
+
runtime_plugin: Callable[[Container], Container] | None = None,
|
101
|
+
) -> None:
|
102
|
+
self._plugin_groups = plugin_groups
|
103
|
+
self._runtime_plugin = runtime_plugin
|
104
|
+
|
105
|
+
def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
|
106
|
+
self._runtime().execute(*exe_ids)
|
107
|
+
|
108
|
+
def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
|
109
|
+
self._runtime().execute_group(*exe_group_ids)
|
110
|
+
|
111
|
+
def execute_callable(self, *callables: Callable[[], None]) -> None:
|
112
|
+
for cb in callables:
|
113
|
+
self._runtime().execute_callable(cb)
|
114
|
+
|
115
|
+
@fallback_service(AppServices.RUNTIME)
|
116
|
+
def _runtime(self) -> Runtime:
|
117
|
+
"""
|
118
|
+
The default runtime defers to AppServices.STANDARD_RUNTIME.
|
119
|
+
|
120
|
+
Define a non-fallback service to override this default implementation.
|
121
|
+
"""
|
122
|
+
return self.get(AppServices.STANDARD_RUNTIME)
|
123
|
+
|
124
|
+
@service(AppServices.STANDARD_RUNTIME)
|
125
|
+
def _std_runtime(self) -> Runtime:
|
126
|
+
"""
|
127
|
+
The standard locally executed runtime.
|
128
|
+
|
129
|
+
Regardless of the configured AppServices.RUNTIME provider, everyone has access to this
|
130
|
+
class for plugin development.
|
131
|
+
"""
|
132
|
+
return StandardRuntime(self)
|
133
|
+
|
134
|
+
@fallback_service(AppServices.CALLABLE_EXE)
|
135
|
+
def _callable_exe(self) -> Executable:
|
136
|
+
"""We use the callable exe ctx here, so we can treat it like any other app downstream."""
|
137
|
+
return self.get(AppServices.CALLABLE_EXE_CTX)
|
138
|
+
|
139
|
+
@fallback_service(AppServices.CALLABLE_EXE_CTX)
|
140
|
+
def _callable_exe_ctx(self) -> ExecutableCallableContext:
|
141
|
+
"""
|
142
|
+
The default executable context client for executing raw callables.
|
143
|
+
|
144
|
+
Define a non-fallback service to override this default implementation.
|
145
|
+
"""
|
146
|
+
return ExecutableCallableContext(AppServices.CALLABLE_EXE)
|
147
|
+
|
148
|
+
@fallback_service(AppServices.CONTAINER)
|
149
|
+
def _container(self) -> Container:
|
150
|
+
"""
|
151
|
+
The default container is the root application instance.
|
152
|
+
|
153
|
+
Define a non-fallback service to override this default implementation.
|
154
|
+
"""
|
155
|
+
return self
|
156
|
+
|
157
|
+
@container()
|
158
|
+
def _plugins(self) -> Container:
|
159
|
+
plugins: list[Container] = [PluginContainers(self, group) for group in self._plugin_groups]
|
160
|
+
if self._runtime_plugin:
|
161
|
+
plugins.append(self._runtime_plugin(self))
|
162
|
+
|
163
|
+
return CompositeContainer(*plugins)
|
rats/cli/__init__.py
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
"""Uses `rats.cli` to streamline the creation of CLI commands written with Click."""
|
2
|
+
|
3
|
+
from ._annotations import CommandId, command, group
|
4
|
+
from ._click import ClickCommandGroup, ClickCommandMapper
|
5
|
+
from ._executable import ClickExecutable
|
6
|
+
from ._plugin import PluginContainer, PluginServices
|
7
|
+
from ._plugins import AttachClickCommands, AttachClickGroup, ClickGroupPlugin, CommandContainer
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"CommandId",
|
11
|
+
"PluginContainer",
|
12
|
+
"command",
|
13
|
+
"group",
|
14
|
+
"PluginServices",
|
15
|
+
"ClickCommandMapper",
|
16
|
+
"ClickExecutable",
|
17
|
+
"ClickGroupPlugin",
|
18
|
+
"ClickCommandGroup",
|
19
|
+
"AttachClickCommands",
|
20
|
+
"AttachClickGroup",
|
21
|
+
"CommandContainer",
|
22
|
+
]
|
rats/cli/_annotations.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any, NamedTuple, TypeVar
|
5
|
+
|
6
|
+
from rats import annotations as anns
|
7
|
+
|
8
|
+
|
9
|
+
class CommandId(NamedTuple):
|
10
|
+
name: str
|
11
|
+
|
12
|
+
# does this api make it impossible to reference a given command that was auto generated?
|
13
|
+
@staticmethod
|
14
|
+
def auto() -> CommandId:
|
15
|
+
return CommandId(name=f"{__name__}:auto")
|
16
|
+
|
17
|
+
|
18
|
+
T = TypeVar("T", bound=Callable[[Any], Any])
|
19
|
+
|
20
|
+
|
21
|
+
def command(command_id: CommandId) -> Callable[[T], T]:
|
22
|
+
def decorator(fn: T) -> T:
|
23
|
+
if command_id == CommandId.auto():
|
24
|
+
return anns.annotation("commands", CommandId(fn.__name__.replace("_", "-")))(fn)
|
25
|
+
return anns.annotation("commands", command_id)(fn)
|
26
|
+
|
27
|
+
return decorator
|
28
|
+
|
29
|
+
|
30
|
+
def group(command_id: CommandId) -> Callable[[T], T]:
|
31
|
+
def decorator(fn: T) -> T:
|
32
|
+
if command_id == CommandId.auto():
|
33
|
+
return anns.annotation("command-groups", CommandId(fn.__name__.replace("_", "-")))(fn)
|
34
|
+
return anns.annotation("commands", command_id)(fn)
|
35
|
+
|
36
|
+
return decorator
|
37
|
+
|
38
|
+
|
39
|
+
def get_class_commands(cls: type) -> anns.AnnotationsContainer:
|
40
|
+
return anns.get_class_annotations(cls).with_namespace("commands")
|
rats/cli/_click.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
from collections.abc import Callable, Mapping
|
2
|
+
from typing import final
|
3
|
+
|
4
|
+
import click
|
5
|
+
|
6
|
+
|
7
|
+
class ClickCommandMapper:
|
8
|
+
_commands: Mapping[str, Callable[[], click.Command]]
|
9
|
+
|
10
|
+
def __init__(
|
11
|
+
self,
|
12
|
+
commands: Mapping[str, Callable[[], click.Command]],
|
13
|
+
) -> None:
|
14
|
+
self._commands = commands
|
15
|
+
|
16
|
+
def names(self) -> frozenset[str]:
|
17
|
+
return frozenset(self._commands.keys())
|
18
|
+
|
19
|
+
def get(self, name: str) -> click.Command:
|
20
|
+
if name not in self._commands:
|
21
|
+
raise ValueError(f"Command {name} not found")
|
22
|
+
|
23
|
+
return self._commands[name]()
|
24
|
+
|
25
|
+
|
26
|
+
@final
|
27
|
+
class ClickCommandGroup(click.Group):
|
28
|
+
_mapper: ClickCommandMapper
|
29
|
+
|
30
|
+
def __init__(self, name: str, mapper: ClickCommandMapper) -> None:
|
31
|
+
super().__init__(name=name)
|
32
|
+
self._mapper = mapper
|
33
|
+
|
34
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
35
|
+
return self._mapper.get(cmd_name)
|
36
|
+
|
37
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
38
|
+
return list(self._mapper.names())
|
rats/cli/_executable.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import click
|
2
|
+
|
3
|
+
from rats import apps
|
4
|
+
|
5
|
+
from ._plugins import ClickGroupPlugin
|
6
|
+
|
7
|
+
|
8
|
+
class ClickExecutable(apps.Executable):
|
9
|
+
_command: apps.ServiceProvider[click.Group]
|
10
|
+
_plugins: apps.PluginRunner[ClickGroupPlugin]
|
11
|
+
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
command: apps.ServiceProvider[click.Group],
|
15
|
+
plugins: apps.PluginRunner[ClickGroupPlugin],
|
16
|
+
) -> None:
|
17
|
+
self._command = command
|
18
|
+
self._plugins = plugins
|
19
|
+
|
20
|
+
def execute(self) -> None:
|
21
|
+
cmd = self._command()
|
22
|
+
self._plugins.apply(lambda plugin: plugin.on_group_open(cmd))
|
23
|
+
cmd()
|
rats/cli/_plugin.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
from typing import cast, final
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from rats import apps
|
6
|
+
|
7
|
+
|
8
|
+
@apps.autoscope
|
9
|
+
class _PluginEvents:
|
10
|
+
@staticmethod
|
11
|
+
def command_open(cmd_id: apps.ServiceId[apps.Executable]) -> apps.ServiceId[apps.Executable]:
|
12
|
+
return apps.ServiceId(f"command-open[{cmd_id.name}]")
|
13
|
+
|
14
|
+
@staticmethod
|
15
|
+
def command_execute(
|
16
|
+
cmd_id: apps.ServiceId[apps.Executable],
|
17
|
+
) -> apps.ServiceId[apps.Executable]:
|
18
|
+
return apps.ServiceId(f"command-execute[{cmd_id.name}]")
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def command_close(cmd_id: apps.ServiceId[apps.Executable]) -> apps.ServiceId[apps.Executable]:
|
22
|
+
return apps.ServiceId(f"command-close[{cmd_id.name}]")
|
23
|
+
|
24
|
+
|
25
|
+
@apps.autoscope
|
26
|
+
class PluginServices:
|
27
|
+
ROOT_COMMAND = apps.ServiceId[apps.Executable]("root-command")
|
28
|
+
EVENTS = _PluginEvents
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
def sub_command(
|
32
|
+
parent: apps.ServiceId[apps.Executable],
|
33
|
+
name: str,
|
34
|
+
) -> apps.ServiceId[apps.Executable]:
|
35
|
+
return apps.ServiceId(f"{parent.name}[{name}]")
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def click_command(cmd_id: apps.ServiceId[apps.Executable]) -> apps.ServiceId[click.Group]:
|
39
|
+
return cast(apps.ServiceId[click.Group], cmd_id)
|
40
|
+
|
41
|
+
|
42
|
+
@final
|
43
|
+
class PluginContainer(apps.Container):
|
44
|
+
_app: apps.Container
|
45
|
+
|
46
|
+
def __init__(self, app: apps.Container) -> None:
|
47
|
+
self._app = app
|
48
|
+
|
49
|
+
@apps.service(PluginServices.ROOT_COMMAND)
|
50
|
+
def _root_command(self) -> apps.Executable:
|
51
|
+
def run() -> None:
|
52
|
+
runtime = self._app.get(apps.AppServices.RUNTIME)
|
53
|
+
runtime.execute_group(
|
54
|
+
PluginServices.EVENTS.command_open(PluginServices.ROOT_COMMAND),
|
55
|
+
PluginServices.EVENTS.command_execute(PluginServices.ROOT_COMMAND),
|
56
|
+
PluginServices.EVENTS.command_close(PluginServices.ROOT_COMMAND),
|
57
|
+
)
|
58
|
+
|
59
|
+
return apps.App(run)
|
60
|
+
|
61
|
+
@apps.fallback_group(PluginServices.EVENTS.command_execute(PluginServices.ROOT_COMMAND))
|
62
|
+
def _default_command(self) -> apps.Executable:
|
63
|
+
group = self._app.get(PluginServices.click_command(PluginServices.ROOT_COMMAND))
|
64
|
+
return apps.App(lambda: group())
|
65
|
+
|
66
|
+
@apps.service(PluginServices.click_command(PluginServices.ROOT_COMMAND))
|
67
|
+
def _root_click_command(self) -> click.Group:
|
68
|
+
return click.Group("groot")
|
rats/cli/_plugins.py
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import abstractmethod
|
4
|
+
from collections.abc import Callable, Iterator
|
5
|
+
from functools import partial
|
6
|
+
from typing import Any, Protocol
|
7
|
+
|
8
|
+
import click
|
9
|
+
|
10
|
+
from rats import apps
|
11
|
+
|
12
|
+
from ._annotations import get_class_commands
|
13
|
+
|
14
|
+
|
15
|
+
class ClickGroupPlugin(Protocol):
|
16
|
+
@abstractmethod
|
17
|
+
def on_group_open(self, group: click.Group) -> None:
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class AttachClickCommands(ClickGroupPlugin):
|
22
|
+
"""When a group is opened, attach a set of commands to it."""
|
23
|
+
|
24
|
+
_commands: Iterator[click.Command]
|
25
|
+
|
26
|
+
def __init__(self, commands: Iterator[click.Command]) -> None:
|
27
|
+
self._commands = commands
|
28
|
+
|
29
|
+
def on_group_open(self, group: click.Group) -> None:
|
30
|
+
for command in self._commands:
|
31
|
+
group.add_command(command)
|
32
|
+
|
33
|
+
|
34
|
+
class AttachClickGroup(ClickGroupPlugin):
|
35
|
+
_group: apps.ServiceProvider[click.Group]
|
36
|
+
_plugins: apps.PluginRunner[ClickGroupPlugin]
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
group: apps.ServiceProvider[click.Group],
|
41
|
+
plugins: apps.PluginRunner[ClickGroupPlugin],
|
42
|
+
) -> None:
|
43
|
+
self._group = group
|
44
|
+
self._plugins = plugins
|
45
|
+
|
46
|
+
def on_group_open(self, group: click.Group) -> None:
|
47
|
+
cmd = self._group()
|
48
|
+
self._plugins.apply(lambda plugin: plugin.on_group_open(cmd))
|
49
|
+
group.add_command(cmd)
|
50
|
+
|
51
|
+
|
52
|
+
class CommandContainer(ClickGroupPlugin):
|
53
|
+
def on_group_open(self, group: click.Group) -> None:
|
54
|
+
def cb(_method: Callable[[Any], Any], *args: Any, **kwargs: Any) -> None:
|
55
|
+
"""
|
56
|
+
Callback handed to `click.Command`. Calls the method with matching name on this class.
|
57
|
+
|
58
|
+
When the command is decorated with `@click.params` and `@click.option`, `click` will
|
59
|
+
call this callback with the parameters in the order they were defined. This callback
|
60
|
+
then calls the method with the same name on this class, passing the parameters in
|
61
|
+
reverse order. This is because the method is defined with the parameters in the
|
62
|
+
reverse order to the decorator, so we need to reverse them again to get the correct
|
63
|
+
order.
|
64
|
+
"""
|
65
|
+
_method(*args, **kwargs)
|
66
|
+
|
67
|
+
commands = get_class_commands(type(self))
|
68
|
+
tates = commands.annotations
|
69
|
+
|
70
|
+
for tate in tates:
|
71
|
+
method = getattr(self, tate.name)
|
72
|
+
params = list(reversed(getattr(method, "__click_params__", [])))
|
73
|
+
for command in tate.groups:
|
74
|
+
group.add_command(
|
75
|
+
click.Command(
|
76
|
+
name=command.name,
|
77
|
+
callback=partial(cb, method),
|
78
|
+
short_help=method.__doc__,
|
79
|
+
params=params,
|
80
|
+
)
|
81
|
+
)
|
rats/cli/py.typed
ADDED
File without changes
|
rats/logs/__init__.py
ADDED
rats/logs/_plugin.py
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
import logging.config
|
2
|
+
|
3
|
+
from rats import apps
|
4
|
+
|
5
|
+
logger = logging.getLogger(__name__)
|
6
|
+
|
7
|
+
|
8
|
+
@apps.autoscope
|
9
|
+
class _PluginEvents:
|
10
|
+
CONFIGURE_LOGGING = apps.ServiceId[apps.Executable]("configure-logging")
|
11
|
+
|
12
|
+
|
13
|
+
@apps.autoscope
|
14
|
+
class PluginServices:
|
15
|
+
EVENTS = _PluginEvents
|
16
|
+
|
17
|
+
|
18
|
+
class PluginContainer(apps.Container):
|
19
|
+
_app: apps.Container
|
20
|
+
|
21
|
+
def __init__(self, app: apps.Container) -> None:
|
22
|
+
self._app = app
|
23
|
+
|
24
|
+
@apps.group(PluginServices.EVENTS.CONFIGURE_LOGGING)
|
25
|
+
def _configure_logging(self) -> apps.Executable:
|
26
|
+
# in the future, we can use this plugin to make logging easily configurable
|
27
|
+
return apps.App(
|
28
|
+
lambda: logging.config.dictConfig(
|
29
|
+
{
|
30
|
+
"version": 1,
|
31
|
+
"disable_existing_loggers": False,
|
32
|
+
"formatters": {
|
33
|
+
"colored": {
|
34
|
+
"()": "colorlog.ColoredFormatter",
|
35
|
+
"format": (
|
36
|
+
"%(log_color)s%(asctime)s %(levelname)-8s [%(name)s][%(lineno)d]: "
|
37
|
+
"%(message)s%(reset)s"
|
38
|
+
),
|
39
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
40
|
+
"log_colors": {
|
41
|
+
"DEBUG": "white",
|
42
|
+
"INFO": "green",
|
43
|
+
"WARNING": "yellow",
|
44
|
+
"ERROR": "red,",
|
45
|
+
"CRITICAL": "bold_red",
|
46
|
+
},
|
47
|
+
}
|
48
|
+
},
|
49
|
+
"handlers": {
|
50
|
+
"console": {
|
51
|
+
"class": "logging.StreamHandler",
|
52
|
+
"level": "DEBUG",
|
53
|
+
"formatter": "colored",
|
54
|
+
"stream": "ext://sys.stderr",
|
55
|
+
}
|
56
|
+
},
|
57
|
+
"loggers": {
|
58
|
+
"": {"level": "INFO", "handlers": ["console"]},
|
59
|
+
"azure": {"level": "WARNING", "handlers": ["console"]},
|
60
|
+
},
|
61
|
+
}
|
62
|
+
)
|
63
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: rats-apps
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.3
|
4
4
|
Summary: research analysis tools for building applications
|
5
5
|
Home-page: https://github.com/microsoft/rats/
|
6
6
|
License: MIT
|
@@ -11,6 +11,8 @@ Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
14
|
+
Requires-Dist: click
|
15
|
+
Requires-Dist: colorlog
|
14
16
|
Requires-Dist: typing_extensions
|
15
17
|
Project-URL: Documentation, https://microsoft.github.io/rats/
|
16
18
|
Project-URL: Repository, https://github.com/microsoft/rats/
|