rats-apps 0.1.3.dev75__py3-none-any.whl → 0.1.3.dev20240624080525__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.
@@ -1,4 +1,3 @@
1
- # type: ignore
2
1
  from __future__ import annotations
3
2
 
4
3
  from collections import defaultdict
@@ -22,14 +21,14 @@ class GroupAnnotations(NamedTuple, Generic[T_GroupType]):
22
21
  groups: tuple[T_GroupType, ...]
23
22
 
24
23
 
25
- class AnnotationsContainer(NamedTuple):
24
+ class AnnotationsContainer(tNamedTuple):
26
25
  """
27
- Holds metadata about the annotated service provider.
26
+ Holds metadata about the annotated functions or class methods.
28
27
 
29
28
  Loosely inspired by: https://peps.python.org/pep-3107/.
30
29
  """
31
30
 
32
- annotations: tuple[GroupAnnotations[...], ...]
31
+ annotations: tuple[GroupAnnotations[Any], ...]
33
32
 
34
33
  @staticmethod
35
34
  def empty() -> AnnotationsContainer:
@@ -38,7 +37,7 @@ class AnnotationsContainer(NamedTuple):
38
37
  def with_group(
39
38
  self,
40
39
  namespace: str,
41
- group_id: T_GroupType,
40
+ group_id: NamedTuple,
42
41
  ) -> AnnotationsContainer:
