rats-apps 0.1.3.dev50__py3-none-any.whl → 0.1.3.dev58__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.
@@ -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,125 @@
1
+ # type: ignore
2
+ from __future__ import annotations
3
+
4
+ from collections import defaultdict
5
+ from collections.abc import Callable
6
+ from functools import cache
7
+ from typing import Any, Generic, ParamSpec, TypeVar
8
+ from typing import NamedTuple as tNamedTuple
9
+
10
+ from typing_extensions import NamedTuple
11
+
12
+ T_GroupType = TypeVar("T_GroupType", bound=NamedTuple)
13
+
14
+
15
+ class GroupAnnotations(NamedTuple, Generic[T_GroupType]):
16
+ """The list of T_GroupType objects identified by a given name in a namespace."""
17
+
18
+ name: str
19
+ """The name of the annotation, typically the name of the function in a class"""
20
+ namespace: str
21
+ """All the groups in a namespace are expected to be of the same T_GroupType."""
22
+ groups: tuple[T_GroupType, ...]
23
+
24
+
25
+ class AnnotationsContainer(NamedTuple):
26
+ """
27
+ Holds metadata about the annotated service provider.
28
+
29
+ Loosely inspired by: https://peps.python.org/pep-3107/.
30
+ """
31
+
32
+ annotations: tuple[GroupAnnotations[...], ...]
33
+
34
+ @staticmethod
35
+ def empty() -> AnnotationsContainer:
36
+ return AnnotationsContainer(annotations=())
37
+
38
+ def with_group(
39
+ self,
40
+ namespace: str,
41
+ group_id: T_GroupType,
42
+ ) -> AnnotationsContainer:
43
+ return AnnotationsContainer(
44
+ annotations=tuple(
45
+ [
46
+ annotation_group
47
+ for x in self.with_namespace(namespace)
48
+ for annotation_group in x
49
+ if group_id in annotation_group.groups
50
+ ]
51
+ ),
52
+ )
53
+
54
+ def with_namespace(
55
+ self,
56
+ namespace: str,
57
+ ) -> AnnotationsContainer:
58
+ return AnnotationsContainer(
59
+ annotations=tuple([x for x in self.annotations if x.namespace == namespace]),
60
+ )
61
+
62
+
63
+ class AnnotationsBuilder:
64
+ _group_ids: dict[str, set[Any]]
65
+
66
+ def __init__(self) -> None:
67
+ self._group_ids = defaultdict(set)
68
+
69
+ def add(self, namespace: str, group_id: NamedTuple | tNamedTuple) -> None:
70
+ self._group_ids[namespace].add(group_id)
71
+
72
+ def make(self, name: str) -> AnnotationsContainer:
73
+ return AnnotationsContainer(
74
+ annotations=tuple(
75
+ [
76
+ GroupAnnotations[Any](name=name, namespace=namespace, groups=tuple(groups))
77
+ for namespace, groups in self._group_ids.items()
78
+ ]
79
+ ),
80
+ )
81
+
82
+
83
+ DecoratorType = TypeVar("DecoratorType", bound=Callable[..., Any])
84
+
85
+
86
+ def annotation(
87
+ namespace: str,
88
+ group_id: NamedTuple | tNamedTuple,
89
+ ) -> Callable[[DecoratorType], DecoratorType]:
90
+ def decorator(fn: DecoratorType) -> DecoratorType:
91
+ if not hasattr(fn, "__rats_annotations__"):
92
+ fn.__rats_annotations__ = AnnotationsBuilder() # type: ignore[reportFunctionMemberAccess]
93
+
94
+ fn.__rats_annotations__.add(namespace, group_id) # type: ignore[reportFunctionMemberAccess]
95
+
96
+ return fn
97
+
98
+ return decorator
99
+
100
+
101
+ @cache
102
+ def get_class_annotations(cls: type) -> AnnotationsContainer:
103
+ tates = []
104
+
105
+ for method_name in dir(cls):
106
+ method = getattr(cls, method_name)
107
+ if not hasattr(method, "__rats_annotations__"):
108
+ continue
109
+
110
+ tates.extend(method.__rats_annotations__.make(method_name).annotations)
111
+
112
+ return AnnotationsContainer(annotations=tuple(tates))
113
+
114
+
115
+ P = ParamSpec("P")
116
+
117
+
118
+ def get_annotations(fn: Callable[..., Any]) -> AnnotationsContainer:
119
+ builder: AnnotationsBuilder = getattr(
120
+ fn,
121
+ "__rats_annotations__",
122
+ AnnotationsBuilder(),
123
+ )
124
+
125
+ return builder.make(fn.__name__)
File without changes
rats/apps/__init__.py CHANGED
@@ -18,11 +18,18 @@ from ._annotations import (
18
18
  service,
19
19
  )
