rats-apps 0.1.3.dev20240812192218__py3-none-any.whl → 0.2.0.dev20240813092513__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/__main__.py +46 -0
- rats/annotations/_functions.py +6 -7
- rats/apps/__init__.py +4 -3
- rats/apps/__main__.py +79 -0
- rats/apps/_annotations.py +6 -6
- rats/apps/_container.py +10 -27
- rats/apps/_runtimes.py +12 -3
- rats/cli/__init__.py +6 -11
- rats/cli/__main__.py +64 -0
- rats/cli/_annotations.py +12 -11
- rats/cli/_app.py +29 -0
- rats/cli/_container.py +50 -0
- rats/cli/_plugin.py +1 -35
- rats/logs/py.typed +0 -0
- {rats_apps-0.1.3.dev20240812192218.dist-info → rats_apps-0.2.0.dev20240813092513.dist-info}/METADATA +1 -1
- rats_apps-0.2.0.dev20240813092513.dist-info/RECORD +32 -0
- rats/cli/_click.py +0 -38
- rats/cli/_executable.py +0 -23
- rats/cli/_plugins.py +0 -81
- rats_apps-0.1.3.dev20240812192218.dist-info/RECORD +0 -29
- {rats_apps-0.1.3.dev20240812192218.dist-info → rats_apps-0.2.0.dev20240813092513.dist-info}/WHEEL +0 -0
- {rats_apps-0.1.3.dev20240812192218.dist-info → rats_apps-0.2.0.dev20240813092513.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# type: ignore[reportUntypedFunctionDecorator]
|
2
|
+
"""..."""
|
3
|
+
|
4
|
+
from typing import NamedTuple
|
5
|
+
|
6
|
+
from rats import annotations
|
7
|
+
|
8
|
+
|
9
|
+
class FruitId(NamedTuple):
|
10
|
+
"""Any named tuple can be attached to functions."""
|
11
|
+
|
12
|
+
name: str
|
13
|
+
color: str
|
14
|
+
|
15
|
+
|
16
|
+
def fruit(fid: FruitId) -> annotations.DecoratorType:
|
17
|
+
"""A decorator that attached a fruit object to a function."""
|
18
|
+
return annotations.annotation("fruits", fid)
|
19
|
+
|
20
|
+
|
21
|
+
@fruit(FruitId("apple", "red"))
|
22
|
+
def some_function() -> None:
|
23
|
+
"""..."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
27
|
+
@fruit(FruitId("banana", "yellow"))
|
28
|
+
class SomeClass:
|
29
|
+
"""Class definitions can also be annotated."""
|
30
|
+
|
31
|
+
@fruit(FruitId("cherry", "red"))
|
32
|
+
def some_method(self) -> None:
|
33
|
+
"""Or class methods."""
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
def _example() -> None:
|
38
|
+
"""Let's define a couple objects and annotate them."""
|
39
|
+
print(annotations.get_class_annotations(SomeClass))
|
40
|
+
print(annotations.get_annotations(SomeClass))
|
41
|
+
print(annotations.get_annotations(some_function))
|
42
|
+
print(annotations.get_annotations(SomeClass.some_method))
|
43
|
+
|
44
|
+
|
45
|
+
if __name__ == "__main__":
|
46
|
+
_example()
|
rats/annotations/_functions.py
CHANGED
@@ -3,15 +3,14 @@ from __future__ import annotations
|
|
3
3
|
from collections import defaultdict
|
4
4
|
from collections.abc import Callable
|
5
5
|
from functools import cache
|
6
|
-
from typing import Any, Generic, ParamSpec, TypeVar
|
7
|
-
from typing import NamedTuple as tNamedTuple
|
6
|
+
from typing import Any, Generic, NamedTuple, ParamSpec, TypeVar
|
8
7
|
|
9
|
-
from typing_extensions import NamedTuple
|
8
|
+
from typing_extensions import NamedTuple as ExtNamedTuple
|
10
9
|
|
11
10
|
T_GroupType = TypeVar("T_GroupType", bound=NamedTuple)
|
12
11
|
|
13
12
|
|
14
|
-
class GroupAnnotations(
|
13
|
+
class GroupAnnotations(ExtNamedTuple, Generic[T_GroupType]):
|
15
14
|
"""The list of T_GroupType objects identified by a given name in a namespace."""
|
16
15
|
|
17
16
|
name: str
|
@@ -21,7 +20,7 @@ class GroupAnnotations(NamedTuple, Generic[T_GroupType]):
|
|
21
20
|
groups: tuple[T_GroupType, ...]
|
22
21
|
|
23
22
|
|
24
|
-
class AnnotationsContainer(
|
23
|
+
class AnnotationsContainer(NamedTuple):
|
25
24
|
"""
|
26
25
|
Holds metadata about the annotated functions or class methods.
|
27
26
|
|
@@ -65,7 +64,7 @@ class AnnotationsBuilder:
|
|
65
64
|
def __init__(self) -> None:
|
66
65
|
self._group_ids = defaultdict(set)
|
67
66
|
|
68
|
-
def add(self, namespace: str, group_id:
|
67
|
+
def add(self, namespace: str, group_id: ExtNamedTuple | NamedTuple) -> None:
|
69
68
|
self._group_ids[namespace].add(group_id)
|
70
69
|
|
71
70
|
def make(self, name: str) -> AnnotationsContainer:
|
@@ -84,7 +83,7 @@ DecoratorType = TypeVar("DecoratorType", bound=Callable[..., Any])
|
|
84
83
|
|
85
84
|
def annotation(
|
86
85
|
namespace: str,
|
87
|
-
group_id:
|
86
|
+
group_id: ExtNamedTuple | NamedTuple,
|
88
87
|
) -> Callable[[DecoratorType], DecoratorType]:
|
89
88
|
"""
|
90
89
|
Decorator to add an annotation to a function.
|
rats/apps/__init__.py
CHANGED
@@ -15,15 +15,15 @@ from ._annotations import (
|
|
15
15
|
)
|
16
16
|
from ._composite_container import CompositeContainer
|
17
17
|
from ._container import (
|
18
|
-
AnnotatedContainer, # type: ignore[reportDeprecated]
|
19
18
|
Container,
|
20
19
|
DuplicateServiceError,
|
20
|
+
Provider,
|
21
21
|
ServiceNotFoundError,
|
22
22
|
ServiceProvider,
|
23
23
|
container,
|
24
24
|
)
|
25
25
|
from ._executables import App, Executable
|
26
|
-
from ._ids import ServiceId, T_ExecutableType
|
26
|
+
from ._ids import ServiceId, T_ExecutableType, T_ServiceType
|
27
27
|
from ._namespaces import ProviderNamespaces
|
28
28
|
from ._plugin_container import PluginContainers
|
29
29
|
from ._plugins import PluginRunner
|
@@ -32,7 +32,6 @@ from ._scoping import autoscope
|
|
32
32
|
from ._simple_apps import AppServices, SimpleApplication, StandardRuntime
|
33
33
|
|
34
34
|
__all__ = [
|
35
|
-
"AnnotatedContainer",
|
36
35
|
"App",
|
37
36
|
"CompositeContainer",
|
38
37
|
"Container",
|
@@ -41,6 +40,7 @@ __all__ = [
|
|
41
40
|
"PluginContainers",
|
42
41
|
"ProviderNamespaces",
|
43
42
|
"ServiceProvider",
|
43
|
+
"Provider",
|
44
44
|
"ServiceId",
|
45
45
|
"ServiceNotFoundError",
|
46
46
|
"autoid_service",
|
@@ -53,6 +53,7 @@ __all__ = [
|
|
53
53
|
"autoid",
|
54
54
|
"service",
|
55
55
|
"T_ExecutableType",
|
56
|
+
"T_ServiceType",
|
56
57
|
"Runtime",
|
57
58
|
"NullRuntime",
|
58
59
|
"AppServices",
|
rats/apps/__main__.py
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
"""..."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import uuid
|
7
|
+
from collections.abc import Iterator
|
8
|
+
|
9
|
+
from rats import apps
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class ExampleData:
|
15
|
+
"""A simple data source that might come from another package."""
|
16
|
+
|
17
|
+
_num_samples: int
|
18
|
+
|
19
|
+
def __init__(self, num_samples: int) -> None:
|
20
|
+
"""..."""
|
21
|
+
self._num_samples = num_samples
|
22
|
+
|
23
|
+
def fetch(self) -> Iterator[str]:
|
24
|
+
"""Yields random samples."""
|
25
|
+
for i in range(self._num_samples):
|
26
|
+
yield json.dumps({"index": i, "sample": str(uuid.uuid4())})
|
27
|
+
|
28
|
+
|
29
|
+
class ExampleExe(apps.Executable):
|
30
|
+
"""..."""
|
31
|
+
|
32
|
+
_example_data: ExampleData
|
33
|
+
|
34
|
+
def __init__(self, example_data: ExampleData) -> None:
|
35
|
+
"""..."""
|
36
|
+
self._example_data = example_data
|
37
|
+
|
38
|
+
def execute(self) -> None:
|
39
|
+
"""..."""
|
40
|
+
for row in self._example_data.fetch():
|
41
|
+
print(row)
|
42
|
+
|
43
|
+
|
44
|
+
@apps.autoscope
|
45
|
+
class PluginServices:
|
46
|
+
"""..."""
|
47
|
+
|
48
|
+
MAIN_EXE = apps.ServiceId[apps.Executable]("main")
|
49
|
+
EXAMPLE_DATA = apps.ServiceId[ExampleData]("example-data")
|
50
|
+
|
51
|
+
|
52
|
+
class PluginContainer(apps.Container):
|
53
|
+
"""..."""
|
54
|
+
|
55
|
+
_app: apps.Container
|
56
|
+
|
57
|
+
def __init__(self, app: apps.Container) -> None:
|
58
|
+
"""..."""
|
59
|
+
self._app = app
|
60
|
+
|
61
|
+
@apps.service(PluginServices.MAIN_EXE)
|
62
|
+
def _main_exe(self) -> apps.Executable:
|
63
|
+
"""..."""
|
64
|
+
return ExampleExe(
|
65
|
+
example_data=self._app.get(PluginServices.EXAMPLE_DATA),
|
66
|
+
)
|
67
|
+
|
68
|
+
@apps.service(PluginServices.EXAMPLE_DATA)
|
69
|
+
def _example_data(self) -> ExampleData:
|
70
|
+
"""..."""
|
71
|
+
return ExampleData(
|
72
|
+
num_samples=int(os.environ.get("EXAMPLE_DATA_NUM_SAMPLES", "5")),
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
if __name__ == "__main__":
|
77
|
+
apps.SimpleApplication(runtime_plugin=PluginContainer).execute(
|
78
|
+
PluginServices.MAIN_EXE,
|
79
|
+
)
|
rats/apps/_annotations.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from collections.abc import Callable
|
2
|
-
from typing import Any, ParamSpec
|
2
|
+
from typing import Any, NamedTuple, ParamSpec, cast
|
3
3
|
|
4
4
|
from rats import annotations
|
5
5
|
|
@@ -14,19 +14,19 @@ def service(
|
|
14
14
|
service_id: ServiceId[T_ServiceType],
|
15
15
|
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
16
16
|
"""A service is anything you would create instances of?"""
|
17
|
-
return annotations.annotation(ProviderNamespaces.SERVICES, service_id)
|
17
|
+
return annotations.annotation(ProviderNamespaces.SERVICES, cast(NamedTuple, service_id))
|
18
18
|
|
19
19
|
|
20
20
|
def autoid_service(fn: Callable[P, T_ServiceType]) -> Callable[P, T_ServiceType]:
|
21
21
|
_service_id = autoid(fn)
|
22
|
-
return annotations.annotation(ProviderNamespaces.SERVICES, _service_id)(fn)
|
22
|
+
return annotations.annotation(ProviderNamespaces.SERVICES, cast(NamedTuple, _service_id))(fn)
|
23
23
|
|
24
24
|
|
25
25
|
def group(
|
26
26
|
group_id: ServiceId[T_ServiceType],
|
27
27
|
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
28
28
|
"""A group is a collection of services."""
|
29
|
-
return annotations.annotation(ProviderNamespaces.GROUPS, group_id)
|
29
|
+
return annotations.annotation(ProviderNamespaces.GROUPS, cast(NamedTuple, group_id))
|
30
30
|
|
31
31
|
|
32
32
|
def fallback_service(
|
@@ -35,7 +35,7 @@ def fallback_service(
|
|
35
35
|
"""A fallback service gets used if no service is defined."""
|
36
36
|
return annotations.annotation(
|
37
37
|
ProviderNamespaces.FALLBACK_SERVICES,
|
38
|
-
service_id,
|
38
|
+
cast(NamedTuple, service_id),
|
39
39
|
)
|
40
40
|
|
41
41
|
|
@@ -45,7 +45,7 @@ def fallback_group(
|
|
45
45
|
"""A fallback group gets used if no group is defined."""
|
46
46
|
return annotations.annotation(
|
47
47
|
ProviderNamespaces.FALLBACK_GROUPS,
|
48
|
-
group_id,
|
48
|
+
cast(NamedTuple, group_id),
|
49
49
|
)
|
50
50
|
|
51
51
|
|
rats/apps/_container.py
CHANGED
@@ -1,10 +1,7 @@
|
|
1
|
-
import abc
|
2
1
|
import logging
|
3
2
|
from abc import abstractmethod
|
4
3
|
from collections.abc import Callable, Iterator
|
5
|
-
from typing import Generic, ParamSpec, Protocol
|
6
|
-
|
7
|
-
from typing_extensions import deprecated
|
4
|
+
from typing import Generic, NamedTuple, ParamSpec, Protocol, cast
|
8
5
|
|
9
6
|
from rats import annotations
|
10
7
|
|
@@ -14,12 +11,16 @@ from ._namespaces import ProviderNamespaces
|
|
14
11
|
logger = logging.getLogger(__name__)
|
15
12
|
|
16
13
|
|
17
|
-
class
|
14
|
+
class Provider(Protocol[Tco_ServiceType]):
|
18
15
|
@abstractmethod
|
19
16
|
def __call__(self) -> Tco_ServiceType:
|
20
17
|
"""Return the service instance."""
|
21
18
|
|
22
19
|
|
20
|
+
# temporary alias for backwards compatibility
|
21
|
+
ServiceProvider = Provider
|
22
|
+
|
23
|
+
|
23
24
|
class GroupProvider(Protocol[Tco_ServiceType]):
|
24
25
|
@abstractmethod
|
25
26
|
def __call__(self) -> Iterator[Tco_ServiceType]:
|
@@ -119,8 +120,9 @@ class Container(Protocol):
|
|
119
120
|
) -> Iterator[T_ServiceType]:
|
120
121
|
"""Retrieve a service group by its id, within a given service namespace."""
|
121
122
|
tates = annotations.get_class_annotations(type(self))
|
123
|
+
# containers are a special service namespace that we look through recursively
|
122
124
|
containers = tates.with_namespace(ProviderNamespaces.CONTAINERS)
|
123
|
-
groups = tates.with_group(namespace, group_id)
|
125
|
+
groups = tates.with_group(namespace, cast(NamedTuple, group_id))
|
124
126
|
|
125
127
|
for annotation in groups.annotations:
|
126
128
|
if not hasattr(self, f"__rats_cache_{annotation.name}"):
|
@@ -140,33 +142,14 @@ class Container(Protocol):
|
|
140
142
|
yield from c.get_namespaced_group(namespace, group_id)
|
141
143
|
|
142
144
|
|
143
|
-
|
144
|
-
" ".join(
|
145
|
-
[
|
146
|
-
"AnnotatedContainer is deprecated and will be removed in the next major release.",
|
147
|
-
"The functionality has been moved into the apps.Container protocol.",
|
148
|
-
"Please extend apps.Container directly.",
|
149
|
-
]
|
150
|
-
),
|
151
|
-
stacklevel=2,
|
152
|
-
)
|
153
|
-
class AnnotatedContainer(Container, abc.ABC):
|
154
|
-
"""
|
155
|
-
A Container implementation that extracts providers from its annotated methods.
|
156
|
-
|
157
|
-
.. deprecated:: 0.1.3
|
158
|
-
The behavior of this class has been made the default within ``Container``.
|
159
|
-
"""
|
160
|
-
|
161
|
-
|
162
|
-
DEFAULT_CONTAINER_GROUP = ServiceId[Container]("__default__")
|
145
|
+
DEFAULT_CONTAINER_GROUP = ServiceId[Container](f"{__name__}:__default__")
|
163
146
|
P = ParamSpec("P")
|
164
147
|
|
165
148
|
|
166
149
|
def container(
|
167
150
|
group_id: ServiceId[T_ServiceType] = DEFAULT_CONTAINER_GROUP,
|
168
151
|
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
169
|
-
return annotations.annotation(ProviderNamespaces.CONTAINERS, group_id)
|
152
|
+
return annotations.annotation(ProviderNamespaces.CONTAINERS, cast(NamedTuple, group_id))
|
170
153
|
|
171
154
|
|
172
155
|
class ServiceNotFoundError(RuntimeError, Generic[T_ServiceType]):
|
rats/apps/_runtimes.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
import logging
|
1
2
|
from abc import abstractmethod
|
2
3
|
from collections.abc import Callable
|
3
4
|
from typing import Protocol, final
|
4
5
|
|
5
6
|
from ._ids import ServiceId, T_ExecutableType
|
6
7
|
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
7
10
|
|
8
11
|
class Runtime(Protocol):
|
9
12
|
@abstractmethod
|
@@ -31,11 +34,17 @@ class Runtime(Protocol):
|
|
31
34
|
|
32
35
|
@final
|
33
36
|
class NullRuntime(Runtime):
|
37
|
+
_msg: str
|
38
|
+
|
39
|
+
def __init__(self, msg: str) -> None:
|
40
|
+
self._msg = msg
|
41
|
+
|
34
42
|
def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
|
35
|
-
|
43
|
+
logger.error(self._msg)
|
44
|
+
raise NotImplementedError(f"NullRuntime cannot execute ids: {exe_ids}")
|
36
45
|
|
37
46
|
def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
|
38
|
-
raise NotImplementedError("NullRuntime
|
47
|
+
raise NotImplementedError(f"NullRuntime cannot execute groups: {exe_group_ids}")
|
39
48
|
|
40
49
|
def execute_callable(self, *callables: Callable[[], None]) -> None:
|
41
|
-
raise NotImplementedError("NullRuntime
|
50
|
+
raise NotImplementedError(f"NullRuntime cannot execute callables: {callables}")
|
rats/cli/__init__.py
CHANGED
@@ -1,22 +1,17 @@
|
|
1
1
|
"""Uses `rats.cli` to streamline the creation of CLI commands written with Click."""
|
2
2
|
|
3
|
-
from ._annotations import
|
4
|
-
from .
|
5
|
-
from .
|
3
|
+
from ._annotations import command, get_class_commands, get_class_groups, group
|
4
|
+
from ._app import ClickApp
|
5
|
+
from ._container import CommandContainer
|
6
6
|
from ._plugin import PluginContainer, PluginServices
|
7
|
-
from ._plugins import AttachClickCommands, AttachClickGroup, ClickGroupPlugin, CommandContainer
|
8
7
|
|
9
8
|
__all__ = [
|
10
|
-
"CommandId",
|
11
9
|
"PluginContainer",
|
12
10
|
"command",
|
11
|
+
"get_class_commands",
|
12
|
+
"get_class_groups",
|
13
13
|
"group",
|
14
|
+
"ClickApp",
|
14
15
|
"PluginServices",
|
15
|
-
"ClickCommandMapper",
|
16
|
-
"ClickExecutable",
|
17
|
-
"ClickGroupPlugin",
|
18
|
-
"ClickCommandGroup",
|
19
|
-
"AttachClickCommands",
|
20
|
-
"AttachClickGroup",
|
21
16
|
"CommandContainer",
|
22
17
|
]
|
rats/cli/__main__.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
"""..."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from rats import apps, cli
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class ExampleCommands(cli.CommandContainer):
|
13
|
+
"""An example collection of cli commands."""
|
14
|
+
|
15
|
+
@cli.command()
|
16
|
+
@click.option("--exe-id", multiple=True)
|
17
|
+
def _run_this(self, exe_id: tuple[str, ...]) -> None:
|
18
|
+
"""Example cli command called run-this."""
|
19
|
+
print(f"running these exes: {exe_id}")
|
20
|
+
|
21
|
+
@cli.command()
|
22
|
+
@click.option("--exe-id", multiple=True)
|
23
|
+
def _run_that(self, exe_id: tuple[str, ...]) -> None:
|
24
|
+
"""Example cli command called run-that."""
|
25
|
+
print(f"running those exes: {exe_id}")
|
26
|
+
|
27
|
+
@cli.group()
|
28
|
+
def _run_these(self) -> None:
|
29
|
+
"""Example cli command called run-these."""
|
30
|
+
print("running these sub-things")
|
31
|
+
|
32
|
+
|
33
|
+
@apps.autoscope
|
34
|
+
class ExampleServices:
|
35
|
+
"""
|
36
|
+
Services used by the example container.
|
37
|
+
|
38
|
+
These classes are global constants to identify the provided services.
|
39
|
+
"""
|
40
|
+
|
41
|
+
MAIN = apps.ServiceId[apps.Executable]("main")
|
42
|
+
|
43
|
+
|
44
|
+
class ExampleContainer(apps.Container):
|
45
|
+
"""An example container of services."""
|
46
|
+
|
47
|
+
_app: apps.Container
|
48
|
+
|
49
|
+
def __init__(self, app: apps.Container) -> None:
|
50
|
+
"""The root container allows us to access services in other plugins."""
|
51
|
+
self._app = app
|
52
|
+
|
53
|
+
@apps.service(ExampleServices.MAIN)
|
54
|
+
def _main(self) -> apps.Executable:
|
55
|
+
return cli.ClickApp(
|
56
|
+
group=click.Group("example", help="An example application."),
|
57
|
+
commands=ExampleCommands(),
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
if __name__ == "__main__":
|
62
|
+
apps.SimpleApplication(runtime_plugin=ExampleContainer).execute(
|
63
|
+
ExampleServices.MAIN,
|
64
|
+
)
|
rats/cli/_annotations.py
CHANGED
@@ -4,37 +4,38 @@ from collections.abc import Callable
|
|
4
4
|
from typing import Any, NamedTuple, TypeVar
|
5
5
|
|
6
6
|
from rats import annotations as anns
|
7
|
+
from rats import apps
|
7
8
|
|
8
9
|
|
9
10
|
class CommandId(NamedTuple):
|
10
11
|
name: str
|
11
12
|
|
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
13
|
|
14
|
+
AUTO_COMMAND = CommandId("__auto__")
|
18
15
|
T = TypeVar("T", bound=Callable[[Any], Any])
|
19
16
|
|
20
17
|
|
21
|
-
def command(command_id: CommandId) -> Callable[
|
18
|
+
def command(command_id: CommandId = AUTO_COMMAND) -> Callable[..., apps.Executable]:
|
22
19
|
def decorator(fn: T) -> T:
|
23
|
-
if command_id ==
|
20
|
+
if command_id == AUTO_COMMAND:
|
24
21
|
return anns.annotation("commands", CommandId(fn.__name__.replace("_", "-")))(fn)
|
25
22
|
return anns.annotation("commands", command_id)(fn)
|
26
23
|
|
27
|
-
return decorator
|
24
|
+
return decorator # type: ignore[reportReturnType]
|
28
25
|
|
29
26
|
|
30
|
-
def group(command_id: CommandId) -> Callable[
|
27
|
+
def group(command_id: CommandId = AUTO_COMMAND) -> Callable[..., apps.Executable]:
|
31
28
|
def decorator(fn: T) -> T:
|
32
|
-
if command_id ==
|
29
|
+
if command_id == AUTO_COMMAND:
|
33
30
|
return anns.annotation("command-groups", CommandId(fn.__name__.replace("_", "-")))(fn)
|
34
31
|
return anns.annotation("commands", command_id)(fn)
|
35
32
|
|
36
|
-
return decorator
|
33
|
+
return decorator # type: ignore[reportReturnType]
|
37
34
|
|
38
35
|
|
39
36
|
def get_class_commands(cls: type) -> anns.AnnotationsContainer:
|
40
37
|
return anns.get_class_annotations(cls).with_namespace("commands")
|
38
|
+
|
39
|
+
|
40
|
+
def get_class_groups(cls: type) -> anns.AnnotationsContainer:
|
41
|
+
return anns.get_class_annotations(cls).with_namespace("command-groups")
|
rats/cli/_app.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import final
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from rats import apps
|
6
|
+
|
7
|
+
from ._container import CommandContainer
|
8
|
+
|
9
|
+
|
10
|
+
@final
|
11
|
+
class ClickApp(apps.Executable):
|
12
|
+
"""..."""
|
13
|
+
|
14
|
+
_group: click.Group
|
15
|
+
_commands: CommandContainer
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
group: click.Group,
|
20
|
+
commands: CommandContainer,
|
21
|
+
) -> None:
|
22
|
+
"""Not sure this is the right interface."""
|
23
|
+
self._group = group
|
24
|
+
self._commands = commands
|
25
|
+
|
26
|
+
def execute(self) -> None:
|
27
|
+
"""This app executes a click application after letting rats plugins attach commands."""
|
28
|
+
self._commands.attach(self._group)
|
29
|
+
self._group.main()
|
rats/cli/_container.py
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
import logging
|
2
|
+
from collections.abc import Callable
|
3
|
+
from functools import partial
|
4
|
+
from typing import Any, Protocol
|
5
|
+
|
6
|
+
import click
|
7
|
+
|
8
|
+
from rats import apps
|
9
|
+
|
10
|
+
from ._annotations import get_class_commands
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class CommandContainer(apps.Container, Protocol):
|
16
|
+
"""A container that can attach click commands to a click group."""
|
17
|
+
|
18
|
+
def attach(self, group: click.Group) -> None:
|
19
|
+
"""..."""
|
20
|
+
|
21
|
+
def cb(_method: Callable[..., None], *args: Any, **kwargs: Any) -> None:
|
22
|
+
"""
|
23
|
+
Callback handed to `click.Command`. Calls the method with matching name on this class.
|
24
|
+
|
25
|
+
When the command is decorated with `@click.params` and `@click.option`, `click` will
|
26
|
+
call this callback with the parameters in the order they were defined. This callback
|
27
|
+
then calls the method with the same name on this class, passing the parameters in
|
28
|
+
reverse order. This is because the method is defined with the parameters in the
|
29
|
+
reverse order to the decorator, so we need to reverse them again to get the correct
|
30
|
+
order.
|
31
|
+
"""
|
32
|
+
_method(*args, **kwargs)
|
33
|
+
|
34
|
+
commands = get_class_commands(type(self))
|
35
|
+
tates = commands.annotations
|
36
|
+
|
37
|
+
for tate in tates:
|
38
|
+
method = getattr(self, tate.name)
|
39
|
+
params = list(reversed(getattr(method, "__click_params__", [])))
|
40
|
+
logger.debug(tate.namespace)
|
41
|
+
for command in tate.groups:
|
42
|
+
if tate.namespace == "commands":
|
43
|
+
group.add_command(
|
44
|
+
click.Command(
|
45
|
+
name=command.name,
|
46
|
+
callback=partial(cb, method),
|
47
|
+
short_help=method.__doc__,
|
48
|
+
params=params,
|
49
|
+
)
|
50
|
+
)
|
rats/cli/_plugin.py
CHANGED
@@ -7,24 +7,11 @@ from rats import apps
|
|
7
7
|
|
8
8
|
@apps.autoscope
|
9
9
|
class _PluginEvents:
|
10
|
-
|
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}]")
|
10
|
+
pass
|
23
11
|
|
24
12
|
|
25
13
|
@apps.autoscope
|
26
14
|
class PluginServices:
|
27
|
-
ROOT_COMMAND = apps.ServiceId[apps.Executable]("root-command")
|
28
15
|
EVENTS = _PluginEvents
|
29
16
|
|
30
17
|
@staticmethod
|
@@ -45,24 +32,3 @@ class PluginContainer(apps.Container):
|
|
45
32
|
|
46
33
|
def __init__(self, app: apps.Container) -> None:
|
47
34
|
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/logs/py.typed
ADDED
File without changes
|
@@ -0,0 +1,32 @@
|
|
1
|
+
rats/annotations/__init__.py,sha256=wsGhRQzZrV2oJTnBAX0aGgpyT1kYT235jkP3Wb8BTRY,498
|
2
|
+
rats/annotations/__main__.py,sha256=vlzQOM9y82P0OL5tYcmSM_4jTg0s8jayAcvEoi9cBvI,1065
|
3
|
+
rats/annotations/_functions.py,sha256=2rIWruEVZ-mBviS_7un88ZODbfGmiJgzIp4CXZoAgEE,4346
|
4
|
+
rats/annotations/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
rats/apps/__init__.py,sha256=LiN5DSEwzB8IeAvTTEH7tQF5btD9QT4WNQwu7_48ZyU,1493
|
6
|
+
rats/apps/__main__.py,sha256=KjdadN4rdP0xhWiLzdmtCsXejWx_gxOK-ah-L1r1dTI,1818
|
7
|
+
rats/apps/_annotations.py,sha256=yOf4MKFGQz3x4hpaWgMf2y2GlbXtbIh4uED15voXzyY,2566
|
8
|
+
rats/apps/_composite_container.py,sha256=s_of6NyyrjFVYWGVehyEHe9WJIPRCnbB-tyWyNF8zyc,585
|
9
|
+
rats/apps/_container.py,sha256=sghUmXUFtzm2YwvhW6H1vlxX6-knxAsB6CNBApbJhK0,5851
|
10
|
+
rats/apps/_executables.py,sha256=QJ5_UPdZPmDQ1a3cLRJDUoeUMzNMwa3ZHYhYeS3AVq4,971
|
11
|
+
rats/apps/_ids.py,sha256=T8Onrj79t8NPfBMQBk0xI6fIWDKF0m2JfFNrdtXAbWg,353
|
12
|
+
rats/apps/_namespaces.py,sha256=THUV_Xj5PtweC23Ob-zsSpk8exC4fT-qRwjpQ6IDm0U,188
|
13
|
+
rats/apps/_plugin_container.py,sha256=wmaBgxmvKo82ue9CrKHRXafgik5wIXh8XkEYMfhcTjs,1530
|
14
|
+
rats/apps/_plugins.py,sha256=mvSYQPi11wTGZpM4umoyD37Rc8CQX8nt5eAJbmLrBFM,688
|
15
|
+
rats/apps/_runtimes.py,sha256=qKhsuaH3ZesSP4pGwRbS8zj2mwapysSxyS_F9pkUtM4,1738
|
16
|
+
rats/apps/_scoping.py,sha256=EIUopw3b38CEV57kCmSKQTAnQQMcXHZ_vwtk9t0K__g,1453
|
17
|
+
rats/apps/_simple_apps.py,sha256=n-3zeHY3iintZ9LN597c7zDHv3DiIdl7c8NTk0gUk1Y,5477
|
18
|
+
rats/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
rats/cli/__init__.py,sha256=_g9xWv6Uy73AudzCn-A9JKP-IpfZMRgvoO1b64dAxPw,464
|
20
|
+
rats/cli/__main__.py,sha256=3JZ7mrTTrrODQHutefm2zJp1-cQQB7I5-1xhYw7SMBU,1656
|
21
|
+
rats/cli/_annotations.py,sha256=xSOfGFRYI2s9MTUdXgJ-ZcgcaD6EntZpiAuMqAVh0sI,1316
|
22
|
+
rats/cli/_app.py,sha256=NjJfXKZYBdd1CZuLbrXyUFB_wRJQah1Rvtxe_zj4y_M,641
|
23
|
+
rats/cli/_container.py,sha256=VddjrsH1lqiarJ6rXf4KUbuNtNXEduCr38UH_TwGgFE,1872
|
24
|
+
rats/cli/_plugin.py,sha256=o5emP-E0LLOGvD14ZBYNY6h407pngrJf8ODMB5Wdd8U,711
|
25
|
+
rats/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
+
rats/logs/__init__.py,sha256=fCn4pfpYiAcTtt5CsnUZX68CjOB3KJHxMSiYxsma4qE,183
|
27
|
+
rats/logs/_plugin.py,sha256=eAAG4ci-XS9A9ituXj9PrcI6zH-xlCqhJlUbSGC-MYM,2175
|
28
|
+
rats/logs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
+
rats_apps-0.2.0.dev20240813092513.dist-info/METADATA,sha256=HItBWvbdKnqpKz3vSiVb-swPARAqeXrjm2Gqup6Llfk,774
|
30
|
+
rats_apps-0.2.0.dev20240813092513.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
31
|
+
rats_apps-0.2.0.dev20240813092513.dist-info/entry_points.txt,sha256=9oOvf2loQr5ACWQgvuu9Q3KZIVIxKE5Aa-rLuUII5WQ,91
|
32
|
+
rats_apps-0.2.0.dev20240813092513.dist-info/RECORD,,
|
rats/cli/_click.py
DELETED
@@ -1,38 +0,0 @@
|
|
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
DELETED
@@ -1,23 +0,0 @@
|
|
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/_plugins.py
DELETED
@@ -1,81 +0,0 @@
|
|
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
|
-
)
|
@@ -1,29 +0,0 @@
|
|
1
|
-
rats/annotations/__init__.py,sha256=wsGhRQzZrV2oJTnBAX0aGgpyT1kYT235jkP3Wb8BTRY,498
|
2
|
-
rats/annotations/_functions.py,sha256=ziAZOqS1lojfjDjZW7qPYNJUAFuLBWtVAoPsmJqxqpE,4356
|
3
|
-
rats/annotations/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
rats/apps/__init__.py,sha256=z_6eujGnU6pW12xTRR1caTJzv5v8VEbt8yLmwnMBiik,1511
|
5
|
-
rats/apps/_annotations.py,sha256=6qBqjxfBBZyVMLT3COGgQ-flQL6Wkuzp_TtndD4TwmE,2458
|
6
|
-
rats/apps/_composite_container.py,sha256=s_of6NyyrjFVYWGVehyEHe9WJIPRCnbB-tyWyNF8zyc,585
|
7
|
-
rats/apps/_container.py,sha256=GTPu6qmVCy69w87kMsQcL5BJavgW9O9RxH0z0zQu3fY,6242
|
8
|
-
rats/apps/_executables.py,sha256=QJ5_UPdZPmDQ1a3cLRJDUoeUMzNMwa3ZHYhYeS3AVq4,971
|
9
|
-
rats/apps/_ids.py,sha256=T8Onrj79t8NPfBMQBk0xI6fIWDKF0m2JfFNrdtXAbWg,353
|
10
|
-
rats/apps/_namespaces.py,sha256=THUV_Xj5PtweC23Ob-zsSpk8exC4fT-qRwjpQ6IDm0U,188
|
11
|
-
rats/apps/_plugin_container.py,sha256=wmaBgxmvKo82ue9CrKHRXafgik5wIXh8XkEYMfhcTjs,1530
|
12
|
-
rats/apps/_plugins.py,sha256=mvSYQPi11wTGZpM4umoyD37Rc8CQX8nt5eAJbmLrBFM,688
|
13
|
-
rats/apps/_runtimes.py,sha256=nQWwC-O-j0bKCEkGpBSXhdtE0-X2ACeP7zXZfqaPGqg,1545
|
14
|
-
rats/apps/_scoping.py,sha256=EIUopw3b38CEV57kCmSKQTAnQQMcXHZ_vwtk9t0K__g,1453
|
15
|
-
rats/apps/_simple_apps.py,sha256=n-3zeHY3iintZ9LN597c7zDHv3DiIdl7c8NTk0gUk1Y,5477
|
16
|
-
rats/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
rats/cli/__init__.py,sha256=dMrubxMJocbl8QoI_3fKI0Dt0QHYAM5Mxh5Vj8OaX-w,664
|
18
|
-
rats/cli/_annotations.py,sha256=0dI8hu_y754Y53Pka1-mGEgHjjVcnIOGd8l1SFx8OBY,1190
|
19
|
-
rats/cli/_click.py,sha256=7-ClnYSW4poFr_B-Q6NT45DnMF1XL7ntUgwQqQ7q_eo,1036
|
20
|
-
rats/cli/_executable.py,sha256=kAQ9hImv3hBaScu6e19o_BMvl7tdYJql38E76S3FjSk,580
|
21
|
-
rats/cli/_plugin.py,sha256=j9exzjL4sTCrGvine0rxumdfl0RPGpOFk7Ysk73aKWk,2278
|
22
|
-
rats/cli/_plugins.py,sha256=H3-QdaICPJhCC5FkLHdXpwqe7Z0mpvsenakhNiPllC8,2739
|
23
|
-
rats/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
|
-
rats/logs/__init__.py,sha256=fCn4pfpYiAcTtt5CsnUZX68CjOB3KJHxMSiYxsma4qE,183
|
25
|
-
rats/logs/_plugin.py,sha256=eAAG4ci-XS9A9ituXj9PrcI6zH-xlCqhJlUbSGC-MYM,2175
|
26
|
-
rats_apps-0.1.3.dev20240812192218.dist-info/METADATA,sha256=kHFSAtQ6vCGDztELYoYp6GCYz76XPL6RPtdU-n6sojA,774
|
27
|
-
rats_apps-0.1.3.dev20240812192218.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
28
|
-
rats_apps-0.1.3.dev20240812192218.dist-info/entry_points.txt,sha256=9oOvf2loQr5ACWQgvuu9Q3KZIVIxKE5Aa-rLuUII5WQ,91
|
29
|
-
rats_apps-0.1.3.dev20240812192218.dist-info/RECORD,,
|
{rats_apps-0.1.3.dev20240812192218.dist-info → rats_apps-0.2.0.dev20240813092513.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|