rats-apps 0.10.3.dev20250520163928__py3-none-any.whl → 0.11.0.dev20250521180658__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,55 @@
1
- """Application context package to help share [apps.Container][] state across machines."""
1
+ """
2
+ Allows marshaling of simple value objects across processes and machines.
3
+
4
+ The [rats.app_context.Collection][] and [rats.app_context.Context][] classes are the building
5
+ blocks for creating a collection of services that can be serialized to json and shared across
6
+ applications. In order to be easily serializable, all service values managed by this module must
7
+ be instances of [dataclasses.dataclass][], must be immutable, and must be made up of simple value
8
+ types.
9
+
10
+ !!! example
11
+ If we have an example data structure, `MySimpleData`, we can store instances of it into a
12
+ [rats.app_context.Context][] object by assigning them a service id. Every service id in a
13
+ context maps to zero or more instances of our data structure. Zero or more context objects can
14
+ be used to create [rats.app_context.Collection][] instances, which can be serialized with the
15
+ [rats.app_context.dumps][] and [rats.app_context.loads][] functions.
16
+
17
+ ```python
18
+ from dataclasses import dataclass
19
+ from rats import apps, app_context
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class MySimpleData:
24
+ blob_path: tuple[str, str, str]
25
+ offset: int
26
+
27
+
28
+ data1 = MySimpleData(
29
+ blob_path=("accountname", "containername", "/some/blob/path"),
30
+ offset=3,
31
+ )
32
+
33
+ data2 = MySimpleData(
34
+ blob_path=("accountname", "containername", "/another/blob/path"),
35
+ offset=3,
36
+ )
37
+
38
+ service_id = apps.ServiceId[MySimpleData]("example-service")
39
+
40
+ ctx = app_context.Context[MySimpleData].make(service_id, data1, data2)
41
+ collection = app_context.Collection.make(ctx)
42
+
43
+ # the encoded json string can be moved between machines through files, env variables, etc.
44
+ json_encoded = app_context.dumps(collection)
45
+ # on the remote process, we can rebuild the original collection object
46
+ original_collection = app_context.loads(json_encoded)
47
+ # we can retrieve the json-encoded contexts
48
+ values = original_collection.values(service_id)
49
+ # and we can decode the values back into the `MySimpleData` instances
50
+ simple_data_instances = original_collection.decoded_values(MySimpleData, service_id)
51
+ ```
52
+ """
2
53
 
