rats-apps 0.1.2.dev14__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.
@@ -7,12 +7,24 @@ from ._ids import ServiceId, T_ServiceType
7
7
 
8
8
 
9
9
  class PluginContainers(Container):
10
+ """
11
+ A container that loads plugins using importlib.metadata.entry_points.
12
+
13
+ When looking for groups, the container loads the specified entry_points and defers the lookups
14
+ to the plugins. Plugin containers are expected to be Callable[[Container], Container] objects,
15
+ where the input container is typically the root application container.
16
+
17
+ TODO: How do we better specify the API for plugins without relying on documentation?
18
+ """
19
+
10
20
  _app: Container
11
21
  _group: str
22
+ _names: tuple[str, ...]
12
23
 
13
- def __init__(self, app: Container, group: str) -> None:
24
+ def __init__(self, app: Container, group: str, *names: str) -> None:
14
25
  self._app = app
15
26
  self._group = group
27
+ self._names = names
16
28
 
17
29
  def get_namespaced_group(
18
30
  self,
@@ -25,4 +37,7 @@ class PluginContainers(Container):
25
37
  @cache # noqa: B019
26
38
  def _load_containers(self) -> Iterable[Container]:
27
39
  entries = entry_points(group=self._group)
28
- return tuple(entry.load()(self._app) for entry in entries)
40
+ return tuple(entry.load()(self._app) for entry in entries if self._is_enabled(entry.name))
41
+
42
+ def _is_enabled(self, plugin_name: str) -> bool:
43
+ return len(self._names) == 0 or plugin_name in self._names
rats/apps/_plugins.py ADDED
@@ -0,0 +1,23 @@
1
+ import warnings
2
+ from collections.abc import Callable, Iterator
3
+ from typing import Generic, TypeVar
4
+
5
+ T_PluginType = TypeVar("T_PluginType")
6
+
7
+
8
+ class PluginRunner(Generic[T_PluginType]):
9
+ """Client to apply a function to a list of plugins."""
10
+
11
+ _plugins: Iterator[T_PluginType]
12
+
13
+ def __init__(self, plugins: Iterator[T_PluginType]) -> None:
14
+ self._plugins = plugins
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
+ )
22
+ for plugin in self._plugins:
23
+ handler(plugin)
rats/apps/_runtimes.py ADDED
@@ -0,0 +1,41 @@
1
+ from abc import abstractmethod
2
+ from collections.abc import Callable
3
+ from typing import Protocol, final
4
+
5
+ from ._ids import ServiceId, T_ExecutableType
6
+
7
+
8
+ class Runtime(Protocol):
9
+ @abstractmethod
10
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
11
+ """Execute a list of executables sequentially."""
12
+
13
+ @abstractmethod
14
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
15
+ """
16
+ Execute one or more groups of executables sequentially.
17
+
18
+ Although each group is expected to be executed sequentially, the groups themselves are not
19
+ executed in a deterministic order. Runtime implementations are free to execute groups in
20
+ parallel or in any order that is convenient.
21
+ """
22
+
23
+ @abstractmethod
24
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
25
+ """
26
+ Execute provided callables by automatically turning them into apps.Executable objects.
27
+
28
+ The used ServiceId is determined by the Runtime implementation.
29
+ """
30
+
31
+
32
+ @final
33
+ class NullRuntime(Runtime):
34
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
35
+ raise NotImplementedError("NullRuntime does not support execution.")
36
+
37
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
38
+ raise NotImplementedError("NullRuntime does not support execution.")
39
+
40
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
41
+ raise NotImplementedError("NullRuntime does not support execution.")
rats/apps/_scoping.py CHANGED
@@ -22,7 +22,7 @@ def autoscope(cls: type[T]) -> type[T]:
22
22
  if not isinstance(result, ServiceId):
23
23
  return result
24
24
 