43
42
  return AnnotationsContainer(
44
43
  annotations=tuple(
@@ -87,6 +86,13 @@ def annotation(
87
86
  namespace: str,
88
87
  group_id: NamedTuple | tNamedTuple,
89
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
+
90
96
  def decorator(fn: DecoratorType) -> DecoratorType:
91
97
  if not hasattr(fn, "__rats_annotations__"):
92
98
  fn.__rats_annotations__ = AnnotationsBuilder() # type: ignore[reportFunctionMemberAccess]
@@ -100,14 +106,22 @@ def annotation(
100
106
 
101
107
  @cache
102
108
  def get_class_annotations(cls: type) -> AnnotationsContainer:
103
- tates = []
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]] = []
104
117
 
105
118
  for method_name in dir(cls):
106
119
  method = getattr(cls, method_name)
107
120
  if not hasattr(method, "__rats_annotations__"):
108
121
  continue
109
122
 
110
- tates.extend(method.__rats_annotations__.make(method_name).annotations)
123
+ builder: AnnotationsBuilder = method.__rats_annotations__
124
+ tates.extend(builder.make(method_name).annotations)
111
125
 
112
126
  return AnnotationsContainer(annotations=tuple(tates))
113
127
 
@@ -116,6 +130,12 @@ P = ParamSpec("P")
116
130
 
117
131
 
118
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
+ """
119
139
  builder: AnnotationsBuilder = getattr(
120
140
  fn,
121
141
  "__rats_annotations__",
rats/apps/__init__.py CHANGED
@@ -1,16 +1,14 @@
1
1
  """
2
- Provides a small set of libraries to help create new applications.
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
- AnnotatedContainer,
10
9
  autoid,
11
10
  autoid_service,
12
11
  config,
13
- container,
14
12
  fallback_config,
15
13
  fallback_group,
16
14
  fallback_service,
@@ -19,23 +17,26 @@ from ._annotations import (
19
17
  )
20
18
  from ._composite_container import CompositeContainer
21
19
  from ._container import (
20
+ AnnotatedContainer, # type: ignore[reportDeprecated]
22
21
  ConfigProvider,
23
22
  Container,
24
23
  DuplicateServiceError,
25
24
  ServiceNotFoundError,
26
25
  ServiceProvider,
26
+ container,
27
27
  )
28
- from ._executables import App, AppContainer, Executable
28
+ from ._executables import App, Executable
29
29
  from ._ids import ConfigId, ServiceId
30
30
  from ._namespaces import ProviderNamespaces
31
31
  from ._plugin_container import PluginContainers
32
32
  from ._plugins import PluginRunner
33
+ from ._runtimes import Runtime, T_ExecutableType
33
34
  from ._scoping import autoscope
35
+ from ._simple_apps import AppServices, SimpleApplication, SimpleRuntime
34
36
 
35
37
  __all__ = [
36
38
  "AnnotatedContainer",
37
39
  "App",
38
- "AppContainer",
39
40
  "CompositeContainer",
40
41
  "ConfigId",
41
42
  "Container",
@@ -58,4 +59,10 @@ __all__ = [
58
59
  "group",
59
60
  "autoid",
60
61
  "service",
62
+ "T_ExecutableType",
63
+ "Runtime",
64
+ "AppServices",
65
+ "SimpleRuntime",
66
+ "SimpleRuntime",
67
+ "SimpleApplication",
61
68
  ]
rats/apps/_annotations.py CHANGED
@@ -1,157 +1,72 @@
1
- import abc
2
- from collections import defaultdict
3
- from collections.abc import Callable, Iterator
4
- from functools import cache
5
- from typing import Any, ParamSpec, cast
1
+ from collections.abc import Callable
2
+ from typing import Any, ParamSpec
6
3
 
7
- from typing_extensions import NamedTuple
4
+ from rats import annotations
8
5
 
9
- from ._container import Container
10
6
  from ._ids import ConfigId, ServiceId, T_ConfigType, T_ServiceType
11
7
  from ._namespaces import ProviderNamespaces
12
8
  from ._scoping import scope_service_name
13
9
 
14
- DEFAULT_CONTAINER_GROUP = ServiceId[Container]("__default__")
15
-
16
-
17
- class GroupAnnotations(NamedTuple):
18
- """
19
- The list of service ids attached to a given function.
20
-
21
- The `name` attribute is the name of the function, and the `namespace` attribute represents a
22
- specific meaning for the group of services.
23
- """
24
-
25
- name: str
26
- namespace: str
27
- groups: tuple[ServiceId[Any], ...]
28
-
29
-
30
- class FunctionAnnotations(NamedTuple):
31
- """
32
- Holds metadata about the annotated service provider.
33
-
34
- Loosely inspired by: https://peps.python.org/pep-3107/.
35
- """
36
-
37
- providers: tuple[GroupAnnotations, ...]
38
-
39
- def group_in_namespace(
40
- self,
41
- namespace: str,
42
- group_id: ServiceId[T_ServiceType],
43
- ) -> tuple[GroupAnnotations, ...]:
44
- return tuple([x for x in self.with_namespace(namespace) if group_id in x.groups])
45
-
46
- def with_namespace(
47
- self,
48
- namespace: str,
49
- ) -> tuple[GroupAnnotations, ...]:
50
- return tuple([x for x in self.providers if x.namespace == namespace])
51
-
52
-
53
- class FunctionAnnotationsBuilder:
54
- _service_ids: dict[str, list[ServiceId[Any]]]
55
-
56
- def __init__(self) -> None:
57
- self._service_ids = defaultdict(list)
58
-
59
- def add(self, namespace: str, service_id: ServiceId[T_ServiceType]) -> None:
60
- self._service_ids[namespace].append(service_id)
61
-
62
- def get_service_names(self, namespace: str) -> tuple[str, ...]:
63
- return tuple(s.name for s in self._service_ids.get(namespace, []))
64
-
65
- def make(self, name: str) -> tuple[GroupAnnotations, ...]:
66
- return tuple(
67
- [
68
- GroupAnnotations(name=name, namespace=namespace, groups=tuple(services))
69
- for namespace, services in self._service_ids.items()
70
- ]
71
- )
72
-
73
-
74
- class AnnotatedContainer(Container, abc.ABC):
75
- def get_namespaced_group(
76
- self,
77
- namespace: str,
78
- group_id: ServiceId[T_ServiceType],
79
- ) -> Iterator[T_ServiceType]:
80
- annotations = _extract_class_annotations(type(self))
81
- containers = annotations.with_namespace(ProviderNamespaces.CONTAINERS)
82
- groups = annotations.group_in_namespace(namespace, group_id)
83
-
84
- for annotation in groups:
85
- yield getattr(self, annotation.name)()
86
-
87
- for container in containers:
88
- c = getattr(self, container.name)()
89
- yield from c.get_namespaced_group(namespace, group_id)
90
-
91
-
92
10
  P = ParamSpec("P")
93
11
 
94
12
 
95
13
  def service(
96
14
  service_id: ServiceId[T_ServiceType],
97
15
  ) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
98
- return fn_annotation_decorator(ProviderNamespaces.SERVICES, service_id)
16
+ """A service is anything you would create instances of?"""
17
+ return annotations.annotation(ProviderNamespaces.SERVICES, service_id)
99
18
 
100
19
 
101
20
  def autoid_service(fn: Callable[P, T_ServiceType]) -> Callable[P, T_ServiceType]:
102
21
  _service_id = autoid(fn)
103
- _add_annotation(ProviderNamespaces.SERVICES, fn, _service_id)
104
- cached_fn = cache(fn)
105
- return cast(Callable[P, T_ServiceType], cached_fn)
22
+ return annotations.annotation(ProviderNamespaces.SERVICES, _service_id)(fn)
106
23
 
107
24
 
108
25
  def group(
109
26
  group_id: ServiceId[T_ServiceType],
110
27
  ) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
111
- return fn_annotation_decorator(ProviderNamespaces.GROUPS, group_id)
28
+ """A group is a collection of services."""
29
+ return annotations.annotation(ProviderNamespaces.GROUPS, group_id)
112
30
 
113
31
 
114
32
  def config(
115
33
  config_id: ConfigId[T_ConfigType],
116
34
  ) -> Callable[[Callable[P, T_ConfigType]], Callable[P, T_ConfigType]]:
117
- return fn_annotation_decorator(ProviderNamespaces.SERVICES, config_id)
35
+ """A service that provides simple data-structures."""
36
+ return annotations.annotation(
37
+ ProviderNamespaces.SERVICES,
38
+ config_id,
39
+ )
118
40
 
119
41
 
120
42
  def fallback_service(
121
43
  service_id: ServiceId[T_ServiceType],
122
44
  ) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
123
- return fn_annotation_decorator(ProviderNamespaces.FALLBACK_SERVICES, service_id)
45
+ """A fallback service gets used if no service is defined."""
46
+ return annotations.annotation(
47
+ ProviderNamespaces.FALLBACK_SERVICES,
48
+ service_id,
49
+ )
124
50
 
125
51
 
126
52
  def fallback_group(
127
53
  group_id: ServiceId[T_ServiceType],
128
54
  ) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
129
- return fn_annotation_decorator(ProviderNamespaces.FALLBACK_GROUPS, group_id)
55
+ """A fallback group gets used if no group is defined."""
56
+ return annotations.annotation(
57
+ ProviderNamespaces.FALLBACK_GROUPS,
58
+ group_id,
59
+ )
130
60
 
131
61
 
132
62
  def fallback_config(
133
63
  config_id: ConfigId[T_ConfigType],
134
64
  ) -> Callable[[Callable[P, T_ConfigType]], Callable[P, T_ConfigType]]:
135
- return fn_annotation_decorator(ProviderNamespaces.FALLBACK_SERVICES, config_id)
136
-
137
-
138
- def container(
139
- group_id: ServiceId[T_ServiceType] = DEFAULT_CONTAINER_GROUP,
140
- ) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
141
- return fn_annotation_decorator(ProviderNamespaces.CONTAINERS, group_id)
142
-
143
-
144
- def _get_method_service_id_name(method: Callable[..., Any]) -> str:
145
- existing_names = _get_annotations_builder(method).get_service_names(
146
- ProviderNamespaces.SERVICES
65
+ """A fallback config gets used if no config is defined."""
66
+ return annotations.annotation(
67
+ ProviderNamespaces.FALLBACK_SERVICES,
68
+ config_id,
147
69
  )
148
- if existing_names:
149
- return existing_names[0]
150
- else:
151
- module_name = method.__module__
152
- class_name, method_name = method.__qualname__.rsplit(".", 1)
153
- service_name = scope_service_name(module_name, class_name, method_name)
154
- return service_name
155
70
 
156
71
 
157
72
  def autoid(method: Callable[..., T_ServiceType]) -> ServiceId[T_ServiceType]:
@@ -168,43 +83,12 @@ def autoid(method: Callable[..., T_ServiceType]) -> ServiceId[T_ServiceType]:
168
83
  return ServiceId[T_ServiceType](service_name)
169
84
 
170
85
 
171
- def fn_annotation_decorator(
172
- namespace: str,
173
- service_id: ServiceId[T_ServiceType],
174
- ) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
175
- def wrapper(
176
- fn: Callable[P, T_ServiceType],
177
- ) -> Callable[P, T_ServiceType]:
178
- _service_id = service_id
179
- _add_annotation(namespace, fn, _service_id)
180
- cached_fn = cache(fn)
181
- # The static type of cached_fn should be correct, but it does not maintain the param-spec,
182
- # so we need to cast.
183
- return cast(Callable[P, T_ServiceType], cached_fn)
184
-
185
- return wrapper
186
-
187
-
188
- @cache
189
- def _extract_class_annotations(cls: Any) -> FunctionAnnotations:
190
- function_annotations: list[GroupAnnotations] = []
191
- for method_name in dir(cls):
192
- if method_name.startswith("_"):
193
- continue
194
-
195
- builder = _get_annotations_builder(getattr(cls, method_name))
196
- function_annotations.extend(list(builder.make(method_name)))
197
-
198
- return FunctionAnnotations(tuple(function_annotations))
199
-
200
-
201
- def _add_annotation(namespace: str, fn: Any, service_id: ServiceId[T_ServiceType]) -> None:
202
- builder = _get_annotations_builder(fn)
203
- builder.add(namespace, service_id)
204
-
86
+ def _get_method_service_id_name(method: Callable[..., Any]) -> str:
87
+ tates = annotations.get_annotations(method).with_namespace(ProviderNamespaces.SERVICES)
205
88
 
206
- def _get_annotations_builder(fn: Any) -> FunctionAnnotationsBuilder:
207
- if not hasattr(fn, "__rats_service_annotations__"):
208
- fn.__rats_service_annotations__ = FunctionAnnotationsBuilder()
89
+ for a in tates.annotations:
90
+ return a.groups[0].name
209
91
 
210
- return cast(FunctionAnnotationsBuilder, fn.__rats_service_annotations__)
92
+ module_name = method.__module__
93
+ class_name, method_name = method.__qualname__.rsplit(".", 1)
94
+ return scope_service_name(module_name, class_name, method_name)
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
6
+
7
+ from typing_extensions import deprecated
8
+
9
+ from rats import annotations
4
10
 
5
11
  from ._ids import ServiceId, T_ServiceType, Tco_ConfigType, 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
@@ -19,15 +27,55 @@ class ConfigProvider(ServiceProvider[Tco_ConfigType], Protocol[Tco_ConfigType]):
19
27
 
20
28
 
21
29
  class Container(Protocol):
22
- """Main interface for service containers."""
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]):
rats/apps/_executables.py CHANGED
@@ -1,19 +1,30 @@
1
1
  from abc import abstractmethod
2
- from collections.abc import Callable, Iterator
3
- from functools import cache
2
+ from collections.abc import Callable
4
3
  from typing import Protocol
5
4
 
6
- from ._container import Container, ServiceId
7
- from ._ids import T_ServiceType
8
-
9
5
 
10
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
+
11
15
  @abstractmethod
12
16
  def execute(self) -> None:
13
17
  """Execute the application."""
14
18
 
15
19
 
16
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
+
17
28
  _callback: Callable[[], None]
18
29
 
19
30
  def __init__(self, callback: Callable[[], None]) -> None:
@@ -23,19 +34,19 @@ class App(Executable):
23
34
  self._callback()
24
35
 
25
36
 
26
- class AppContainer(Container):
27
- _container: Callable[[Container], Container]
28
-
29
- def __init__(self, container: Callable[[Container], Container]) -> None:
30
- self._container = container
31
-
32
- def get_namespaced_group(
33
- self,
34
- namespace: str,
35
- group_id: ServiceId[T_ServiceType],
36
- ) -> Iterator[T_ServiceType]:
37
- yield from self._load_container().get_namespaced_group(namespace, group_id)
38
-
39
- @cache # noqa: B019
40
- def _load_container(self) -> Container:
41
- return self._container(self)
37
+ # class AppContainer(Container):
38
+ # _container: Callable[[Container], Container]
39
+ #
40
+ # def __init__(self, container: Callable[[Container], Container]) -> None:
41
+ # self._container = container
42
+ #
43
+ # def get_namespaced_group(
44
+ # self,
45
+ # namespace: str,
46
+ # group_id: ServiceId[T_ServiceType],
47
+ # ) -> Iterator[T_ServiceType]:
48
+ # yield from self._load_container().get_namespaced_group(namespace, group_id)
49
+ #
50
+ # @cache
51
+ # def _load_container(self) -> Container:
52
+ # return self._container(self)
@@ -9,10 +9,12 @@ from ._ids import ServiceId, T_ServiceType
9
9
  class PluginContainers(Container):
10
10
  _app: Container
11
11
  _group: str
12
+ _names: tuple[str, ...]
12
13
 
13
- def __init__(self, app: Container, group: str) -> None:
14
+ def __init__(self, app: Container, group: str, *names: str) -> None:
14
15
  self._app = app
15
16
  self._group = group
17
+ self._names = names
16
18
 
17
19
  def get_namespaced_group(
18
20
  self,
@@ -25,4 +27,7 @@ class PluginContainers(Container):
25
27
  @cache # noqa: B019
26
28
  def _load_containers(self) -> Iterable[Container]:
27
29
  entries = entry_points(group=self._group)
28
- return tuple(entry.load()(self._app) for entry in entries)
30
+ return tuple(entry.load()(self._app) for entry in entries if self._is_enabled(entry.name))
31
+
32
+ def _is_enabled(self, plugin_name: str) -> bool:
33
+ return len(self._names) == 0 or plugin_name in self._names
rats/apps/_plugins.py CHANGED
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from collections.abc import Callable, Iterator
2
3
  from typing import Generic, TypeVar
3
4
 
@@ -13,5 +14,10 @@ class PluginRunner(Generic[T_PluginType]):
13
14
  self._plugins = plugins
14
15
 
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
+ )
16
22
  for plugin in self._plugins:
17
23
  handler(plugin)
rats/apps/_runtimes.py ADDED
@@ -0,0 +1,32 @@
1
+ from abc import abstractmethod
2
+ from collections.abc import Callable
3
+ from typing import Protocol, TypeVar
4
+
5
+ from ._executables import Executable
6
+ from ._ids import ServiceId
7
+
8
+ T_ExecutableType = TypeVar("T_ExecutableType", bound=Executable)
9
+
10
+
11
+ class Runtime(Protocol):
12
+ @abstractmethod
13
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
14
+ """Execute a list of executables sequentially."""
15
+
16
+ @abstractmethod
17
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
18
+ """
19
+ Execute one or more groups of executables sequentially.
20
+
21
+ Although each group is expected to be executed sequentially, the groups themselves are not
22
+ executed in a deterministic order. Runtime implementations are free to execute groups in
23
+ parallel or in any order that is convenient.
24
+ """
25
+
26
+ @abstractmethod
27
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
28
+ """
29
+ Execute provided callables by automatically turning them into apps.Executable objects.
30
+
31
+ The used ServiceId is determined by the Runtime implementation.
32
+ """
rats/apps/_scoping.py CHANGED
@@ -8,10 +8,6 @@ T = TypeVar("T")
8
8
  P = ParamSpec("P")
9
9
 
10
10
 
11
- def scope_service_name(module_name: str, cls_name: str, name: str) -> str:
12
- return f"{module_name}:{cls_name}[{name}]"
13
-
14
-
15
11
  def autoscope(cls: type[T]) -> type[T]:
16
12
  """
17
13
  Decorator that replaces all ServiceId instances in the class with scoped ServiceId instances.
@@ -45,3 +41,7 @@ def autoscope(cls: type[T]) -> type[T]:
45
41
  setattr(cls, prop_name, prop)
46
42
 
47
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,144 @@
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
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
+ CONTAINER = ServiceId[Container]("app-container")
60
+ CALLABLE_EXE_CTX = ServiceId[ExecutableCallableContext]("callable-exe-ctx")
61
+ CALLABLE_EXE = ServiceId[Executable]("callable-exe")
62
+
63
+
64
+ @final
65
+ class SimpleRuntime(Runtime):
66
+ """A simple runtime that executes sequentially and in a single thread."""
67
+
68
+ _app: Container
69
+
70
+ def __init__(self, app: Container) -> None:
71
+ self._app = app
72
+
73
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
74
+ for exe_id in exe_ids:
75
+ self._app.get(exe_id).execute()
76
+
77
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
78
+ for exe_group_id in exe_group_ids:
79
+ for exe in self._app.get_group(exe_group_id):
80
+ exe.execute()
81
+
82
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
83
+ ctx: ExecutableCallableContext = self._app.get(AppServices.CALLABLE_EXE_CTX)
84
+ for cb in callables:
85
+ with ctx.open_callable(cb) as exe:
86
+ exe.execute()
87
+
88
+
89
+ @final
90
+ class SimpleApplication(Runtime, Container):
91
+ """An application without anything fancy."""
92
+
93
+ _plugin_groups: Iterable[str]
94
+
95
+ def __init__(self, *plugin_groups: str) -> None:
96
+ self._plugin_groups = plugin_groups
97
+
98
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
99
+ self._runtime().execute(*exe_ids)
100
+
101
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
102
+ self._runtime().execute_group(*exe_group_ids)
103
+
104
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
105
+ for cb in callables:
106
+ self._runtime().execute_callable(cb)
107
+
108
+ @fallback_service(AppServices.RUNTIME)
109
+ def _runtime(self) -> Runtime:
110
+ """
111
+ The default runtime is an instance of SimpleRuntime.
112
+
113
+ Define a non-fallback service to override this default implementation.
114
+ """
115
+ return SimpleRuntime(self)
116
+
117
+ @fallback_service(AppServices.CALLABLE_EXE)
118
+ def _callable_exe(self) -> Executable:
119
+ """We use the callable exe ctx here, so we can treat it like any other app downstream."""
120
+ return self.get(AppServices.CALLABLE_EXE_CTX)
121
+
122
+ @fallback_service(AppServices.CALLABLE_EXE_CTX)
123
+ def _callable_exe_ctx(self) -> ExecutableCallableContext:
124
+ """
125
+ The default executable context client for executing raw callables.
126
+
127
+ Define a non-fallback service to override this default implementation.
128
+ """
129
+ return ExecutableCallableContext(AppServices.CALLABLE_EXE)
130
+
131
+ @fallback_service(AppServices.CONTAINER)
132
+ def _container(self) -> Container:
133
+ """
134
+ The default container is the root application instance.
135
+
136
+ Define a non-fallback service to override this default implementation.
137
+ """
138
+ return self
139
+
140
+ @container()
141
+ def _plugins(self) -> Container:
142
+ return CompositeContainer(
143
+ *[PluginContainers(self, group) for group in self._plugin_groups],
144
+ )
rats/cli/__init__.py CHANGED
@@ -1,14 +1,17 @@
1
- """Uses `rats.annotations` to streamline the creation of CLI commands written with Click."""
1
+ """Uses `rats.cli` to streamline the creation of CLI commands written with Click."""
2
2
 
3
3
  from ._annotations import CommandId, command, group
4
4
  from ._click import ClickCommandGroup, ClickCommandMapper
5
5
  from ._executable import ClickExecutable
6
+ from ._plugin import PluginContainer, PluginServices
6
7
  from ._plugins import AttachClickCommands, AttachClickGroup, ClickGroupPlugin, CommandContainer
7
8
 
8
9
  __all__ = [
9
10
  "CommandId",
11
+ "PluginContainer",
10
12
  "command",
11
13
  "group",
14
+ "PluginServices",
12
15
  "ClickCommandMapper",
13
16
  "ClickExecutable",
14
17
  "ClickGroupPlugin",
rats/cli/_plugin.py ADDED
@@ -0,0 +1,69 @@
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
+ # autowrapped!
40
+ return cast(apps.ServiceId[click.Group], cmd_id)
41
+
42
+
43
+ @final
44
+ class PluginContainer(apps.Container):
45
+ _app: apps.Container
46
+
47
+ def __init__(self, app: apps.Container) -> None:
48
+ self._app = app
49
+
50
+ @apps.service(PluginServices.ROOT_COMMAND)
51
+ def _root_command(self) -> apps.Executable:
52
+ def run() -> None:
53
+ runtime = self._app.get(apps.AppServices.RUNTIME)
54
+ runtime.execute_group(
55
+ PluginServices.EVENTS.command_open(PluginServices.ROOT_COMMAND),
56
+ PluginServices.EVENTS.command_execute(PluginServices.ROOT_COMMAND),
57
+ PluginServices.EVENTS.command_close(PluginServices.ROOT_COMMAND),
58
+ )
59
+
60
+ return apps.App(run)
61
+
62
+ @apps.fallback_group(PluginServices.EVENTS.command_execute(PluginServices.ROOT_COMMAND))
63
+ def _default_command(self) -> apps.Executable:
64
+ group = self._app.get(PluginServices.click_command(PluginServices.ROOT_COMMAND))
65
+ return apps.App(lambda: group())
66
+
67
+ @apps.service(PluginServices.click_command(PluginServices.ROOT_COMMAND))
68
+ def _root_click_command(self) -> click.Group:
69
+ return click.Group("groot")
rats/logs/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Small package to help configure logging for rats applications."""
2
+
3
+ from ._plugin import PluginContainer, PluginServices
4
+
5
+ __all__ = [
6
+ "PluginServices",
7
+ "PluginContainer",
8
+ ]
rats/logs/_plugin.py ADDED
@@ -0,0 +1,60 @@
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
+ "root": {"level": "INFO", "handlers": ["console"]},
58
+ }
59
+ )
60
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rats-apps
3
- Version: 0.1.3.dev75
3
+ Version: 0.1.3.dev20240624080525
4
4
  Summary: research analysis tools for building applications