3
54
  from ._collection import (
4
55
  EMPTY_COLLECTION,
@@ -7,12 +58,13 @@ from ._collection import (
7
58
  loads,
8
59
  )
9
60
  from ._container import GroupContainer, ServiceContainer
10
- from ._context import Context, T_ContextType
61
+ from ._context import Context, ContextValue, T_ContextType
11
62
 
12
63
  __all__ = [
13
64
  "EMPTY_COLLECTION",
14
65
  "Collection",
15
66
  "Context",
67
+ "ContextValue",
16
68
  "GroupContainer",
17
69
  "ServiceContainer",
18
70
  "T_ContextType",
@@ -25,24 +25,37 @@ class Collection(Generic[T_ContextType]):
25
25
  """
26
26
 
27
27
  items: tuple[Context[T_ContextType], ...]
28
+ """Access to the raw [rats.app_context.Context][] instances in the collection."""
28
29
 
29
30
  @staticmethod
30
31
  def merge(*collections: Collection[T_ContextType]) -> Collection[T_ContextType]:
32
+ """
33
+ Merge multiple collections into a new, combined instance.
34
+
35
+ Args:
36
+ *collections: zero or more collections to merge into one.
37
+ """
31
38
  return Collection[T_ContextType].make(
32
39
  *[ctx for collection in collections for ctx in collection.items]
33
40
  )
34
41
 
35
42
  @staticmethod
36
43
  def empty() -> Collection[T_ContextType]:
37
- """Useful when wanting to define a simple default value for a context collection."""
44
+ """A shortcut to retrieving an empty collection."""
38
45
  return Collection(items=())
39
46
 
40
47
  @staticmethod
41
48
  def make(*items: Context[T_ContextType]) -> Collection[T_ContextType]:
42
- # just a handy shortcut to remove a nested tuple from the end-user code.
49
+ """
50
+ Convenience function to create a collection from a list of contexts.
51
+
52
+ Args:
53
+ *items: zero or more context objects to turn into a collection.
54
+ """
43
55
  return Collection(items=items)
44
56
 
45
57
  def service_ids(self) -> set[apps.ServiceId[T_ContextType]]:
58
+ """Get the list of service items found in the collection, across all context instances."""
46
59
  return {item.service_id for item in self.items}
47
60
 
48
61
  def decoded_values(
@@ -50,9 +63,31 @@ class Collection(Generic[T_ContextType]):
50
63
  cls: type[T_ContextType],
51
64
  service_id: apps.ServiceId[T_ContextType],
52
65
  ) -> tuple[T_ContextType, ...]:
66
+ """
67
+ Given a reference to a dataclass type, retrieves and builds instances matching a service id.
68
+
69
+ !!! info
70
+ The dataclass used in the two communicating processes does not need to be the same, but
71
+ we expect the constructor arguments to be compatible.
72
+
73
+ Args:
74
+ cls: the type all selected context objects will be cast into before returning.
75
+ service_id: the selector for the expected context objects.
76
+ """
53
77
  return tuple(dataclass_wizard.fromlist(cls, list(self.values(service_id))))
54
78
 
55
79
  def values(self, service_id: apps.ServiceId[T_ContextType]) -> tuple[ContextValue, ...]:
80
+ """
81
+ Retrieves the raw data structures matching a service id.
82
+
83
+ The values returned here have been encoded into simple dictionaries and are ready to be
84
+ serialized and transferred across machines. See the
85
+ [rats.app_context.Collection.decoded_values][] method to retrieve context values that have
86
+ been turned back into the desired dataclass objects.
87
+
88
+ Args:
89
+ service_id: the selector for the expected context objects.
90
+ """
56
91
  results: list[ContextValue] = []
57
92
  for item in self.with_id(service_id).items:
58
93
  for value in item.values:
@@ -61,19 +96,32 @@ class Collection(Generic[T_ContextType]):
61
96
  return tuple(results)
62
97
 
63
98
  def add(self, *items: Context[T_ContextType]) -> Collection[T_ContextType]:
64
- """Creates a new Collection with the provided items added to the current ones."""
99
+ """
100
+ Creates a new Collection with the provided items added to the current ones.
101
+
102
+ Args:
103
+ *items: the context items to be added in the new collection.
104
+ """
65
105
  return Collection[T_ContextType].make(
66
106
  *self.items,
67
107
  *items,
68
108
  )
69
109
 
70
110
  def with_id(self, *service_ids: apps.ServiceId[T_ContextType]) -> Collection[T_ContextType]:
71
- """Filter out context items not matching the provided service ids and return a new Collection."""
111
+ """
112
+ Filter out context items not matching the provided service ids and return a new Collection.
113
+
114
+ Args:
115
+ *service_ids: the selector for the returned context collection.
116
+ """
72
117
  return Collection[T_ContextType](
73
118
  items=tuple(item for item in self.items if item.service_id in service_ids),
74
119
  )
75
120
 
76
121
 
122
+ # disable key transformation for our collection class
123
+ dataclass_wizard.DumpMeta(key_transform="NONE").bind_to(Collection) # type: ignore[reportUnknownMemberType]
124
+ dataclass_wizard.LoadMeta(key_transform="NONE").bind_to(Collection) # type: ignore[reportUnknownMemberType]
77
125
  EMPTY_COLLECTION = Collection[Any].empty()
78
126
  """Empty collection constant usable as default method values."""
79
127
 
@@ -92,10 +140,10 @@ def loads(context: str) -> Collection[Any]:
92
140
  items=tuple(
93
141
  [
94
142
  Context(
95
- service_id=apps.ServiceId(*x["serviceId"]),
143
+ service_id=apps.ServiceId(*x["service_id"]),
96
144
  values=tuple(x["values"]),
97
145
  )
98
- for x in data["items"]
146
+ for x in data.get("items", [])
99
147
  ]
100
148
  ),
101
149
  )
@@ -107,8 +155,5 @@ def dumps(collection: Collection[Any]) -> str:
107
155
 
108
156
  Args:
109
157
  collection: collection of serializable services wanting to be shared between machines.
110
-
111
- Returns: a string that can easily be shared between machines.
112
-
113
158
  """
114
- return json.dumps(dataclass_wizard.asdict(collection)) # type: ignore
159
+ return json.dumps(dataclass_wizard.asdict(collection), indent=4) # type: ignore
@@ -10,7 +10,17 @@ from rats import apps
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
  T_ContextType = TypeVar("T_ContextType") # TODO: figure out how to bind this to dataclasses :(
13
+ """
14
+ Generic type for context value objects.
15
+
16
+ !!! warning
17
+ Python lacks the ability to bind generic values to require them to be a
18
+ [dataclasses.dataclass][] instance, but all [rats.app_context.T_ContextType][] instances must
19
+ be dataclasses in order for them to serialize and deserialize. This is mostly a leaky private
20
+ detail, and we hope to remove this requirement in the future.
21
+ """
13
22
  ContextValue = dict[str, Any]
23
+ """Values found in [rats.app_context.Context][] in their simple value type representations."""
14
24
 
15
25
 
16
26
  @final
@@ -25,7 +35,11 @@ class Context(dataclass_wizard.JSONSerializable, Generic[T_ContextType]):
25
35
  """
26
36
 
27
37
  service_id: apps.ServiceId[T_ContextType]
38
+ """
39
+ The [rats.apps.ServiceId][] the remote app will be able to use to retrieve the stored values.
40
+ """
28
41
  values: tuple[ContextValue, ...]
42
+ """The encoded value object, ready to be turned into json for marshaling."""
29
43
 
30
44
  @staticmethod
31
45
  def make(
@@ -39,18 +53,28 @@ class Context(dataclass_wizard.JSONSerializable, Generic[T_ContextType]):
39
53
  from dataclass import dataclass
40
54
 
41
55
 
56
+ @dataclass(frozen=True)
42
57
  class MySimpleData:
43
58
  blob_path: Tuple[str, str, str]
44
59
  offset: int
45
60
 
46
61
 
62
+ service_id = apps.ServiceId[MySimpleData]("example-service")
47
63
  ctx = app_context.Context[MySimpleData].make(
64
+ service_id,
48
65
  MySimpleData(
49
66
  blob_path=("accountname", "containername", "/some/blob/path"),
50
- )
67
+ ),
51
68
  )
52
69
  ```
70
+
71
+ Args:
72
+ service_id: The service id the context values will be retrievable as.
73
+ *contexts: Zero or more [rats.app_context.T_ContextType][] instances.
53
74
  """
75
+ if len(contexts) > 0:
76
+ cls = contexts[0].__class__
77
+ dataclass_wizard.DumpMeta(key_transform="NONE").bind_to(cls) # type: ignore[reportUnknownMemberType]
54
78
  return Context(
55
79
  service_id,
56
80
  tuple([dataclass_wizard.asdict(ctx) for ctx in contexts]), # type: ignore
rats/cli/_click_app.py CHANGED
@@ -40,7 +40,7 @@ class ClickApp(apps.Executable):
40
40
  Attach any commands provided by the [rats.cli.Container][] and then execute the group.
41
41
 
42
42
  !!! note
43
- Currently, this method runs [click.BaseCommand.main][] without arguments, causing Click
43
+ Currently, this method runs [click.Command.main][] without arguments, causing Click
44
44
  to pull the command arguments from [sys.argv][]. If you are needing to change this,
45
45
  skip using this class and implement the custom logic yourself.
46
46
  """
rats/logs/__init__.py CHANGED
@@ -1,7 +1,22 @@
1
- """Small package to help configure logging for rats applications."""
1
+ """
2
+ Small module to help configure logging for rats applications.
2
3
 
3
- from ._app import ConfigureApplication
4
+ We provide a rats application that can be executed at the beginning of your process, that will
5
+ handle configuring the python logging libraries, with a few configuration options being exposed.
6
+
7
+ !!! warning
8
+ We expose the logging functionality through a [rats.apps.AppContainer][] in order to leverage
9
+ the built in plugin system to give users the ability to adjust the default settings, but this
10
+ entire application should be lightweight and should not contain very complex logic, avoiding
11
+ logic that is very time consuming or has a chance of failing with confusing errors.
12
+
13
+ If the logging options made available through this module are far from what is desired, instead
14
+ of adding flags and options to this module, we recommend configuring logging in your own code.
15
+ """
16
+
17
+ from ._app import AppConfigs, ConfigureApplication
4
18
 
5
19
  __all__ = [
20
+ "AppConfigs",
6
21
  "ConfigureApplication",
7
22
  ]
rats/logs/_app.py CHANGED
@@ -1,12 +1,58 @@
1
1
  import logging.config
2
+ import warnings
3
+ from collections.abc import Iterator
4
+ from typing import Any
2
5
 
3
6
  from rats import apps
4
7
 
5
8
  logger = logging.getLogger(__name__)
9
+ LoggerConfigEntry = tuple[str, dict[str, Any]]
10
+
11
+
12
+ @apps.autoscope
13
+ class AppConfigs:
14
+ LOGGERS = apps.ServiceId[LoggerConfigEntry]("loggers.config-group")
15
+ """
16
+ Register additional loggers to this service group.
17
+
18
+ ```python
19
+ from rats import apps
20
+
21
+
22
+ class PluginContainer(apps.Container, apps.PluginMixin):
23
+ @apps.group(AppConfigs.LOGGERS)
24
+ def _custom_loggers(self) -> Iterator[LoggerConfigEntry]:
25
+ yield "", {"level": "INFO", "handlers": ["console"]}
26
+ yield "azure", {"level": "WARNING", "handlers": ["console"]}
27
+
28
+
29
+ # provide the new configuration when executing `logs.ConfigureAppilcation`
30
+ apps.run(apps.AppBundle(
31
+ app_plugin=logs.ConfigureApplication,
32
+ container_plugin=PluginContainer,
33
+ ))
34
+ ```
35
+ """
6
36
 
7
37
 
8
38
  class ConfigureApplication(apps.AppContainer, apps.PluginMixin):
39
+ """
40
+ Configure logging for the current process.
41
+
42
+ We try to provide some simple default loggers, but you can replace the loggers by providing the
43
+ [rats.logs.AppConfigs.LOGGERS][] service group.
44
+
45
+ ```python
46
+ from rats import apps, logs
47
+
48
+ apps.run(apps.AppBundle(app_plugin=logs.ConfigureApplication))
49
+ ```
50
+ """
51
+
9
52
  def execute(self) -> None:
53
+ """Applies the logging configuration."""
54
+ logger_configs = self._app.get_group(AppConfigs.LOGGERS)
55
+ loggers_dict = {key: value for key, value in logger_configs}
10
56
  logging.config.dictConfig(
11
57
  {
12
58
  "version": 1,
@@ -36,10 +82,15 @@ class ConfigureApplication(apps.AppContainer, apps.PluginMixin):
36
82
  "stream": "ext://sys.stderr",
37
83
  }
38
84
  },
39
- "loggers": {
40
- "": {"level": "INFO", "handlers": ["console"]},
41
- "azure": {"level": "WARNING", "handlers": ["console"]},
42
- },
85
+ "loggers": loggers_dict,
43
86
  }
44
87
  )
88
+ # enable deprecation warnings by default
89
+ logging.captureWarnings(True)
90
+ warnings.simplefilter("default", DeprecationWarning)
45
91
  logger.debug("done configuring logging")
92
+
93
+ @apps.fallback_group(AppConfigs.LOGGERS)
94
+ def _default_loggers(self) -> Iterator[LoggerConfigEntry]:
95
+ yield "", {"level": "INFO", "handlers": ["console"]}
96
+ yield "azure", {"level": "WARNING", "handlers": ["console"]}
@@ -0,0 +1,64 @@
1
+ """
2
+ Run rats applications, providing external context.
3
+
4
+ The provided `rats-runtime` cli allows the registration and execution of [rats.apps.AppContainer][]
5
+ applications. Register to the python plugin group called `rats.runtime.apps`, and list available
6
+ plugins with `rats-runtime list`.
7
+
8
+ ```toml title="pyproject.toml"
9
+ [project.entry-points."rats.runtime.apps"]
10
+ "rats_e2e.runtime" = "rats_e2e.runtime:Application"
11
+ ```
12
+
13
+ We register an example application you can run with `rats-runtime run rats_e2e.runtime`. The name
14
+ of the registered entry point in your `pyproject.toml` will map to the name provided to
15
+ `rats-runtime run`.
16
+
17
+ ```bash
18
+ $ rats-runtime run rats_e2e.runtime
19
+ hello, world!
20
+ looking for any registered context: rats_e2e.runtime._app:AppServices[example-data]
21
+ ```
22
+
23
+ Our example application looks for any context services, which can be provided to the `run` command.
24
+
25
+ === "/code"
26
+ ```bash
27
+ $ rats-runtime run \
28
+ rats_e2e.runtime \
29
+ --context-file src/rats_resources/runtime/example-context.yaml
30
+ hello, world!
31
+ looking for any registered context: rats_e2e.runtime._app:AppServices[example-data]
32
+ found example data element: ExampleData(id='111', thing_a='111', thing_b='111')
33
+ found example data element: ExampleData(id='222', thing_a='222', thing_b='222')
34
+ ```
35
+ === "src/rats_resources/runtime/example-context.yaml"
36
+ ```yaml
37
+ --8<-- "rats_resources/runtime/example-context.yaml"
38
+ ```
39
+ === "src/rats_e2e/runtime/_app.py"
40
+ ```python
41
+ --8<-- "rats_e2e/runtime/_app.py"
42
+ ```
43
+ === "src/rats_e2e/runtime/_data.py"
44
+ ```python
45
+ --8<-- "rats_e2e/runtime/_data.py"
46
+ ```
47
+
48
+ !!! info
49
+ Most users won't typically use `rats-runtime` directly, but this interface is used by other
50
+ abstractions to execute applications remotely, like the [rats.aml][] module, adding the ability
51
+ to execute applications within an azure ml job.
52
+ """
53
+
54
+ from ._app import Application, AppServices, main
55
+ from ._request import DuplicateRequestError, Request, RequestNotFoundError
56
+
57
+ __all__ = [
58
+ "AppServices",
59
+ "Application",
60
+ "DuplicateRequestError",
61
+ "Request",
62
+ "RequestNotFoundError",
63
+ "main",
64
+ ]
@@ -0,0 +1,5 @@
1
+ """Run the rats-runtime commands."""
2
+
3
+ from rats import runtime
4
+
5
+ runtime.main()
rats/runtime/_app.py ADDED
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import sys
6
+ from collections.abc import Iterator
7
+ from functools import cache
8
+ from importlib import metadata
9
+ from importlib.metadata import EntryPoint
10
+ from pathlib import Path
11
+ from typing import Any, final
12
+
13
+ import click
14
+ import yaml
15
+
16
+ from rats import app_context, apps, cli, logs
17
+
18
+ from ._request import DuplicateRequestError, Request, RequestNotFoundError
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @apps.autoscope
24
+ class AppServices:
25
+ """Services used by the `rats.runtime.Application` cli command."""
26
+
27
+ CONTEXT = apps.ServiceId[app_context.Collection[Any]]("app-ctx-collection.config")
28
+ """
29
+ [rats.app_context.Collection][] available in the application run using `rats-runtime run`.
30
+
31
+ ```python
32
+ from rats import apps, runtime
33
+
34
+
35
+ class Application(apps.AppContainer, apps.PluginMixin):
36
+ def execute(self) -> None:
37
+ context = self._app.get(runtime.AppConfigs.CONTEXT)
38
+ print("loaded context:")
39
+ for item in context_collection.items:
40
+ print(f"{item.service_id} -> {item.values}")
41
+ ```
42
+ """
43
+ REQUEST = apps.ServiceId[Request]("runtime-details-client")
44
+ """
45
+ The request information, available after the call to [rats.runtime.Application.execute][].
46
+ """
47
+
48
+
49
+ @final
50
+ class Application(apps.AppContainer, cli.Container, apps.PluginMixin):
51
+ """
52
+ The `rats-runtime` cli application.
53
+
54
+ ```bash
55
+ $ rats-runtime --help
56
+ Usage: rats-runtime [OPTIONS] COMMAND [ARGS]...
57
+
58
+ Options:
59
+ --help Show this message and exit.
60
+
61
+ Commands:
62
+ list List all the exes and groups that are configured to run with `rats-
63
+ runtime run`.
64
+ run Run one or more apps, adding an optional additional context.
65
+ ```
66
+ """
67
+
68
+ _request: Request | None = None
69
+
70
+ def execute(self) -> None:
71
+ """Runs the `rats-runtime` cli."""
72
+ argv = self._app.get(cli.PluginConfigs.ARGV)
73
+ cli.create_group(click.Group("rats-runtime"), self).main(
74
+ args=argv[1:],
75
+ prog_name=Path(argv[0]).name,
76
+ auto_envvar_prefix="RATS_RUNTIME",
77
+ # don't end the process
78
+ standalone_mode=False,
79
+ )
80
+
81
+ @cli.command()
82
+ def _list(self) -> None:
83
+ """List all the exes and groups that are configured to run with `rats-runtime run`."""
84
+ for entry in self._get_app_entrypoints():
85
+ click.echo(entry.name)
86
+
87
+ @cli.command()
88
+ @click.argument("app-ids", nargs=-1)
89
+ @click.option("--context", default="{}")
90
+ @click.option("--context-file")
91
+ def _run(self, app_ids: tuple[str, ...], context: str, context_file: str | None) -> None:
92
+ """Run one or more apps, adding an optional additional context."""
93
+ if self._request is not None:
94
+ print(self._request)
95
+ raise DuplicateRequestError()
96
+
97
+ ctx_collection = app_context.loads(context)
98
+ if context_file:
99
+ p = Path(context_file)
100
+ if not p.is_file():
101
+ raise RuntimeError(f"context file not found: {context_file}")
102
+
103
+ data = yaml.safe_load(p.read_text())
104
+ ctx_collection = ctx_collection.merge(app_context.loads(json.dumps(data)))
105
+
106
+ self._request = Request(
107
+ app_ids=app_ids,
108
+ context=ctx_collection,
109
+ )
110
+
111
+ def _load_app(name: str, ctx: app_context.Collection[Any]) -> apps.AppContainer:
112
+ return apps.AppBundle(
113
+ app_plugin=self._find_app(name),
114
+ context=apps.StaticContainer(
115
+ apps.StaticProvider(
116
+ namespace=apps.ProviderNamespaces.SERVICES,
117
+ service_id=AppServices.CONTEXT,
118
+ call=lambda: ctx,
119
+ ),
120
+ ),
121
+ )
122
+
123
+ if len(app_ids) == 0:
124
+ logger.warning("No applications were passed to the command")
125
+
126
+ for app_id in app_ids:
127
+ app = _load_app(app_id, ctx_collection)
128
+ app.execute()
129
+
130
+ def _find_app(self, name: str) -> type[apps.AppContainer]:
131
+ for e in self._get_app_entrypoints():
132
+ if e.name == name:
133
+ return e.load()
134
+
135
+ raise RuntimeError(f"rats app-id not found: {name}")
136
+
137
+ @cache # noqa: B019
138
+ def _get_app_entrypoints(self) -> Iterator[EntryPoint]:
139
+ yield from metadata.entry_points(group="rats.runtime.apps")
140
+
141
+ @apps.service(AppServices.REQUEST)
142
+ def _request_provider(self) -> Request:
143
+ if self._request is None:
144
+ raise RequestNotFoundError()
145
+
146
+ return self._request
147
+
148
+ @apps.container()
149
+ def _plugins(self) -> apps.Container:
150
+ return cli.PluginContainer(self._app)
151
+
152
+
153
+ def main() -> None:
154
+ """Main entry-point to the `rats-runtime` cli command."""
155
+ try:
156
+ apps.run_plugin(logs.ConfigureApplication)
157
+ apps.run_plugin(Application)
158
+ except click.exceptions.ClickException as e:
159
+ e.show()
160
+ sys.exit(e.exit_code)
@@ -0,0 +1,23 @@
1
+ from typing import Any, NamedTuple
2
+
3
+ from rats import app_context
4
+
5
+
6
+ class Request(NamedTuple):
7
+ app_ids: tuple[str, ...]
8
+ """The app ids being run by [rats.runtime.Application][]."""
9
+
10
+ context: app_context.Collection[Any]
11
+ """The [rats.app_context.Collection][] that was submitted and made available to executed apps."""
12
+
13
+
14
+ class RequestNotFoundError(RuntimeError):
15
+ """Thrown if [rats.runtime.AppServices.REQUEST][] is accessed without the application being run."""
16
+
17
+ def __init__(self) -> None:
18
+ super().__init__("no running runtime request found.")
19
+
20
+
21
+ class DuplicateRequestError(RuntimeError):
22
+ def __init__(self) -> None:
23
+ super().__init__("request already executed.")
rats/runtime/py.typed ADDED
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: rats-apps
3
- Version: 0.10.3.dev20250520163928
3
+ Version: 0.11.0.dev20250521180658
4
4
  Summary: research analysis tools for building applications
5
5
  License: MIT
6
6
  Keywords: pipelines,machine learning,research
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Dist: click
17
17
  Requires-Dist: colorlog
18
18
  Requires-Dist: dataclass-wizard
19
+ Requires-Dist: pyyaml
19
20
  Requires-Dist: typing-extensions
20
21
  Project-URL: Documentation, https://microsoft.github.io/rats
21
22
  Project-URL: Repository, https://github.com/microsoft/rats
@@ -2,10 +2,10 @@ rats/annotations/__init__.py,sha256=YmydaopUSlpoX6MvQ-3p18sgEC_DB_y683wVICD-jnU,
2
2
  rats/annotations/__main__.py,sha256=vlzQOM9y82P0OL5tYcmSM_4jTg0s8jayAcvEoi9cBvI,1065
3
3
  rats/annotations/_functions.py,sha256=UkHh3zdBivluE7dBeGQ17zoIfGdyIokMAkFmpWaIlDc,4284
4
4
  rats/annotations/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- rats/app_context/__init__.py,sha256=WHFPJXl7WHHIgfQxzmDbGHIW8YTbDsCrusLlFctGW_o,447
6
- rats/app_context/_collection.py,sha256=5-Nog2Du2v0tAZbhsbpg3vMjMcZ--hSZ4acWM2HTlMY,3717
5
+ rats/app_context/__init__.py,sha256=8d8LCe2fOi4_W1bPnJlk5t_HETHW8sV4c35z5iwAuxo,2529
6
+ rats/app_context/_collection.py,sha256=f9sqdAokVsyt7zFz0Va7_48wK18wT44H7berPrx2RpM,5571
7
7
  rats/app_context/_container.py,sha256=YkNG25jngfN_064jv8fZmKW1FSuL6RRJAp9F_hm2NZg,2954
8
- rats/app_context/_context.py,sha256=VUm-cg2WKtFU2VvIJI8OmTdRRoDLXeWPBYu_ynpUjlY,1599
8
+ rats/app_context/_context.py,sha256=S7D8NLefZJI4MDEpYKdenfNsemus4bUlQCG-gd3U3g8,2777
9
9
  rats/app_context/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  rats/apps/__init__.py,sha256=6B5qQvzx1xlRrNRY4QJi6GEywuQ1MoW454kC4nO2Yqg,2084
11
11
  rats/apps/_annotations.py,sha256=6M_M7K8haNVda0Tx02EpFf3s9EjnWYacNMjTIkNEdRU,4617
@@ -24,15 +24,20 @@ rats/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  rats/cli/__init__.py,sha256=zMywV-4vqbtlobIoMItwbxxdvYGC20wiGU2TKm3II2A,714
25
25
  rats/cli/__main__.py,sha256=WeldAKjA3Kmz9ZRnZVd3G8Ud66Y_gSRDLIPNE1JyhV0,1418
26
26
  rats/cli/_annotations.py,sha256=-B2Y1bYjPbeTNTBMZzMdAU92qu9AyutKHN_NdFy-xhA,2964
27
- rats/cli/_click_app.py,sha256=4z2jiRHafCAN7S-cbMhfQJhLGwl6ljFS3r6P1COgR00,1454
27
+ rats/cli/_click_app.py,sha256=Jvs6OqNC4Yoe6kbc2tCzjzUieNFa1n_zwXPsdo971Wc,1450
28
28
  rats/cli/_command.py,sha256=kyU3UqqF9aiTTaFvlQFBKDLXvArQS1QgjoQqlMbKzok,597
29
29
  rats/cli/_container.py,sha256=FIQBqi9PTNpE2P52qkfGET51BczdD-JvcWXLTjCNPDI,3512
30
30
  rats/cli/_functions.py,sha256=BNmgWVquQUEqJAYsed_l8vLnlLP7u3XC1TDyEFI1AiU,1552
31
31
  rats/cli/_plugin.py,sha256=o-pmEqU6mVH3QoRfRBrbG-XRTWCzt6pLKtSV3-5VSx0,1144
32
32
  rats/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- rats/logs/__init__.py,sha256=sizLa6JbqekhJGKDyRZUUQvTuqiJx6yxwA5jT-tDQck,152
34
- rats/logs/_app.py,sha256=TF6rfQXQfaHd3AFW2anAoAGaxzU31kqJMoKkjuBtbF0,1617
33
+ rats/logs/__init__.py,sha256=dSO-V2eu2W7w4w7oRau-UOfXXIkyZ2bHB6-kaasbeFM,970
34
+ rats/logs/_app.py,sha256=SoTxk6g84tygOP_346NwXPcG1o6KZLd7lIgayq5vMdc,3239
35
35
  rats/logs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ rats/runtime/__init__.py,sha256=f9aEnynHuePP9OP5XCtB-6nCBvNWWlQQLTzuxFEueoQ,2150
37
+ rats/runtime/__main__.py,sha256=y01yOymsL075poX95pc02sJR1HD0pDNFRZdpOdi0R6Y,79
38
+ rats/runtime/_app.py,sha256=zR4o5kd3Xg6oJB-S92iG5ffoUafG1xDYQfcTBnshUuA,5001
39
+ rats/runtime/_request.py,sha256=E2Oos_mkruPh4h-chys64AMFR_YMdrAx95avXsPeaog,706
40
+ rats/runtime/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
41
  rats_e2e/apps/__init__.py,sha256=c2QG818ACyS9MBXs_Evg5yYT6k-KPMQfIqF7Z77Cau0,142
37
42
  rats_e2e/apps/__main__.py,sha256=ETECGDD8glcbmx0P-WWmjQQX6gtF-5L7_DxZjNmx3X4,350
38
43
  rats_e2e/apps/_example_cli.py,sha256=XzWOdnQrSp-F_FZEdm2CCm8dUJZWOSm2KFPIK0jzoi8,2840
@@ -42,6 +47,14 @@ rats_e2e/apps/inputs/_app.py,sha256=FiaLgOZc-d1ryKSwKnL5XBNGcOP1bHbxxeMJqoU_RJg,
42
47
  rats_e2e/apps/minimal/__init__.py,sha256=bUR6Oexx6Jsouxor0cL9emXoVha4cm3WqyhU1pgchsI,521
43
48
  rats_e2e/apps/minimal/__main__.py,sha256=Mf-a2iQKTTgh9hMd6AeuzmU9araMIyf1AtdWkh_L07E,117
44
49
  rats_e2e/apps/minimal/_app.py,sha256=CQ09LVTNRarz7Pb1wiSuNHrZ_2KGcgH8nUqy4BjxMUY,849
45
- rats_apps-0.10.3.dev20250520163928.dist-info/METADATA,sha256=8S_U7EpOEgolriB2hEv7VauOv1PKzo-R63MmSwFMXfE,867
46
- rats_apps-0.10.3.dev20250520163928.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
47
- rats_apps-0.10.3.dev20250520163928.dist-info/RECORD,,
50
+ rats_e2e/apps/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ rats_e2e/runtime/__init__.py,sha256=0oWKhywQaXzjs2bHI3WHYBF8vIbK-NAWBJsxTal7sC0,163
52
+ rats_e2e/runtime/_app.py,sha256=hz0BB15CIkjLsz8YPfZiIm9jKiOq8JFaHR7_cJGBkMA,790
53
+ rats_e2e/runtime/_data.py,sha256=3d1F_JO2gEOPUjBp_KYMP3TefyneiG_ktlJjdIIYUy8,125
54
+ rats_e2e/runtime/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
+ rats_resources/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ rats_resources/runtime/example-context.yaml,sha256=eiLsNFquFfkIpUhxUCQLzLigH21QF2F00fzA_e_aOKk,215
57
+ rats_apps-0.11.0.dev20250521180658.dist-info/METADATA,sha256=dcJpnIbCfQHHsWtD0bjxHQ26671OecfVVNeISRUoJYg,889
58
+ rats_apps-0.11.0.dev20250521180658.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
59
+ rats_apps-0.11.0.dev20250521180658.dist-info/entry_points.txt,sha256=Gf6bPwxIVjWd3Xx71upZo7eDJA5cujniLew6fxJMgA4,117
60
+ rats_apps-0.11.0.dev20250521180658.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ rats-runtime=rats.runtime:main
3
+
4
+ [rats.runtime.apps]
5
+ rats_e2e.runtime=rats_e2e.runtime:Application
6
+
rats_e2e/apps/py.typed ADDED
File without changes
@@ -0,0 +1,7 @@
1
+ """A small example application, registered to be run with `rats-runtime run rats_e2e.runtime`."""
2
+
3
+ from ._app import Application
4
+
5
+ __all__ = [
6
+ "Application",
7
+ ]
@@ -0,0 +1,23 @@
1
+ from collections.abc import Iterator
2
+
3
+ from rats import apps, runtime
4
+
5
+ from ._data import ExampleData
6
+
7
+
8
+ @apps.autoscope
9
+ class AppServices:
10
+ EXAMPLE_DATA = apps.ServiceId[ExampleData]("example-data")
11
+
12
+
13
+ class Application(apps.AppContainer, apps.PluginMixin):
14
+ def execute(self) -> None:
15
+ print("hello, world!")
16
+ print(f"looking for any registered context: {AppServices.EXAMPLE_DATA.name}")
17
+ for data in self._app.get_group(AppServices.EXAMPLE_DATA):
18
+ print(f"found example data element: {data}")
19
+
20
+ @apps.fallback_group(AppServices.EXAMPLE_DATA)
21
+ def _data_from_context(self) -> Iterator[ExampleData]:
22
+ collection = self._app.get(runtime.AppServices.CONTEXT)
23
+ yield from collection.decoded_values(ExampleData, AppServices.EXAMPLE_DATA)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class ExampleData:
6
+ id: str
7
+ thing_a: str
8
+ thing_b: str
File without changes
File without changes
@@ -0,0 +1,9 @@
1
+ items:
2
+ - service_id: ["rats_e2e.runtime._app:AppServices[example-data]"]
3
+ values:
4
+ - id: "111"
5
+ thing_a: "111"
6
+ thing_b: "111"
7
+ - id: "222"
8
+ thing_a: "222"
9
+ thing_b: "222"