rats-apps 0.1.2.dev15__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.dev15.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.dev15.dist-info/RECORD +0 -13
- rats_apps-0.1.2.dev15.dist-info/entry_points.txt +0 -3
- {rats_apps-0.1.2.dev15.dist-info → rats_apps-0.1.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
"""
|
2
|
+
General purpose library to attach annotations to functions.
|
3
|
+
|
4
|
+
Annotations are typically, but not exclusively, attached using decorators.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from ._functions import (
|
8
|
+
AnnotationsContainer,
|
9
|
+
DecoratorType,
|
10
|
+
GroupAnnotations,
|
11
|
+
T_GroupType,
|
12
|
+
annotation,
|
13
|
+
get_annotations,
|
14
|
+
get_class_annotations,
|
15
|
+
)
|
16
|
+
|
17
|
+
__all__ = [
|
18
|
+
"annotation",
|
19
|
+
"DecoratorType",
|
20
|
+
"AnnotationsContainer",
|
21
|
+
"get_annotations",
|
22
|
+
"get_class_annotations",
|
23
|
+
"GroupAnnotations",
|
24
|
+
"T_GroupType",
|
25
|
+
]
|
@@ -0,0 +1,145 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections import defaultdict
|
4
|
+
from collections.abc import Callable
|
5
|
+
from functools import cache
|
6
|
+
from typing import Any, Generic, ParamSpec, TypeVar
|
7
|
+
from typing import NamedTuple as tNamedTuple
|
8
|
+
|
9
|
+
from typing_extensions import NamedTuple
|
10
|
+
|
11
|
+
T_GroupType = TypeVar("T_GroupType", bound=NamedTuple)
|
12
|
+
|
13
|
+
|
14
|
+
class GroupAnnotations(NamedTuple, Generic[T_GroupType]):
|
15
|
+
"""The list of T_GroupType objects identified by a given name in a namespace."""
|
16
|
+
|
17
|
+
name: str
|
18
|
+
"""The name of the annotation, typically the name of the function in a class"""
|
19
|
+
namespace: str
|
20
|
+
"""All the groups in a namespace are expected to be of the same T_GroupType."""
|
21
|
+
groups: tuple[T_GroupType, ...]
|
22
|
+
|
23
|
+
|
24
|
+
class AnnotationsContainer(tNamedTuple):
|
25
|
+
"""
|
26
|
+
Holds metadata about the annotated functions or class methods.
|
27
|
+
|
28
|
+
Loosely inspired by: https://peps.python.org/pep-3107/.
|
29
|
+
"""
|
30
|
+
|
31
|
+
annotations: tuple[GroupAnnotations[Any], ...]
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
def empty() -> AnnotationsContainer:
|
35
|
+
return AnnotationsContainer(annotations=())
|
36
|
+
|
37
|
+
def with_group(
|
38
|
+
self,
|
39
|
+
namespace: str,
|
40
|
+
group_id: NamedTuple,
|
41
|
+
) -> AnnotationsContainer:
|
42
|
+
return AnnotationsContainer(
|
43
|
+
annotations=tuple(
|
44
|
+
[
|
45
|
+
annotation_group
|
46
|
+
for x in self.with_namespace(namespace)
|
47
|
+
for annotation_group in x
|
48
|
+
if group_id in annotation_group.groups
|
49
|
+
]
|
50
|
+
),
|
51
|
+
)
|
52
|
+
|
53
|
+
def with_namespace(
|
54
|
+
self,
|
55
|
+
namespace: str,
|
56
|
+
) -> AnnotationsContainer:
|
57
|
+
return AnnotationsContainer(
|
58
|
+
annotations=tuple([x for x in self.annotations if x.namespace == namespace]),
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
class AnnotationsBuilder:
|
63
|
+
_group_ids: dict[str, set[Any]]
|
64
|
+
|
65
|
+
def __init__(self) -> None:
|
66
|
+
self._group_ids = defaultdict(set)
|
67
|
+
|
68
|
+
def add(self, namespace: str, group_id: NamedTuple | tNamedTuple) -> None:
|
69
|
+
self._group_ids[namespace].add(group_id)
|
70
|
+
|
71
|
+
def make(self, name: str) -> AnnotationsContainer:
|
72
|
+
return AnnotationsContainer(
|
73
|
+
annotations=tuple(
|
74
|
+
[
|
75
|
+
GroupAnnotations[Any](name=name, namespace=namespace, groups=tuple(groups))
|
76
|
+
for namespace, groups in self._group_ids.items()
|
77
|
+
]
|
78
|
+
),
|
79
|
+
)
|
80
|
+
|
81
|
+
|
82
|
+
DecoratorType = TypeVar("DecoratorType", bound=Callable[..., Any])
|
83
|
+
|
84
|
+
|
85
|
+
def annotation(
|
86
|
+
namespace: str,
|
87
|
+
group_id: NamedTuple | tNamedTuple,
|
88
|
+
) -> Callable[[DecoratorType], DecoratorType]:
|
89
|
+
"""
|
90
|
+
Decorator to add an annotation to a function.
|
91
|
+
|
92
|
+
Typically used to create domain-specific annotation functions for things like DI Containers.
|
93
|
+
For examples, see the rats.apps annotations, like service() and group().
|
94
|
+
"""
|
95
|
+
|
96
|
+
def decorator(fn: DecoratorType) -> DecoratorType:
|
97
|
+
if not hasattr(fn, "__rats_annotations__"):
|
98
|
+
fn.__rats_annotations__ = AnnotationsBuilder() # type: ignore[reportFunctionMemberAccess]
|
99
|
+
|
100
|
+
fn.__rats_annotations__.add(namespace, group_id) # type: ignore[reportFunctionMemberAccess]
|
101
|
+
|
102
|
+
return fn
|
103
|
+
|
104
|
+
return decorator
|
105
|
+
|
106
|
+
|
107
|
+
@cache
|
108
|
+
def get_class_annotations(cls: type) -> AnnotationsContainer:
|
109
|
+
"""
|
110
|
+
Get all annotations for a class.
|
111
|
+
|
112
|
+
Traverses the class methods looking for any annotated with "__rats_annotations__" and returns
|
113
|
+
an instance of AnnotationsContainer. This function tries to cache the results to avoid any
|
114
|
+
expensive parsing of the class methods.
|
115
|
+
"""
|
116
|
+
tates: list[GroupAnnotations[Any]] = []
|
117
|
+
|
118
|
+
for method_name in dir(cls):
|
119
|
+
method = getattr(cls, method_name)
|
120
|
+
if not hasattr(method, "__rats_annotations__"):
|
121
|
+
continue
|
122
|
+
|
123
|
+
builder: AnnotationsBuilder = method.__rats_annotations__
|
124
|
+
tates.extend(builder.make(method_name).annotations)
|
125
|
+
|
126
|
+
return AnnotationsContainer(annotations=tuple(tates))
|
127
|
+
|
128
|
+
|
129
|
+
P = ParamSpec("P")
|
130
|
+
|
131
|
+
|
132
|
+
def get_annotations(fn: Callable[..., Any]) -> AnnotationsContainer:
|
133
|
+
"""
|
134
|
+
Get all annotations for a function or class method.
|
135
|
+
|
136
|
+
Builds an instance of AnnotationsContainer from the annotations found in the object's
|
137
|
+
"__rats_annotations__" attribute.
|
138
|
+
"""
|
139
|
+
builder: AnnotationsBuilder = getattr(
|
140
|
+
fn,
|
141
|
+
"__rats_annotations__",
|
142
|
+
AnnotationsBuilder(),
|
143
|
+
)
|
144
|
+
|
145
|
+
return builder.make(fn.__name__)
|
File without changes
|
rats/apps/__init__.py
CHANGED
@@ -1,39 +1,62 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Libraries to help create applications with a strong focus on composability and testability.
|
3
3
|
|
4
4
|
Applications give you the ability to define a development experience to match your project's
|
5
5
|
domain.
|
6
6
|
"""
|
7
7
|
|
8
8
|
from ._annotations import (
|
9
|
-
|
10
|
-
|
11
|
-
container,
|
12
|
-
fallback_config,
|
9
|
+
autoid,
|
10
|
+
autoid_service,
|
13
11
|
fallback_group,
|
14
12
|
fallback_service,
|
15
13
|
group,
|
16
14
|
service,
|
17
15
|
)
|
18
|
-
from .
|
19
|
-
from .
|
16
|
+
from ._composite_container import CompositeContainer
|
17
|
+
from ._container import (
|
18
|
+
AnnotatedContainer, # type: ignore[reportDeprecated]
|
19
|
+
Container,
|
20
|
+
DuplicateServiceError,
|
21
|
+
ServiceNotFoundError,
|
22
|
+
ServiceProvider,
|
23
|
+
container,
|
24
|
+
)
|
25
|
+
from ._executables import App, Executable
|
26
|
+
from ._ids import ServiceId, T_ExecutableType
|
20
27
|
from ._namespaces import ProviderNamespaces
|
28
|
+
from ._plugin_container import PluginContainers
|
29
|
+
from ._plugins import PluginRunner
|
30
|
+
from ._runtimes import NullRuntime, Runtime
|
21
31
|
from ._scoping import autoscope
|
32
|
+
from ._simple_apps import AppServices, SimpleApplication, StandardRuntime
|
22
33
|
|
23
34
|
__all__ = [
|
24
35
|
"AnnotatedContainer",
|
25
|
-
"
|
36
|
+
"App",
|
37
|
+
"CompositeContainer",
|
26
38
|
"Container",
|
27
39
|
"DuplicateServiceError",
|
40
|
+
"Executable",
|
41
|
+
"PluginContainers",
|
28
42
|
"ProviderNamespaces",
|
43
|
+
"ServiceProvider",
|
29
44
|
"ServiceId",
|
30
45
|
"ServiceNotFoundError",
|
46
|
+
"autoid_service",
|
31
47
|
"autoscope",
|
32
|
-
"config",
|
33
48
|
"container",
|
34
|
-
"
|
49
|
+
"PluginRunner",
|
35
50
|
"fallback_group",
|
36
51
|
"fallback_service",
|
37
52
|
"group",
|
53
|
+
"autoid",
|
38
54
|
"service",
|
55
|
+
"T_ExecutableType",
|
56
|
+
"Runtime",
|
57
|
+
"NullRuntime",
|
58
|
+
"AppServices",
|
59
|
+
"StandardRuntime",
|
60
|
+
"StandardRuntime",
|
61
|
+
"SimpleApplication",
|
39
62
|
]
|
rats/apps/_annotations.py
CHANGED
@@ -1,164 +1,74 @@
|
|
1
|
-
from collections import
|
2
|
-
from
|
3
|
-
from functools import cache
|
4
|
-
from typing import Any, cast
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any, ParamSpec
|
5
3
|
|
6
|
-
from
|
4
|
+
from rats import annotations
|
7
5
|
|
8
|
-
from .
|
9
|
-
from ._ids import ConfigId, ServiceId, T_ConfigType, T_ServiceType
|
6
|
+
from ._ids import ServiceId, T_ServiceType
|
10
7
|
from ._namespaces import ProviderNamespaces
|
8
|
+
from ._scoping import scope_service_name
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
class GroupAnnotations(NamedTuple):
|
16
|
-
"""
|
17
|
-
The list of service ids attached to a given function.
|
18
|
-
|
19
|
-
The `name` attribute is the name of the function, and the `namespace` attribute represents a
|
20
|
-
specific meaning for the group of services.
|
21
|
-
"""
|
22
|
-
|
23
|
-
name: str
|
24
|
-
namespace: str
|
25
|
-
groups: tuple[ServiceId[Any], ...]
|
26
|
-
|
27
|
-
|
28
|
-
class FunctionAnnotations(NamedTuple):
|
29
|
-
"""
|
30
|
-
Holds metadata about the annotated service provider.
|
31
|
-
|
32
|
-
Loosely inspired by: https://peps.python.org/pep-3107/.
|
33
|
-
"""
|
34
|
-
|
35
|
-
providers: tuple[GroupAnnotations, ...]
|
36
|
-
|
37
|
-
def group_in_namespace(
|
38
|
-
self,
|
39
|
-
namespace: str,
|
40
|
-
group_id: ServiceId[T_ServiceType],
|
41
|
-
) -> tuple[GroupAnnotations, ...]:
|
42
|
-
return tuple([x for x in self.with_namespace(namespace) if group_id in x.groups])
|
43
|
-
|
44
|
-
def with_namespace(
|
45
|
-
self,
|
46
|
-
namespace: str,
|
47
|
-
) -> tuple[GroupAnnotations, ...]:
|
48
|
-
return tuple([x for x in self.providers if x.namespace == namespace])
|
49
|
-
|
50
|
-
|
51
|
-
class FunctionAnnotationsBuilder:
|
52
|
-
_service_ids: dict[str, list[ServiceId[Any]]]
|
53
|
-
|
54
|
-
def __init__(self) -> None:
|
55
|
-
self._service_ids = defaultdict(list)
|
56
|
-
|
57
|
-
def add(self, namespace: str, service_id: ServiceId[T_ServiceType]) -> None:
|
58
|
-
self._service_ids[namespace].append(service_id)
|
59
|
-
|
60
|
-
def make(self, name: str) -> tuple[GroupAnnotations, ...]:
|
61
|
-
return tuple(
|
62
|
-
[
|
63
|
-
GroupAnnotations(name=name, namespace=namespace, groups=tuple(services))
|
64
|
-
for namespace, services in self._service_ids.items()
|
65
|
-
]
|
66
|
-
)
|
67
|
-
|
68
|
-
|
69
|
-
class AnnotatedContainer(Container):
|
70
|
-
def get_namespaced_group(
|
71
|
-
self,
|
72
|
-
namespace: str,
|
73
|
-
group_id: ServiceId[T_ServiceType],
|
74
|
-
) -> Iterator[T_ServiceType]:
|
75
|
-
annotations = _extract_class_annotations(type(self))
|
76
|
-
containers = annotations.with_namespace(ProviderNamespaces.CONTAINERS)
|
77
|
-
groups = annotations.group_in_namespace(namespace, group_id)
|
78
|
-
|
79
|
-
for annotation in groups:
|
80
|
-
yield getattr(self, annotation.name)()
|
81
|
-
|
82
|
-
for container in containers:
|
83
|
-
c = getattr(self, container.name)()
|
84
|
-
yield from c.get_namespaced_group(namespace, group_id)
|
10
|
+
P = ParamSpec("P")
|
85
11
|
|
86
12
|
|
87
13
|
def service(
|
88
14
|
service_id: ServiceId[T_ServiceType],
|
89
|
-
) -> Callable[
|
90
|
-
|
15
|
+
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
16
|
+
"""A service is anything you would create instances of?"""
|
17
|
+
return annotations.annotation(ProviderNamespaces.SERVICES, service_id)
|
91
18
|
|
92
19
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
return fn_annotation_decorator(ProviderNamespaces.GROUPS, group_id)
|
20
|
+
def autoid_service(fn: Callable[P, T_ServiceType]) -> Callable[P, T_ServiceType]:
|
21
|
+
_service_id = autoid(fn)
|
22
|
+
return annotations.annotation(ProviderNamespaces.SERVICES, _service_id)(fn)
|
97
23
|
|
98
24
|
|
99
|
-
def
|
100
|
-
|
101
|
-
) -> Callable[
|
102
|
-
|
25
|
+
def group(
|
26
|
+
group_id: ServiceId[T_ServiceType],
|
27
|
+
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
28
|
+
"""A group is a collection of services."""
|
29
|
+
return annotations.annotation(ProviderNamespaces.GROUPS, group_id)
|
103
30
|
|
104
31
|
|
105
32
|
def fallback_service(
|
106
33
|
service_id: ServiceId[T_ServiceType],
|
107
|
-
) -> Callable[
|
108
|
-
|
34
|
+
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
35
|
+
"""A fallback service gets used if no service is defined."""
|
36
|
+
return annotations.annotation(
|
37
|
+
ProviderNamespaces.FALLBACK_SERVICES,
|
38
|
+
service_id,
|
39
|
+
)
|
109
40
|
|
110
41
|
|
111
42
|
def fallback_group(
|
112
43
|
group_id: ServiceId[T_ServiceType],
|
113
|
-
) -> Callable[
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
) -> Callable[..., Callable[..., T_ConfigType]]:
|
120
|
-
return fn_annotation_decorator(ProviderNamespaces.FALLBACK_SERVICES, config_id)
|
44
|
+
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
45
|
+
"""A fallback group gets used if no group is defined."""
|
46
|
+
return annotations.annotation(
|
47
|
+
ProviderNamespaces.FALLBACK_GROUPS,
|
48
|
+
group_id,
|
49
|
+
)
|
121
50
|
|
122
51
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
return fn_annotation_decorator(ProviderNamespaces.CONTAINERS, group_id)
|
127
|
-
|
128
|
-
|
129
|
-
def fn_annotation_decorator(
|
130
|
-
namespace: str,
|
131
|
-
service_id: ServiceId[T_ServiceType],
|
132
|
-
) -> Callable[..., Callable[..., T_ServiceType]]:
|
133
|
-
def wrapper(
|
134
|
-
fn: Callable[..., T_ServiceType],
|
135
|
-
) -> Callable[..., T_ServiceType]:
|
136
|
-
_add_annotation(namespace, fn, service_id)
|
137
|
-
return cache(fn)
|
138
|
-
|
139
|
-
return wrapper
|
140
|
-
|
141
|
-
|
142
|
-
@cache
|
143
|
-
def _extract_class_annotations(cls: Any) -> FunctionAnnotations:
|
144
|
-
function_annotations: list[GroupAnnotations] = []
|
145
|
-
for method_name in dir(cls):
|
146
|
-
if method_name.startswith("_"):
|
147
|
-
continue
|
148
|
-
|
149
|
-
builder = _get_annotations_builder(getattr(cls, method_name))
|
150
|
-
function_annotations.extend(list(builder.make(method_name)))
|
52
|
+
def autoid(method: Callable[..., T_ServiceType]) -> ServiceId[T_ServiceType]:
|
53
|
+
"""
|
54
|
+
Get a service id for a method.
|
151
55
|
|
152
|
-
|
56
|
+
The service id is constructed from the module, class and method name. It should be identical
|
57
|
+
regardless of whether the method is bound or not, and regardless of the instance it is bound
|
58
|
+
to.
|
153
59
|
|
60
|
+
The service type is the return type of the method.
|
61
|
+
"""
|
62
|
+
service_name = _get_method_service_id_name(method)
|
63
|
+
return ServiceId[T_ServiceType](service_name)
|
154
64
|
|
155
|
-
def _add_annotation(namespace: str, fn: Any, service_id: ServiceId[T_ServiceType]) -> None:
|
156
|
-
builder = _get_annotations_builder(fn)
|
157
|
-
builder.add(namespace, service_id)
|
158
65
|
|
66
|
+
def _get_method_service_id_name(method: Callable[..., Any]) -> str:
|
67
|
+
tates = annotations.get_annotations(method).with_namespace(ProviderNamespaces.SERVICES)
|
159
68
|
|
160
|
-
|
161
|
-
|
162
|
-
fn.__rats_service_annotations__ = FunctionAnnotationsBuilder()
|
69
|
+
for a in tates.annotations:
|
70
|
+
return a.groups[0].name
|
163
71
|
|
164
|
-
|
72
|
+
module_name = method.__module__
|
73
|
+
class_name, method_name = method.__qualname__.rsplit(".", 1)
|
74
|
+
return scope_service_name(module_name, class_name, method_name)
|
@@ -1,11 +1,13 @@
|
|
1
1
|
from collections.abc import Iterator
|
2
|
+
from typing import final
|
2
3
|
|
3
4
|
from ._container import Container
|
4
5
|
from ._ids import ServiceId, T_ServiceType
|
5
6
|
|
6
7
|
|
8
|
+
@final
|
7
9
|
class CompositeContainer(Container):
|
8
|
-
|
10
|
+
_containers: tuple[Container, ...]
|
9
11
|
|
10
12
|
def __init__(self, *containers: Container) -> None:
|
11
13
|
self._containers = containers
|
rats/apps/_container.py
CHANGED
@@ -1,10 +1,18 @@
|
|
1
|
+
import abc
|
2
|
+
import logging
|
1
3
|
from abc import abstractmethod
|
2
|
-
from collections.abc import Iterator
|
3
|
-
from typing import Generic, Protocol
|
4
|
+
from collections.abc import Callable, Iterator
|
5
|
+
from typing import Generic, ParamSpec, Protocol
|
4
6
|
|
5
|
-
from
|
7
|
+
from typing_extensions import deprecated
|
8
|
+
|
9
|
+
from rats import annotations
|
10
|
+
|
11
|
+
from ._ids import ServiceId, T_ServiceType, Tco_ServiceType
|
6
12
|
from ._namespaces import ProviderNamespaces
|
7
13
|
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
8
16
|
|
9
17
|
class ServiceProvider(Protocol[Tco_ServiceType]):
|
10
18
|
@abstractmethod
|
@@ -12,22 +20,62 @@ class ServiceProvider(Protocol[Tco_ServiceType]):
|
|
12
20
|
"""Return the service instance."""
|
13
21
|
|
14
22
|
|
15
|
-
class
|
23
|
+
class GroupProvider(Protocol[Tco_ServiceType]):
|
16
24
|
@abstractmethod
|
17
|
-
def __call__(self) ->
|
18
|
-
"""Return the
|
25
|
+
def __call__(self) -> Iterator[Tco_ServiceType]:
|
26
|
+
"""Return the group instances."""
|
19
27
|
|
20
28
|
|
21
29
|
class Container(Protocol):
|
22
|
-
"""
|
30
|
+
"""
|
31
|
+
Main interface for service containers.
|
32
|
+
|
33
|
+
The default methods in this protocol attempt to find service providers that have been
|
34
|
+
annotated.
|
35
|
+
|
36
|
+
Example:
|
37
|
+
.. code-block:: python
|
38
|
+
|
39
|
+
from rats import apps
|
40
|
+
|
41
|
+
|
42
|
+
class MyStorageClient:
|
43
|
+
def save(self, data: str) -> None:
|
44
|
+
print(f"Saving data: {data}")
|
45
|
+
|
46
|
+
|
47
|
+
class MyPluginServices:
|
48
|
+
STORAGE_CLIENT = ServiceId[MyStorageClient]("storage-client")
|
49
|
+
|
50
|
+
|
51
|
+
class MyPluginContainer(apps.Container):
|
52
|
+
@apps.service(MyPluginServices.STORAGE_CLIENT)
|
53
|
+
def _storage_client() -> MyStorageClient:
|
54
|
+
return MyStorageClient()
|
55
|
+
|
56
|
+
|
57
|
+
container = MyPluginContainer()
|
58
|
+
storage_client = container.get(MyPluginServices.STORAGE_CLIENT)
|
59
|
+
storage_client.save("Hello, world!")
|
60
|
+
"""
|
23
61
|
|
24
62
|
def has(self, service_id: ServiceId[T_ServiceType]) -> bool:
|
63
|
+
"""
|
64
|
+
Check if a service is provided by this container.
|
65
|
+
|
66
|
+
Example:
|
67
|
+
.. code-block:: python
|
68
|
+
|
69
|
+
if not container.has(MyPluginServices.STORAGE_CLIENT):
|
70
|
+
print("Did you forget to configure a storage client?")
|
71
|
+
"""
|
25
72
|
try:
|
26
73
|
return self.get(service_id) is not None
|
27
74
|
except ServiceNotFoundError:
|
28
75
|
return False
|
29
76
|
|
30
77
|
def has_group(self, group_id: ServiceId[T_ServiceType]) -> bool:
|
78
|
+
"""Check if a service group has at least one provider in the container."""
|
31
79
|
try:
|
32
80
|
return next(self.get_group(group_id)) is not None
|
33
81
|
except StopIteration:
|
@@ -64,13 +112,61 @@ class Container(Protocol):
|
|
64
112
|
|
65
113
|
yield from self.get_namespaced_group(ProviderNamespaces.GROUPS, group_id)
|
66
114
|
|
67
|
-
@abstractmethod
|
68
115
|
def get_namespaced_group(
|
69
116
|
self,
|
70
117
|
namespace: str,
|
71
118
|
group_id: ServiceId[T_ServiceType],
|
72
119
|
) -> Iterator[T_ServiceType]:
|
73
120
|
"""Retrieve a service group by its id, within a given service namespace."""
|
121
|
+
tates = annotations.get_class_annotations(type(self))
|
122
|
+
containers = tates.with_namespace(ProviderNamespaces.CONTAINERS)
|
123
|
+
groups = tates.with_group(namespace, group_id)
|
124
|
+
|
125
|
+
for annotation in groups.annotations:
|
126
|
+
if not hasattr(self, f"__rats_cache_{annotation.name}"):
|
127
|
+
setattr(self, f"__rats_cache_{annotation.name}", getattr(self, annotation.name)())
|
128
|
+
|
129
|
+
yield getattr(self, f"__rats_cache_{annotation.name}")
|
130
|
+
|
131
|
+
for annotation in containers.annotations:
|
132
|
+
if not hasattr(self, f"__rats_container_cache_{annotation.name}"):
|
133
|
+
setattr(
|
134
|
+
self,
|
135
|
+
f"__rats_container_cache_{annotation.name}",
|
136
|
+
getattr(self, annotation.name)(),
|
137
|
+
)
|
138
|
+
|
139
|
+
c = getattr(self, f"__rats_container_cache_{annotation.name}")
|
140
|
+
yield from c.get_namespaced_group(namespace, group_id)
|
141
|
+
|
142
|
+
|
143
|
+
@deprecated(
|
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__")
|
163
|
+
P = ParamSpec("P")
|
164
|
+
|
165
|
+
|
166
|
+
def container(
|
167
|
+
group_id: ServiceId[T_ServiceType] = DEFAULT_CONTAINER_GROUP,
|
168
|
+
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
|
169
|
+
return annotations.annotation(ProviderNamespaces.CONTAINERS, group_id)
|
74
170
|
|
75
171
|
|
76
172
|
class ServiceNotFoundError(RuntimeError, Generic[T_ServiceType]):
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from abc import abstractmethod
|
2
|
+
from collections.abc import Callable
|
3
|
+
from typing import Protocol
|
4
|
+
|
5
|
+
|
6
|
+
class Executable(Protocol):
|
7
|
+
"""
|
8
|
+
An interface for an executable object.
|
9
|
+
|
10
|
+
One of the lowest level abstractions in the rats-apps library, executables are meant to be
|
11
|
+
easy to run from anywhere, with limited knowledge of the implementation details of the object,
|
12
|
+
by ensuring that the object has an `execute` method with no arguments.
|
13
|
+
"""
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def execute(self) -> None:
|
17
|
+
"""Execute the application."""
|
18
|
+
|
19
|
+
|
20
|
+
class App(Executable):
|
21
|
+
"""
|
22
|
+
Wraps a plain callable objects as an executable.
|
23
|
+
|
24
|
+
This simple object allows for turning any callable object into an executable that is recognized
|
25
|
+
by the rest of the rats application.
|
26
|
+
"""
|
27
|
+
|
28
|
+
_callback: Callable[[], None]
|
29
|
+
|
30
|
+
def __init__(self, callback: Callable[[], None]) -> None:
|
31
|
+
self._callback = callback
|
32
|
+
|
33
|
+
def execute(self) -> None:
|
34
|
+
self._callback()
|
rats/apps/_ids.py
CHANGED
@@ -1,17 +1,13 @@
|
|
1
|
-
import typing
|
2
1
|
from typing import Generic, TypeVar
|
3
2
|
|
4
3
|
from typing_extensions import NamedTuple
|
5
4
|
|
5
|
+
from ._executables import Executable
|
6
|
+
|
6
7
|
T_ServiceType = TypeVar("T_ServiceType")
|
7
|
-
|
8
|
+
T_ExecutableType = TypeVar("T_ExecutableType", bound=Executable)
|
8
9
|
Tco_ServiceType = TypeVar("Tco_ServiceType", covariant=True)
|
9
|
-
Tco_ConfigType = TypeVar("Tco_ConfigType", bound=NamedTuple, covariant=True)
|
10
10
|
|
11
11
|
|
12
12
|
class ServiceId(NamedTuple, Generic[T_ServiceType]):
|
13
13
|
name: str
|
14
|
-
|
15
|
-
|
16
|
-
class ConfigId(ServiceId[T_ConfigType]):
|
17
|
-
pass
|