Skip to content

Commit

Permalink
rats.apps optimizations and dx (#329)
Browse files Browse the repository at this point in the history
trying to tackle some performance issues and improving the api for
defining service groups by making group providers generators.

this api breaking change should only require `return` statements in
service group providers to be switched to `yield` along with the
necessary type annotation change to `Iterator[T_ServiceType]`.

this pr also renames a few private details and makes `rats.examples`
public. my goal for the examples package is to turn it into an app that
allows others to register examples as they develop libraries.
  • Loading branch information
ms-lolo authored Sep 28, 2024
1 parent 14a9dae commit a4de54b
Show file tree
Hide file tree
Showing 42 changed files with 618 additions and 555 deletions.
2 changes: 1 addition & 1 deletion bin/rats-devtools.setup
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ cat <<EOF> $LOCAL_PATH
#!/bin/bash
export DEVTOOLS_PROJECT_ROOT=${REPO_DIR}
exec ${REPO_DIR}/rats-devtools/.tmp/venv-${LOCAL_SUFFIX}/bin/python -m rats._dev "\$@"
exec ${REPO_DIR}/rats-devtools/.tmp/venv-${LOCAL_SUFFIX}/bin/python -m rats._local "\$@"
EOF
chmod +x $LOCAL_PATH
Expand Down
54 changes: 28 additions & 26 deletions rats-apps/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rats-apps/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "rats-apps"
description = "research analysis tools for building applications"
version = "0.2.0"
version = "0.3.0"
readme = "README.md"
authors = []
packages = [
Expand Down
12 changes: 8 additions & 4 deletions rats-apps/src/python/rats/apps/_annotations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from collections.abc import Callable
from collections.abc import Callable, Iterator
from typing import Any, Concatenate, Generic, NamedTuple, ParamSpec, TypeVar, cast

from rats import annotations
Expand Down Expand Up @@ -26,7 +26,7 @@ def autoid_service(fn: Callable[P, T_ServiceType]) -> Callable[P, T_ServiceType]

def group(
group_id: ServiceId[T_ServiceType],
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
) -> Callable[[Callable[P, Iterator[T_ServiceType]]], Callable[P, Iterator[T_ServiceType]]]:
"""A group is a collection of services."""
return annotations.annotation(ProviderNamespaces.GROUPS, cast(NamedTuple, group_id))

Expand All @@ -43,7 +43,7 @@ def fallback_service(

def fallback_group(
group_id: ServiceId[T_ServiceType],
) -> Callable[[Callable[P, T_ServiceType]], Callable[P, T_ServiceType]]:
) -> Callable[[Callable[P, Iterator[T_ServiceType]]], Callable[P, Iterator[T_ServiceType]]]:
"""A fallback group gets used if no group is defined."""
return annotations.annotation(
ProviderNamespaces.FALLBACK_GROUPS,
Expand All @@ -69,7 +69,7 @@ def factory(*args: P.args, **kwargs: P.kwargs) -> R:
return new_method


class factory_service(Generic[P, R]):
class _FactoryService(Generic[P, R]):
"""
A decorator to create a factory service.
Expand All @@ -89,6 +89,10 @@ def __call__(
return service(self._service_id)(new_method)


# alias so we can think of it as a function
factory_service = _FactoryService


def autoid_factory_service(
method: Callable[Concatenate[T_Container, P], R],
) -> Callable[[T_Container], Callable[P, R]]:
Expand Down
98 changes: 70 additions & 28 deletions rats-apps/src/python/rats/apps/_container.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
from abc import abstractmethod
from collections.abc import Callable, Iterator
from typing import Generic, NamedTuple, ParamSpec, Protocol, cast
from typing import Any, Generic, NamedTuple, ParamSpec, Protocol, cast

from typing_extensions import NamedTuple as ExtNamedTuple

from rats import annotations

Expand Down Expand Up @@ -109,41 +111,81 @@ def get_group(
) -> Iterator[T_ServiceType]:
"""Retrieve a service group by its id."""
if not self.has_namespace(ProviderNamespaces.GROUPS, group_id):
yield from self.get_namespaced_group(ProviderNamespaces.FALLBACK_GROUPS, group_id)
# groups are expected to return iterable services
# TODO: we need to clean up the meaning of groups and services somehow
for i in self.get_namespaced_group(ProviderNamespaces.FALLBACK_GROUPS, group_id):
yield from cast(Iterator[T_ServiceType], i)

yield from self.get_namespaced_group(ProviderNamespaces.GROUPS, group_id)
for i in self.get_namespaced_group(ProviderNamespaces.GROUPS, group_id):
yield from cast(Iterator[T_ServiceType], i)

def get_namespaced_group(
self,
namespace: str,
group_id: ServiceId[T_ServiceType],
) -> Iterator[T_ServiceType]:
"""Retrieve a service group by its id, within a given service namespace."""
tates = annotations.get_class_annotations(type(self))
# containers are a special service namespace that we look through recursively
containers = tates.with_namespace(ProviderNamespaces.CONTAINERS)
groups = tates.with_group(namespace, cast(NamedTuple, group_id))

for annotation in groups.annotations:
if not hasattr(self, f"__rats_cache_{annotation.name}_{group_id.name}"):
setattr(
self,
f"__rats_cache_{annotation.name}_{group_id.name}",
getattr(self, annotation.name)(),
)

yield getattr(self, f"__rats_cache_{annotation.name}_{group_id.name}")

for annotation in containers.annotations:
if not hasattr(self, f"__rats_container_cache_{annotation.name}"):
setattr(
self,
f"__rats_container_cache_{annotation.name}",
getattr(self, annotation.name)(),
)

c: Container = getattr(self, f"__rats_container_cache_{annotation.name}")
yield from c.get_namespaced_group(namespace, group_id)
yield from _get_cached_services_for_group(self, namespace, group_id)

for subcontainer in _get_subcontainers(self):
yield from subcontainer.get_namespaced_group(namespace, group_id)


def _get_subcontainers(c: Container) -> Iterator[Container]:
yield from _get_cached_services_for_group(
c, ProviderNamespaces.CONTAINERS, DEFAULT_CONTAINER_GROUP
)


class _ProviderInfo(ExtNamedTuple, Generic[T_ServiceType]):
attr: str
group_id: ServiceId[T_ServiceType]


def _get_cached_services_for_group(
c: Container,
namespace: str,
group_id: ServiceId[T_ServiceType],
) -> Iterator[T_ServiceType]:
provider_cache = _get_provider_cache(c)
info_cache = _get_provider_info_cache(c)

if (namespace, group_id) not in info_cache:
info_cache[(namespace, group_id)] = list(_get_providers_for_group(c, namespace, group_id))

for provider in info_cache[(namespace, group_id)]:
if provider not in provider_cache:
provider_cache[provider] = getattr(c, provider.attr)()

yield provider_cache[provider]


def _get_provider_cache(obj: object) -> dict[_ProviderInfo[Any], Any]:
if not hasattr(obj, "__rats_apps_provider_cache__"):
obj.__rats_apps_provider_cache__ = {} # type: ignore[reportAttributeAccessIssue]

return obj.__rats_apps_provider_cache__ # type: ignore[reportAttributeAccessIssue]


def _get_provider_info_cache(
obj: object,
) -> dict[tuple[str, ServiceId[Any]], list[_ProviderInfo[Any]]]:
if not hasattr(obj, "__rats_apps_provider_info_cache__"):
obj.__rats_apps_provider_info_cache__ = {} # type: ignore[reportAttributeAccessIssue]

return obj.__rats_apps_provider_info_cache__ # type: ignore[reportAttributeAccessIssue]


def _get_providers_for_group(
c: Container,
namespace: str,
group_id: ServiceId[T_ServiceType],
) -> Iterator[_ProviderInfo[T_ServiceType]]:
tates = annotations.get_class_annotations(type(c))
groups = tates.with_group(namespace, cast(NamedTuple, group_id))

for annotation in groups.annotations:
yield _ProviderInfo(annotation.name, group_id)


DEFAULT_CONTAINER_GROUP = ServiceId[Container](f"{__name__}:__default__")
Expand Down
5 changes: 3 additions & 2 deletions rats-apps/src/python/rats/logs/_plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging.config
from collections.abc import Iterator

from rats import apps

Expand All @@ -22,9 +23,9 @@ def __init__(self, app: apps.Container) -> None:
self._app = app

@apps.group(PluginServices.EVENTS.CONFIGURE_LOGGING)
def _configure_logging(self) -> apps.Executable:
def _configure_logging(self) -> Iterator[apps.Executable]:
# in the future, we can use this plugin to make logging easily configurable
return apps.App(
yield apps.App(
lambda: logging.config.dictConfig(
{
"version": 1,
Expand Down
14 changes: 8 additions & 6 deletions rats-apps/test/python/rats_test/apps/example/_example_groups.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections.abc import Iterator

from rats import apps

from ._ids import ExampleIds
Expand All @@ -11,12 +13,12 @@ def __init__(self, app: apps.Container) -> None:
self._app = app

@apps.fallback_group(ExampleIds.GROUPS.STORAGE)
def fallback_group_1(self) -> StorageClient:
return StorageClient(self._app.get(ExampleIds.CONFIGS.STORAGE))
def fallback_group_1(self) -> Iterator[StorageClient]:
yield StorageClient(self._app.get(ExampleIds.CONFIGS.STORAGE))

@apps.fallback_group(ExampleIds.GROUPS.STORAGE)
def fallback_group_2(self) -> StorageClient:
return StorageClient(self._app.get(ExampleIds.CONFIGS.STORAGE))
def fallback_group_2(self) -> Iterator[StorageClient]:
yield StorageClient(self._app.get(ExampleIds.CONFIGS.STORAGE))


class ExampleGroupsPlugin2(apps.Container):
Expand All @@ -26,5 +28,5 @@ def __init__(self, app: apps.Container) -> None:
self._app = app

@apps.group(ExampleIds.GROUPS.STORAGE)
def storage_group_1(self) -> StorageClient:
return StorageClient(self._app.get(ExampleIds.CONFIGS.STORAGE))
def storage_group_1(self) -> Iterator[StorageClient]:
yield StorageClient(self._app.get(ExampleIds.CONFIGS.STORAGE))
2 changes: 1 addition & 1 deletion rats-devtools/bin/rats-devtools
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
DEVTOOLS_DIR="$SCRIPT_DIR/.."

exec $DEVTOOLS_DIR/.venv/bin/python -m rats._dev "$@"
exec $DEVTOOLS_DIR/.venv/bin/python -m rats._local "$@"
2 changes: 1 addition & 1 deletion rats-devtools/bin/rats-examples
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
DEVTOOLS_DIR="$SCRIPT_DIR/.."

exec $DEVTOOLS_DIR/.venv/bin/python -m rats._examples "$@"
exec $DEVTOOLS_DIR/.venv/bin/python -m rats.examples "$@"
Loading

0 comments on commit a4de54b

Please sign in to comment.