rats-apps 0.1.2__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/apps/__init__.py +39 -0
- rats/apps/_annotations.py +164 -0
- rats/apps/_composite_container.py +19 -0
- rats/apps/_container.py +89 -0
- rats/apps/_ids.py +17 -0
- rats/apps/_namespaces.py +6 -0
- rats/apps/_plugin_container.py +28 -0
- rats/apps/_scoping.py +43 -0
- rats/apps/py.typed +0 -0
- rats_apps-0.1.2.dist-info/METADATA +21 -0
- rats_apps-0.1.2.dist-info/RECORD +13 -0
- rats_apps-0.1.2.dist-info/WHEEL +4 -0
- rats_apps-0.1.2.dist-info/entry_points.txt +3 -0
rats/apps/__init__.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
"""
|
2
|
+
Provides a small set of libraries to help create new applications.
|
3
|
+
|
4
|
+
Applications give you the ability to define a development experience to match your project's
|
5
|
+
domain.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from ._annotations import (
|
9
|
+
AnnotatedContainer,
|
10
|
+
config,
|
11
|
+
container,
|
12
|
+
fallback_config,
|
13
|
+
fallback_group,
|
14
|
+
fallback_service,
|
15
|
+
group,
|
16
|
+
service,
|
17
|
+
)
|
18
|
+
from ._container import Container, DuplicateServiceError, ServiceNotFoundError
|
19
|
+
from ._ids import ConfigId, ServiceId
|
20
|
+
from ._namespaces import ProviderNamespaces
|
21
|
+
from ._scoping import autoscope
|
22
|
+
|
23
|
+
__all__ = [
|
24
|
+
"AnnotatedContainer",
|
25
|
+
"ConfigId",
|
26
|
+
"Container",
|
27
|
+
"DuplicateServiceError",
|
28
|
+
"ProviderNamespaces",
|
29
|
+
"ServiceId",
|
30
|
+
"ServiceNotFoundError",
|
31
|
+
"autoscope",
|
32
|
+
"config",
|
33
|
+
"container",
|
34
|
+
"fallback_config",
|
35
|
+
"fallback_group",
|
36
|
+
"fallback_service",
|
37
|
+
"group",
|
38
|
+
"service",
|
39
|
+
]
|
@@ -0,0 +1,164 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from collections.abc import Callable, Iterator
|
3
|
+
from functools import cache
|
4
|
+
from typing import Any, cast
|
5
|
+
|
6
|
+
from typing_extensions import NamedTuple
|
7
|
+
|
8
|
+
from ._container import Container
|
9
|
+
from ._ids import ConfigId, ServiceId, T_ConfigType, T_ServiceType
|
10
|
+
from ._namespaces import ProviderNamespaces
|
11
|
+
|
12
|
+
DEFAULT_CONTAINER_GROUP = ServiceId[Container]("__default__")
|
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)
|
85
|
+
|
86
|
+
|
87
|
+
def service(
|
88
|
+
service_id: ServiceId[T_ServiceType],
|
89
|
+
) -> Callable[..., Callable[..., T_ServiceType]]:
|
90
|
+
return fn_annotation_decorator(ProviderNamespaces.SERVICES, service_id)
|
91
|
+
|
92
|
+
|
93
|
+
def group(
|
94
|
+
group_id: ServiceId[T_ServiceType],
|
95
|
+
) -> Callable[..., Callable[..., T_ServiceType]]:
|
96
|
+
return fn_annotation_decorator(ProviderNamespaces.GROUPS, group_id)
|
97
|
+
|
98
|
+
|
99
|
+
def config(
|
100
|
+
config_id: ConfigId[T_ConfigType],
|
101
|
+
) -> Callable[..., Callable[..., T_ConfigType]]:
|
102
|
+
return fn_annotation_decorator(ProviderNamespaces.SERVICES, config_id)
|
103
|
+
|
104
|
+
|
105
|
+
def fallback_service(
|
106
|
+
service_id: ServiceId[T_ServiceType],
|
107
|
+
) -> Callable[..., Callable[..., T_ServiceType]]:
|
108
|
+
return fn_annotation_decorator(ProviderNamespaces.FALLBACK_SERVICES, service_id)
|
109
|
+
|
110
|
+
|
111
|
+
def fallback_group(
|
112
|
+
group_id: ServiceId[T_ServiceType],
|
113
|
+
) -> Callable[..., Callable[..., T_ServiceType]]:
|
114
|
+
return fn_annotation_decorator(ProviderNamespaces.FALLBACK_GROUPS, group_id)
|
115
|
+
|
116
|
+
|
117
|
+
def fallback_config(
|
118
|
+
config_id: ConfigId[T_ConfigType],
|
119
|
+
) -> Callable[..., Callable[..., T_ConfigType]]:
|
120
|
+
return fn_annotation_decorator(ProviderNamespaces.FALLBACK_SERVICES, config_id)
|
121
|
+
|
122
|
+
|
123
|
+
def container(
|
124
|
+
group_id: ServiceId[T_ServiceType] = DEFAULT_CONTAINER_GROUP,
|
125
|
+
) -> Callable[..., Callable[..., T_ServiceType]]:
|
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)))
|
151
|
+
|
152
|
+
return FunctionAnnotations(tuple(function_annotations))
|
153
|
+
|
154
|
+
|
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
|
+
|
159
|
+
|
160
|
+
def _get_annotations_builder(fn: Any) -> FunctionAnnotationsBuilder:
|
161
|
+
if not hasattr(fn, "__rats_service_annotations__"):
|
162
|
+
fn.__rats_service_annotations__ = FunctionAnnotationsBuilder()
|
163
|
+
|
164
|
+
return cast(FunctionAnnotationsBuilder, fn.__rats_service_annotations__)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from collections.abc import Iterator
|
2
|
+
|
3
|
+
from ._container import Container
|
4
|
+
from ._ids import ServiceId, T_ServiceType
|
5
|
+
|
6
|
+
|
7
|
+
class CompositeContainer(Container):
|
8
|
+
_contailers: tuple[Container, ...]
|
9
|
+
|
10
|
+
def __init__(self, *containers: Container) -> None:
|
11
|
+
self._containers = containers
|
12
|
+
|
13
|
+
def get_namespaced_group(
|
14
|
+
self,
|
15
|
+
namespace: str,
|
16
|
+
group_id: ServiceId[T_ServiceType],
|
17
|
+
) -> Iterator[T_ServiceType]:
|
18
|
+
for container in self._containers:
|
19
|
+
yield from container.get_namespaced_group(namespace, group_id)
|
rats/apps/_container.py
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
from abc import abstractmethod
|
2
|
+
from collections.abc import Iterator
|
3
|
+
from typing import Generic, Protocol
|
4
|
+
|
5
|
+
from ._ids import ServiceId, T_ServiceType, Tco_ConfigType, Tco_ServiceType
|
6
|
+
from ._namespaces import ProviderNamespaces
|
7
|
+
|
8
|
+
|
9
|
+
class ServiceProvider(Protocol[Tco_ServiceType]):
|
10
|
+
@abstractmethod
|
11
|
+
def __call__(self) -> Tco_ServiceType:
|
12
|
+
"""Return the service instance."""
|
13
|
+
|
14
|
+
|
15
|
+
class ConfigProvider(ServiceProvider[Tco_ConfigType], Protocol[Tco_ConfigType]):
|
16
|
+
@abstractmethod
|
17
|
+
def __call__(self) -> Tco_ConfigType:
|
18
|
+
"""Return the config instance."""
|
19
|
+
|
20
|
+
|
21
|
+
class Container(Protocol):
|
22
|
+
"""Main interface for service containers."""
|
23
|
+
|
24
|
+
def has(self, service_id: ServiceId[T_ServiceType]) -> bool:
|
25
|
+
try:
|
26
|
+
return self.get(service_id) is not None
|
27
|
+
except ServiceNotFoundError:
|
28
|
+
return False
|
29
|
+
|
30
|
+
def has_group(self, group_id: ServiceId[T_ServiceType]) -> bool:
|
31
|
+
try:
|
32
|
+
return next(self.get_group(group_id)) is not None
|
33
|
+
except StopIteration:
|
34
|
+
return False
|
35
|
+
|
36
|
+
def has_namespace(self, namespace: str, group_id: ServiceId[T_ServiceType]) -> bool:
|
37
|
+
try:
|
38
|
+
return next(self.get_namespaced_group(namespace, group_id)) is not None
|
39
|
+
except StopIteration:
|
40
|
+
return False
|
41
|
+
|
42
|
+
def get(self, service_id: ServiceId[T_ServiceType]) -> T_ServiceType:
|
43
|
+
"""Retrieve a service instance by its id."""
|
44
|
+
services = list(self.get_namespaced_group(ProviderNamespaces.SERVICES, service_id))
|
45
|
+
if len(services) == 0:
|
46
|
+
services.extend(
|
47
|
+
list(self.get_namespaced_group(ProviderNamespaces.FALLBACK_SERVICES, service_id)),
|
48
|
+
)
|
49
|
+
|
50
|
+
if len(services) > 1:
|
51
|
+
raise DuplicateServiceError(service_id)
|
52
|
+
elif len(services) == 0:
|
53
|
+
raise ServiceNotFoundError(service_id)
|
54
|
+
else:
|
55
|
+
return services[0]
|
56
|
+
|
57
|
+
def get_group(
|
58
|
+
self,
|
59
|
+
group_id: ServiceId[T_ServiceType],
|
60
|
+
) -> Iterator[T_ServiceType]:
|
61
|
+
"""Retrieve a service group by its id."""
|
62
|
+
if not self.has_namespace(ProviderNamespaces.GROUPS, group_id):
|
63
|
+
yield from self.get_namespaced_group(ProviderNamespaces.FALLBACK_GROUPS, group_id)
|
64
|
+
|
65
|
+
yield from self.get_namespaced_group(ProviderNamespaces.GROUPS, group_id)
|
66
|
+
|
67
|
+
@abstractmethod
|
68
|
+
def get_namespaced_group(
|
69
|
+
self,
|
70
|
+
namespace: str,
|
71
|
+
group_id: ServiceId[T_ServiceType],
|
72
|
+
) -> Iterator[T_ServiceType]:
|
73
|
+
"""Retrieve a service group by its id, within a given service namespace."""
|
74
|
+
|
75
|
+
|
76
|
+
class ServiceNotFoundError(RuntimeError, Generic[T_ServiceType]):
|
77
|
+
service_id: ServiceId[T_ServiceType]
|
78
|
+
|
79
|
+
def __init__(self, service_id: ServiceId[T_ServiceType]) -> None:
|
80
|
+
super().__init__(f"Service id not found: {service_id}")
|
81
|
+
self.service_id = service_id
|
82
|
+
|
83
|
+
|
84
|
+
class DuplicateServiceError(RuntimeError, Generic[T_ServiceType]):
|
85
|
+
service_id: ServiceId[T_ServiceType]
|
86
|
+
|
87
|
+
def __init__(self, service_id: ServiceId[T_ServiceType]) -> None:
|
88
|
+
super().__init__(f"Service id provided multiple times: {service_id}")
|
89
|
+
self.service_id = service_id
|
rats/apps/_ids.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
import typing
|
2
|
+
from typing import Generic, TypeVar
|
3
|
+
|
4
|
+
from typing_extensions import NamedTuple
|
5
|
+
|
6
|
+
T_ServiceType = TypeVar("T_ServiceType")
|
7
|
+
T_ConfigType = TypeVar("T_ConfigType", bound=typing.NamedTuple)
|
8
|
+
Tco_ServiceType = TypeVar("Tco_ServiceType", covariant=True)
|
9
|
+
Tco_ConfigType = TypeVar("Tco_ConfigType", bound=NamedTuple, covariant=True)
|
10
|
+
|
11
|
+
|
12
|
+
class ServiceId(NamedTuple, Generic[T_ServiceType]):
|
13
|
+
name: str
|
14
|
+
|
15
|
+
|
16
|
+
class ConfigId(ServiceId[T_ConfigType]):
|
17
|
+
pass
|
rats/apps/_namespaces.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
from collections.abc import Iterable, Iterator
|
2
|
+
from functools import cache
|
3
|
+
from importlib.metadata import entry_points
|
4
|
+
|
5
|
+
from ._container import Container
|
6
|
+
from ._ids import ServiceId, T_ServiceType
|
7
|
+
|
8
|
+
|
9
|
+
class PluginContainers(Container):
|
10
|
+
_app: Container
|
11
|
+
_group: str
|
12
|
+
|
13
|
+
def __init__(self, app: Container, group: str) -> None:
|
14
|
+
self._app = app
|
15
|
+
self._group = group
|
16
|
+
|
17
|
+
def get_namespaced_group(
|
18
|
+
self,
|
19
|
+
namespace: str,
|
20
|
+
group_id: ServiceId[T_ServiceType],
|
21
|
+
) -> Iterator[T_ServiceType]:
|
22
|
+
for container in self._load_containers():
|
23
|
+
yield from container.get_namespaced_group(namespace, group_id)
|
24
|
+
|
25
|
+
@cache # noqa: B019
|
26
|
+
def _load_containers(self) -> Iterable[Container]:
|
27
|
+
entries = entry_points(group=self._group)
|
28
|
+
return tuple(entry.load()(self._app) for entry in entries)
|
rats/apps/_scoping.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from types import FunctionType
|
3
|
+
from typing import Any, ParamSpec, TypeVar, cast
|
4
|
+
|
5
|
+
from ._ids import ServiceId
|
6
|
+
|
7
|
+
T = TypeVar("T")
|
8
|
+
P = ParamSpec("P")
|
9
|
+
|
10
|
+
|
11
|
+
def autoscope(cls: type[T]) -> type[T]:
|
12
|
+
"""
|
13
|
+
Decorator that replaces all ServiceId instances in the class with scoped ServiceId instances.
|
14
|
+
|
15
|
+
The scoped ServiceId instances have a prefix to eliminate the chance of conflicts across
|
16
|
+
packages.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def wrap(func: Callable[..., Any]) -> Callable[..., Any]:
|
20
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ServiceId[Any]:
|
21
|
+
result = func(*args, **kwargs)
|
22
|
+
if not isinstance(result, ServiceId):
|
23
|
+
return result
|
24
|
+
|
25
|
+
return ServiceId[Any](f"{cls.__module__}:{cls.__name__}[{result.name}]")
|
26
|
+
|
27
|
+
return cast(FunctionType, wrapper)
|
28
|
+
|
29
|
+
props = [prop for prop in dir(cls) if not prop.startswith("_")]
|
30
|
+
|
31
|
+
for prop_name in props:
|
32
|
+
non_ns = getattr(cls, prop_name)
|
33
|
+
|
34
|
+
if isinstance(non_ns, FunctionType):
|
35
|
+
setattr(cls, prop_name, wrap(non_ns))
|
36
|
+
else:
|
37
|
+
if not isinstance(non_ns, ServiceId):
|
38
|
+
continue
|
39
|
+
|
40
|
+
prop = ServiceId[Any](f"{cls.__module__}:{cls.__name__}[{non_ns.name}]")
|
41
|
+
setattr(cls, prop_name, prop)
|
42
|
+
|
43
|
+
return cls
|
rats/apps/py.typed
ADDED
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: rats-apps
|
3
|
+
Version: 0.1.2
|
4
|
+
Summary: research analysis tools for building applications
|
5
|
+
Home-page: https://github.com/microsoft/rats/
|
6
|
+
License: MIT
|
7
|
+
Keywords: pipelines,machine learning,research
|
8
|
+
Requires-Python: >=3.10,<4.0
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
14
|
+
Requires-Dist: typing_extensions
|
15
|
+
Project-URL: Documentation, https://microsoft.github.io/rats/
|
16
|
+
Project-URL: Repository, https://github.com/microsoft/rats/
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
|
19
|
+
# rats-apps
|
20
|
+
...
|
21
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
rats/apps/__init__.py,sha256=JYVss3L2R4uxdOp4Ozfg_cHWgRvCWq79OyVZefFw9wY,858
|
2
|
+
rats/apps/_annotations.py,sha256=NGCw1JMZdPMoK_pE61iXgjeI1Xn7P5WXLt7t8zWligg,5125
|
3
|
+
rats/apps/_composite_container.py,sha256=wSWVQWPin2xxIlEoSgk_D1rlc3N2gpTxQ2y9UFdqXy0,553
|
4
|
+
rats/apps/_container.py,sha256=5uiCyxN6HS2z97XcTOFP-t72cNoB1U1sJMkMcfSfDps,3129
|
5
|
+
rats/apps/_ids.py,sha256=dxWCPMpMA_vpaTDJEKNByIBJaX97Db203XqWLhaOezo,457
|
6
|
+
rats/apps/_namespaces.py,sha256=THUV_Xj5PtweC23Ob-zsSpk8exC4fT-qRwjpQ6IDm0U,188
|
7
|
+
rats/apps/_plugin_container.py,sha256=W_xQD2btc0N2dEb3c5tXM-ZZ4A4diMpkCjbOZdlXYuI,853
|
8
|
+
rats/apps/_scoping.py,sha256=lRV1DDq-U4mr4WOQhvFjTiCQe2dKY95LNn6b0RXRjFA,1305
|
9
|
+
rats/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
rats_apps-0.1.2.dist-info/METADATA,sha256=CwHSo9ZWpDzbbE60oLdJO4IG4aV9ub07NzfWoilk0xE,711
|
11
|
+
rats_apps-0.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
12
|
+
rats_apps-0.1.2.dist-info/entry_points.txt,sha256=Vu1IgAPQvL4xMJzW_OG2JSPFac7HalCHyXiRr0-OfCI,86
|
13
|
+
rats_apps-0.1.2.dist-info/RECORD,,
|