25
- return ServiceId[Any](f"{cls.__module__}:{cls.__name__}[{result.name}]")
25
+ return ServiceId[Any](scope_service_name(cls.__module__, cls.__name__, result.name))
26
26
 
27
27
  return cast(FunctionType, wrapper)
28
28
 
@@ -37,7 +37,11 @@ def autoscope(cls: type[T]) -> type[T]:
37
37
  if not isinstance(non_ns, ServiceId):
38
38
  continue
39
39
 
40
- prop = ServiceId[Any](f"{cls.__module__}:{cls.__name__}[{non_ns.name}]")
40
+ prop = ServiceId[Any](scope_service_name(cls.__module__, cls.__name__, non_ns.name))
41
41
  setattr(cls, prop_name, prop)
42
42
 
43
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,163 @@
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, 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
+ STANDARD_RUNTIME = ServiceId[Runtime]("standard-runtime")
60
+ CONTAINER = ServiceId[Container]("app-container")
61
+ CALLABLE_EXE_CTX = ServiceId[ExecutableCallableContext]("callable-exe-ctx")
62
+ CALLABLE_EXE = ServiceId[Executable]("callable-exe")
63
+
64
+
65
+ @final
66
+ class StandardRuntime(Runtime):
67
+ """A simple runtime that executes sequentially and in a single thread."""
68
+
69
+ _app: Container
70
+
71
+ def __init__(self, app: Container) -> None:
72
+ self._app = app
73
+
74
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
75
+ for exe_id in exe_ids:
76
+ self._app.get(exe_id).execute()
77
+
78
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
79
+ for exe_group_id in exe_group_ids:
80
+ for exe in self._app.get_group(exe_group_id):
81
+ exe.execute()
82
+
83
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
84
+ ctx: ExecutableCallableContext = self._app.get(AppServices.CALLABLE_EXE_CTX)
85
+ for cb in callables:
86
+ with ctx.open_callable(cb) as exe:
87
+ exe.execute()
88
+
89
+
90
+ @final
91
+ class SimpleApplication(Runtime, Container):
92
+ """An application without anything fancy."""
93
+
94
+ _plugin_groups: Iterable[str]
95
+ _runtime_plugin: Callable[[Container], Container] | None
96
+
97
+ def __init__(
98
+ self,
99
+ *plugin_groups: str,
100
+ runtime_plugin: Callable[[Container], Container] | None = None,
101
+ ) -> None:
102
+ self._plugin_groups = plugin_groups
103
+ self._runtime_plugin = runtime_plugin
104
+
105
+ def execute(self, *exe_ids: ServiceId[T_ExecutableType]) -> None:
106
+ self._runtime().execute(*exe_ids)
107
+
108
+ def execute_group(self, *exe_group_ids: ServiceId[T_ExecutableType]) -> None:
109
+ self._runtime().execute_group(*exe_group_ids)
110
+
111
+ def execute_callable(self, *callables: Callable[[], None]) -> None:
112
+ for cb in callables:
113
+ self._runtime().execute_callable(cb)
114
+
115
+ @fallback_service(AppServices.RUNTIME)
116
+ def _runtime(self) -> Runtime:
117
+ """
118
+ The default runtime defers to AppServices.STANDARD_RUNTIME.
119
+
120
+ Define a non-fallback service to override this default implementation.
121
+ """
122
+ return self.get(AppServices.STANDARD_RUNTIME)
123
+
124
+ @service(AppServices.STANDARD_RUNTIME)
125
+ def _std_runtime(self) -> Runtime:
126
+ """
127
+ The standard locally executed runtime.
128
+
129
+ Regardless of the configured AppServices.RUNTIME provider, everyone has access to this
130
+ class for plugin development.
131
+ """
132
+ return StandardRuntime(self)
133
+
134
+ @fallback_service(AppServices.CALLABLE_EXE)
135
+ def _callable_exe(self) -> Executable:
136
+ """We use the callable exe ctx here, so we can treat it like any other app downstream."""
137
+ return self.get(AppServices.CALLABLE_EXE_CTX)
138
+
139
+ @fallback_service(AppServices.CALLABLE_EXE_CTX)
140
+ def _callable_exe_ctx(self) -> ExecutableCallableContext:
141
+ """
142
+ The default executable context client for executing raw callables.
143
+
144
+ Define a non-fallback service to override this default implementation.
145
+ """
146
+ return ExecutableCallableContext(AppServices.CALLABLE_EXE)
147
+
148
+ @fallback_service(AppServices.CONTAINER)
149
+ def _container(self) -> Container:
150
+ """
151
+ The default container is the root application instance.
152
+
153
+ Define a non-fallback service to override this default implementation.
154
+ """
155
+ return self
156
+
157
+ @container()
158
+ def _plugins(self) -> Container:
159
+ plugins: list[Container] = [PluginContainers(self, group) for group in self._plugin_groups]
160
+ if self._runtime_plugin:
161
+ plugins.append(self._runtime_plugin(self))
162
+
163
+ return CompositeContainer(*plugins)
rats/cli/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Uses `rats.cli` 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 ._plugin import PluginContainer, PluginServices
7
+ from ._plugins import AttachClickCommands, AttachClickGroup, ClickGroupPlugin, CommandContainer
8
+
9
+ __all__ = [
10
+ "CommandId",
11
+ "PluginContainer",
12
+ "command",
13
+ "group",
14
+ "PluginServices",
15
+ "ClickCommandMapper",
16
+ "ClickExecutable",
17
+ "ClickGroupPlugin",
18
+ "ClickCommandGroup",
19
+ "AttachClickCommands",
20
+ "AttachClickGroup",
21
+ "CommandContainer",
22
+ ]
@@ -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/_plugin.py ADDED
@@ -0,0 +1,68 @@
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
+ return cast(apps.ServiceId[click.Group], cmd_id)
40
+
41
+
42
+ @final
43
+ class PluginContainer(apps.Container):
44
+ _app: apps.Container
45
+
46
+ def __init__(self, app: apps.Container) -> None:
47
+ self._app = app
48
+
49
+ @apps.service(PluginServices.ROOT_COMMAND)
50
+ def _root_command(self) -> apps.Executable:
51
+ def run() -> None:
52
+ runtime = self._app.get(apps.AppServices.RUNTIME)
53
+ runtime.execute_group(
54
+ PluginServices.EVENTS.command_open(PluginServices.ROOT_COMMAND),
55
+ PluginServices.EVENTS.command_execute(PluginServices.ROOT_COMMAND),
56
+ PluginServices.EVENTS.command_close(PluginServices.ROOT_COMMAND),
57
+ )
58
+
59
+ return apps.App(run)
60
+
61
+ @apps.fallback_group(PluginServices.EVENTS.command_execute(PluginServices.ROOT_COMMAND))
62
+ def _default_command(self) -> apps.Executable:
63
+ group = self._app.get(PluginServices.click_command(PluginServices.ROOT_COMMAND))
64
+ return apps.App(lambda: group())
65
+
66
+ @apps.service(PluginServices.click_command(PluginServices.ROOT_COMMAND))
67
+ def _root_click_command(self) -> click.Group:
68
+ return click.Group("groot")
rats/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
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,63 @@
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
+ "loggers": {
58
+ "": {"level": "INFO", "handlers": ["console"]},
59
+ "azure": {"level": "WARNING", "handlers": ["console"]},
60
+ },
61
+ }
62
+ )
63
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rats-apps
3
- Version: 0.1.2.dev14
3
+ Version: 0.1.3
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,8 @@ 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
15
+ Requires-Dist: colorlog
14
16
  Requires-Dist: typing_extensions
15
17
  Project-URL: Documentation, https://microsoft.github.io/rats/
16
18
  Project-URL: Repository, https://github.com/microsoft/rats/