20
20
  from ._composite_container import CompositeContainer
21
- from ._container import Container, DuplicateServiceError, ServiceNotFoundError
21
+ from ._container import (
22
+ ConfigProvider,
23
+ Container,
24
+ DuplicateServiceError,
25
+ ServiceNotFoundError,
26
+ ServiceProvider,
27
+ )
22
28
  from ._executables import App, AppContainer, Executable
23
29
  from ._ids import ConfigId, ServiceId
24
30
  from ._namespaces import ProviderNamespaces
25
31
  from ._plugin_container import PluginContainers
32
+ from ._plugins import PluginRunner
26
33
  from ._scoping import autoscope
27
34
 
28
35
  __all__ = [
@@ -36,12 +43,15 @@ __all__ = [
36
43
  "Executable",
37
44
  "PluginContainers",
38
45
  "ProviderNamespaces",
46
+ "ConfigProvider",
47
+ "ServiceProvider",
39
48
  "ServiceId",
40
49
  "ServiceNotFoundError",
41
50
  "autoid_service",
42
51
  "autoscope",
43
52
  "config",
44
53
  "container",
54
+ "PluginRunner",
45
55
  "fallback_config",
46
56
  "fallback_group",
47
57
  "fallback_service",
rats/apps/_annotations.py CHANGED
@@ -1,3 +1,4 @@
1
+ import abc
1
2
  from collections import defaultdict
2
3
  from collections.abc import Callable, Iterator
3
4
  from functools import cache
@@ -70,7 +71,7 @@ class FunctionAnnotationsBuilder:
70
71
  )
71
72
 
72
73
 
73
- class AnnotatedContainer(Container):
74
+ class AnnotatedContainer(Container, abc.ABC):
74
75
  def get_namespaced_group(
75
76
  self,
76
77
  namespace: str,
rats/apps/_plugins.py ADDED
@@ -0,0 +1,17 @@
1
+ from collections.abc import Callable, Iterator
2
+ from typing import Generic, TypeVar
3
+
4
+ T_PluginType = TypeVar("T_PluginType")
5
+
6
+
7
+ class PluginRunner(Generic[T_PluginType]):
8
+ """Client to apply a function to a list of plugins."""
9
+
10
+ _plugins: Iterator[T_PluginType]
11
+
12
+ def __init__(self, plugins: Iterator[T_PluginType]) -> None:
13
+ self._plugins = plugins
14
+
15
+ def apply(self, handler: Callable[[T_PluginType], None]) -> None:
16
+ for plugin in self._plugins:
17
+ handler(plugin)
rats/cli/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Uses `rats.annotations` to streamline the creation of CLI commands written with Click."""
2
+
3
+ from ._annotations import CommandId, command, group
4
+ from ._click import ClickCommandGroup, ClickCommandMapper
5
+ from ._executable import ClickExecutable
6
+ from ._plugins import AttachClickCommands, AttachClickGroup, ClickGroupPlugin, CommandContainer
7
+
8
+ __all__ = [
9
+ "CommandId",
10
+ "command",
11
+ "group",
12
+ "ClickCommandMapper",
13
+ "ClickExecutable",
14
+ "ClickGroupPlugin",
15
+ "ClickCommandGroup",
16
+ "AttachClickCommands",
17
+ "AttachClickGroup",
18
+ "CommandContainer",
19
+ ]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, NamedTuple, TypeVar
5
+
6
+ from rats import annotations as anns
7
+
8
+
9
+ class CommandId(NamedTuple):
10
+ name: str
11
+
12
+ # does this api make it impossible to reference a given command that was auto generated?
13
+ @staticmethod
14
+ def auto() -> CommandId:
15
+ return CommandId(name=f"{__name__}:auto")
16
+
17
+
18
+ T = TypeVar("T", bound=Callable[[Any], Any])
19
+
20
+
21
+ def command(command_id: CommandId) -> Callable[[T], T]:
22
+ def decorator(fn: T) -> T:
23
+ if command_id == CommandId.auto():
24
+ return anns.annotation("commands", CommandId(fn.__name__.replace("_", "-")))(fn)
25
+ return anns.annotation("commands", command_id)(fn)
26
+
27
+ return decorator
28
+
29
+
30
+ def group(command_id: CommandId) -> Callable[[T], T]:
31
+ def decorator(fn: T) -> T:
32
+ if command_id == CommandId.auto():
33
+ return anns.annotation("command-groups", CommandId(fn.__name__.replace("_", "-")))(fn)
34
+ return anns.annotation("commands", command_id)(fn)
35
+
36
+ return decorator
37
+
38
+
39
+ def get_class_commands(cls: type) -> anns.AnnotationsContainer:
40
+ return anns.get_class_annotations(cls).with_namespace("commands")
rats/cli/_click.py ADDED
@@ -0,0 +1,38 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import final
3
+
4
+ import click
5
+
6
+
7
+ class ClickCommandMapper:
8
+ _commands: Mapping[str, Callable[[], click.Command]]
9
+
10
+ def __init__(
11
+ self,
12
+ commands: Mapping[str, Callable[[], click.Command]],
13
+ ) -> None:
14
+ self._commands = commands
15
+
16
+ def names(self) -> frozenset[str]:
17
+ return frozenset(self._commands.keys())
18
+
19
+ def get(self, name: str) -> click.Command:
20
+ if name not in self._commands:
21
+ raise ValueError(f"Command {name} not found")
22
+
23
+ return self._commands[name]()
24
+
25
+
26
+ @final
27
+ class ClickCommandGroup(click.Group):
28
+ _mapper: ClickCommandMapper
29
+
30
+ def __init__(self, name: str, mapper: ClickCommandMapper) -> None:
31
+ super().__init__(name=name)
32
+ self._mapper = mapper
33
+
34
+ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
35
+ return self._mapper.get(cmd_name)
36
+
37
+ def list_commands(self, ctx: click.Context) -> list[str]:
38
+ return list(self._mapper.names())
@@ -0,0 +1,23 @@
1
+ import click
2
+
3
+ from rats import apps
4
+
5
+ from ._plugins import ClickGroupPlugin
6
+
7
+
8
+ class ClickExecutable(apps.Executable):
9
+ _command: apps.ServiceProvider[click.Group]
10
+ _plugins: apps.PluginRunner[ClickGroupPlugin]
11
+
12
+ def __init__(
13
+ self,
14
+ command: apps.ServiceProvider[click.Group],
15
+ plugins: apps.PluginRunner[ClickGroupPlugin],
16
+ ) -> None:
17
+ self._command = command
18
+ self._plugins = plugins
19
+
20
+ def execute(self) -> None:
21
+ cmd = self._command()
22
+ self._plugins.apply(lambda plugin: plugin.on_group_open(cmd))
23
+ cmd()
rats/cli/_plugins.py ADDED
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable, Iterator
5
+ from functools import partial
6
+ from typing import Any, Protocol
7
+
8
+ import click
9
+
10
+ from rats import apps
11
+
12
+ from ._annotations import get_class_commands
13
+
14
+
15
+ class ClickGroupPlugin(Protocol):
16
+ @abstractmethod
17
+ def on_group_open(self, group: click.Group) -> None:
18
+ pass
19
+
20
+
21
+ class AttachClickCommands(ClickGroupPlugin):
22
+ """When a group is opened, attach a set of commands to it."""
23
+
24
+ _commands: Iterator[click.Command]
25
+
26
+ def __init__(self, commands: Iterator[click.Command]) -> None:
27
+ self._commands = commands
28
+
29
+ def on_group_open(self, group: click.Group) -> None:
30
+ for command in self._commands:
31
+ group.add_command(command)
32
+
33
+
34
+ class AttachClickGroup(ClickGroupPlugin):
35
+ _group: apps.ServiceProvider[click.Group]
36
+ _plugins: apps.PluginRunner[ClickGroupPlugin]
37
+
38
+ def __init__(
39
+ self,
40
+ group: apps.ServiceProvider[click.Group],
41
+ plugins: apps.PluginRunner[ClickGroupPlugin],
42
+ ) -> None:
43
+ self._group = group
44
+ self._plugins = plugins
45
+
46
+ def on_group_open(self, group: click.Group) -> None:
47
+ cmd = self._group()
48
+ self._plugins.apply(lambda plugin: plugin.on_group_open(cmd))
49
+ group.add_command(cmd)
50
+
51
+
52
+ class CommandContainer(ClickGroupPlugin):
53
+ def on_group_open(self, group: click.Group) -> None:
54
+ def cb(_method: Callable[[Any], Any], *args: Any, **kwargs: Any) -> None:
55
+ """
56
+ Callback handed to `click.Command`. Calls the method with matching name on this class.
57
+
58
+ When the command is decorated with `@click.params` and `@click.option`, `click` will
59
+ call this callback with the parameters in the order they were defined. This callback
60
+ then calls the method with the same name on this class, passing the parameters in
61
+ reverse order. This is because the method is defined with the parameters in the
62
+ reverse order to the decorator, so we need to reverse them again to get the correct
63
+ order.
64
+ """
65
+ _method(*args, **kwargs)
66
+
67
+ commands = get_class_commands(type(self))
68
+ tates = commands.annotations
69
+
70
+ for tate in tates:
71
+ method = getattr(self, tate.name)
72
+ params = list(reversed(getattr(method, "__click_params__", [])))
73
+ for command in tate.groups:
74
+ group.add_command(
75
+ click.Command(
76
+ name=command.name,
77
+ callback=partial(cb, method),
78
+ short_help=method.__doc__,
79
+ params=params,
80
+ )
81
+ )
rats/cli/py.typed ADDED
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rats-apps
3
- Version: 0.1.3.dev50
3
+ Version: 0.1.3.dev58
4
4
  Summary: research analysis tools for building applications
5
5
  Home-page: https://github.com/microsoft/rats/
6
6
  License: MIT
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: click
14
15
  Requires-Dist: typing_extensions
15
16
  Project-URL: Documentation, https://microsoft.github.io/rats/
16
17
  Project-URL: Repository, https://github.com/microsoft/rats/
@@ -0,0 +1,23 @@
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.dev58.dist-info/METADATA,sha256=5Jtm7M9adH9QwyM7lEb_CsSMSQV1sgydmD3X89iT6dE,738
22
+ rats_apps-0.1.3.dev58.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
23
+ rats_apps-0.1.3.dev58.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- rats/apps/__init__.py,sha256=C2AHnjIf0Bf4gnrS4sO0QSEknIQXfn3X9qzknY7t9PE,1182
2
- rats/apps/_annotations.py,sha256=MMYqJ0F6MzynwOT3ZWKiIkXwQsaD4rvkpkURP4EzZDE,6971
3
- rats/apps/_composite_container.py,sha256=wSWVQWPin2xxIlEoSgk_D1rlc3N2gpTxQ2y9UFdqXy0,553
4
- rats/apps/_container.py,sha256=5uiCyxN6HS2z97XcTOFP-t72cNoB1U1sJMkMcfSfDps,3129
5
- rats/apps/_executables.py,sha256=8ITn__pjTLHo7FEb-3C6ZQrs1mow0gZn6d-24XGBSu8,1079
6
- rats/apps/_ids.py,sha256=dxWCPMpMA_vpaTDJEKNByIBJaX97Db203XqWLhaOezo,457
7
- rats/apps/_namespaces.py,sha256=THUV_Xj5PtweC23Ob-zsSpk8exC4fT-qRwjpQ6IDm0U,188
8
- rats/apps/_plugin_container.py,sha256=W_xQD2btc0N2dEb3c5tXM-ZZ4A4diMpkCjbOZdlXYuI,853
9
- rats/apps/_scoping.py,sha256=plSVEq3rJ8JFAu2epVg2NQpuTbpSTA3a0Tha_DwJL_Y,1453
10
- rats/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- rats_apps-0.1.3.dev50.dist-info/METADATA,sha256=u0iyVLDioMUseN9RcFdzffdxCGNpnZ9EltaWmoVhNc0,717
12
- rats_apps-0.1.3.dev50.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
13
- rats_apps-0.1.3.dev50.dist-info/RECORD,,