5
5
  Home-page: https://github.com/microsoft/rats/
6
6
  License: MIT
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: click
15
+ Requires-Dist: colorlog
15
16
  Requires-Dist: typing_extensions
16
17
  Project-URL: Documentation, https://microsoft.github.io/rats/
17
18
  Project-URL: Repository, https://github.com/microsoft/rats/
@@ -0,0 +1,29 @@
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=yvwCwhSZrPre0_SNESf0asSrabZrru9P_JteKcljemU,1611
5
+ rats/apps/_annotations.py,sha256=2dvq0ZXDUMC9LK4athRzHrVlx23rGYtw4Gn0aNA9aSk,3063
6
+ rats/apps/_composite_container.py,sha256=wSWVQWPin2xxIlEoSgk_D1rlc3N2gpTxQ2y9UFdqXy0,553
7
+ rats/apps/_container.py,sha256=tIpU4o1nK5tQaOKZHh35qGBw6TV4uuU0k5vDY6NwS3Y,6280
8
+ rats/apps/_executables.py,sha256=acwOXlWpGmLuSFr30dPzg2LU6-ae1yfOtbDRTIXLEjc,1520
9
+ rats/apps/_ids.py,sha256=dxWCPMpMA_vpaTDJEKNByIBJaX97Db203XqWLhaOezo,457
10
+ rats/apps/_namespaces.py,sha256=THUV_Xj5PtweC23Ob-zsSpk8exC4fT-qRwjpQ6IDm0U,188
11
+ rats/apps/_plugin_container.py,sha256=Vhh1IdCzpUVwRdh301sALX7DQKny7Hru-yF_GwdxVqc,1075
12
+ rats/apps/_plugins.py,sha256=mvSYQPi11wTGZpM4umoyD37Rc8CQX8nt5eAJbmLrBFM,688
13
+ rats/apps/_runtimes.py,sha256=r6aRoI4rAkUh0uv3IKn-rNuwEu2MmB724rHsJpHDO74,1138
14
+ rats/apps/_scoping.py,sha256=EIUopw3b38CEV57kCmSKQTAnQQMcXHZ_vwtk9t0K__g,1453
15
+ rats/apps/_simple_apps.py,sha256=wjrPUt2_GLnp065gKSfIgG_3aLAU9ixi9rRt2d9mIJw,4742
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=iUf2-ScR8o5p3g-0M1qNTAb3l9Q-FIBVjT_HEskISJY,2301
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=j3hLJDcdF4DvxeZ84zdMGP4EHgfMEzZc0wBGQjvrzGc,2039
26
+ rats_apps-0.1.3.dev20240624080525.dist-info/METADATA,sha256=Vy9kUx6YxdnFpwc9rhhw6toJ3F6LiVfDR5_oH3_QcT4,774
27
+ rats_apps-0.1.3.dev20240624080525.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
28
+ rats_apps-0.1.3.dev20240624080525.dist-info/entry_points.txt,sha256=9oOvf2loQr5ACWQgvuu9Q3KZIVIxKE5Aa-rLuUII5WQ,91
29
+ rats_apps-0.1.3.dev20240624080525.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ [rats.apps.plugins]
2
+ rats.cli=rats.cli:PluginContainer
3
+ rats.logs=rats.logs:PluginContainer
4
+
@@ -1,23 +0,0 @@
1
- rats/annotations/__init__.py,sha256=wsGhRQzZrV2oJTnBAX0aGgpyT1kYT235jkP3Wb8BTRY,498
2
- rats/annotations/_functions.py,sha256=r4Y-9fOZ9M0lDpcIv8nMuggEffc2bgZLeubWRj8uj-o,3552
3
- rats/annotations/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- rats/apps/__init__.py,sha256=e9RQPGGgm3IG1XZgaK4e2u18-jQ4D9Seo7vpGgwuD0k,1340
5
- rats/apps/_annotations.py,sha256=LOuVckzeUmbDWko8tB2HpACc_xdLoldeNJpAgvclf-s,6991
6
- rats/apps/_composite_container.py,sha256=wSWVQWPin2xxIlEoSgk_D1rlc3N2gpTxQ2y9UFdqXy0,553
7
- rats/apps/_container.py,sha256=5uiCyxN6HS2z97XcTOFP-t72cNoB1U1sJMkMcfSfDps,3129
8
- rats/apps/_executables.py,sha256=8ITn__pjTLHo7FEb-3C6ZQrs1mow0gZn6d-24XGBSu8,1079
9
- rats/apps/_ids.py,sha256=dxWCPMpMA_vpaTDJEKNByIBJaX97Db203XqWLhaOezo,457
10
- rats/apps/_namespaces.py,sha256=THUV_Xj5PtweC23Ob-zsSpk8exC4fT-qRwjpQ6IDm0U,188
11
- rats/apps/_plugin_container.py,sha256=W_xQD2btc0N2dEb3c5tXM-ZZ4A4diMpkCjbOZdlXYuI,853
12
- rats/apps/_plugins.py,sha256=i1K5dCRC9cRA5QLiIdVUDJNM2rG935fdvqSTAK49h38,499
13
- rats/apps/_scoping.py,sha256=plSVEq3rJ8JFAu2epVg2NQpuTbpSTA3a0Tha_DwJL_Y,1453
14
- rats/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- rats/cli/__init__.py,sha256=LJe3zGI4IH2Siti4kQjqHOrxygv03g3hDE7jwpfYS9E,574
16
- rats/cli/_annotations.py,sha256=0dI8hu_y754Y53Pka1-mGEgHjjVcnIOGd8l1SFx8OBY,1190
17
- rats/cli/_click.py,sha256=7-ClnYSW4poFr_B-Q6NT45DnMF1XL7ntUgwQqQ7q_eo,1036
18
- rats/cli/_executable.py,sha256=kAQ9hImv3hBaScu6e19o_BMvl7tdYJql38E76S3FjSk,580
19
- rats/cli/_plugins.py,sha256=H3-QdaICPJhCC5FkLHdXpwqe7Z0mpvsenakhNiPllC8,2739
20
- rats/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- rats_apps-0.1.3.dev75.dist-info/METADATA,sha256=LLwWeGo0LQUWWTvwzATROfpAPux4cSLOydQcLHcYK8o,738
22
- rats_apps-0.1.3.dev75.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
23
- rats_apps-0.1.3.dev75.dist-info/RECORD,,