Skip to content

Commit

Permalink
🐛 FlamaWorker only when sqlalchemy is available
Browse files Browse the repository at this point in the history
  • Loading branch information
perdy committed Nov 26, 2024
1 parent 2498889 commit 89fcb87
Show file tree
Hide file tree
Showing 19 changed files with 285 additions and 134 deletions.
17 changes: 8 additions & 9 deletions flama/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import typing as t

from flama import asgi, exceptions, http, injection, types, url, validation, websockets
from flama.ddd.components import WorkerComponent
from flama.events import Events
from flama.middleware import MiddlewareStack
from flama.models.modules import ModelsModule
Expand All @@ -14,10 +15,8 @@
from flama.schemas.modules import SchemaModule

try:
from flama.ddd.components import WorkerComponent
from flama.resources.workers import FlamaWorker
except AssertionError:
WorkerComponent = None
except exceptions.DependencyNotInstalled:
FlamaWorker = None

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -87,10 +86,14 @@ def __init__(
}
)

# Initialise components
default_components = []

# Create worker
worker = FlamaWorker() if FlamaWorker else None
if (worker := FlamaWorker() if FlamaWorker else None) and WorkerComponent:
default_components.append(WorkerComponent(worker=worker))

# Initialize Modules
# Initialise modules
default_modules = [
ResourcesModule(worker=worker),
SchemaModule(title, version, description, schema=schema, docs=docs),
Expand All @@ -99,10 +102,6 @@ def __init__(
self.modules = Modules(app=self, modules={*default_modules, *(modules or [])})

# Initialize router
default_components = []
if worker and WorkerComponent:
default_components.append(WorkerComponent(worker=worker))

self.app = self.router = Router(
routes=routes, components=[*default_components, *(components or [])], lifespan=lifespan
)
Expand Down
10 changes: 8 additions & 2 deletions flama/ddd/repositories/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import abc

__all__ = ["AbstractRepository"]
__all__ = ["AbstractRepository", "BaseRepository"]


class AbstractRepository(abc.ABC):
"""Base class for repositories."""
"""Abstract class for repositories."""

def __init__(self, *args, **kwargs):
...


class BaseRepository(AbstractRepository):
"""Base class for repositories."""

...
4 changes: 2 additions & 2 deletions flama/ddd/repositories/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
import httpx

from flama.ddd import exceptions
from flama.ddd.repositories import AbstractRepository
from flama.ddd.repositories import BaseRepository

if t.TYPE_CHECKING:
from flama.client import Client

__all__ = ["HTTPRepository", "HTTPResourceManager", "HTTPResourceRepository"]


class HTTPRepository(AbstractRepository):
class HTTPRepository(BaseRepository):
def __init__(self, client: "Client", *args, **kwargs):
super().__init__(*args, **kwargs)
self._client = client
Expand Down
4 changes: 2 additions & 2 deletions flama/ddd/repositories/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from flama import exceptions
from flama.ddd import exceptions as ddd_exceptions
from flama.ddd.repositories import AbstractRepository
from flama.ddd.repositories import BaseRepository

try:
import sqlalchemy
Expand All @@ -17,7 +17,7 @@
__all__ = ["SQLAlchemyRepository", "SQLAlchemyTableManager", "SQLAlchemyTableRepository"]


class SQLAlchemyRepository(AbstractRepository):
class SQLAlchemyRepository(BaseRepository):
"""Base class for SQLAlchemy repositories. It provides a connection to the database."""

def __init__(self, connection: AsyncConnection, *args, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions flama/ddd/workers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from flama.ddd.workers.base import * # noqa
from flama.ddd.workers.http import * # noqa
from flama.ddd.workers.noop import * # noqa
94 changes: 41 additions & 53 deletions flama/ddd/workers/base.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,29 @@
import abc
import asyncio
import inspect
import logging
import typing as t

from flama.ddd.repositories import AbstractRepository
from flama.ddd.repositories import BaseRepository
from flama.exceptions import ApplicationError

if t.TYPE_CHECKING:
from flama import Flama

logger = logging.getLogger(__name__)

Repositories = t.NewType("Repositories", dict[str, type[AbstractRepository]])
__all__ = ["AbstractWorker", "BaseWorker"]

__all__ = ["WorkerType", "AbstractWorker", "Worker"]


class WorkerType(abc.ABCMeta):
"""Metaclass for workers.
It will gather all the repositories defined in the class as class attributes as a single dictionary under the name
`_repositories`.
"""

def __new__(mcs, name: str, bases: tuple[type], namespace: dict[str, t.Any]):
if not mcs._is_abstract(namespace) and "__annotations__" in namespace:
namespace["_repositories"] = Repositories(
{
k: v
for k, v in namespace["__annotations__"].items()
if inspect.isclass(v) and issubclass(v, AbstractRepository)
}
)

namespace["__annotations__"] = {
k: v for k, v in namespace["__annotations__"].items() if k not in namespace["_repositories"]
}

return super().__new__(mcs, name, bases, namespace)

@staticmethod
def _is_abstract(namespace: dict[str, t.Any]) -> bool:
return namespace.get("__module__") == "flama.ddd.workers" and namespace.get("__qualname__") == "AbstractWorker"
Repositories = t.NewType("Repositories", dict[str, type[BaseRepository]])


class AbstractWorker(abc.ABC, metaclass=WorkerType):
class AbstractWorker(abc.ABC):
"""Abstract class for workers.
It will be used to define the workers for the application. A worker consists of a set of repositories that will be
used to interact with entities and a mechanism for isolate a single unit of work.
It will be used to define the workers for the application. A worker must provide a mechanism to isolate a single
unit of work that will be used to interact with the repositories and entities of the application.
"""

_repositories: t.ClassVar[dict[str, type[AbstractRepository]]]

def __init__(self, app: t.Optional["Flama"] = None):
"""Initialize the worker.
Expand Down Expand Up @@ -125,27 +95,45 @@ async def rollback(self) -> None:
...


class Worker(AbstractWorker):
"""Worker class.
class WorkerType(abc.ABCMeta):
"""Metaclass for workers.
A basic implementation of the worker class that does not apply any specific behavior.
It will gather all the repositories defined in the class as class attributes as a single dictionary under the name
`_repositories` and remove them from the class annotations.
"""

async def begin(self) -> None:
"""Start a unit of work."""
...
def __new__(mcs, name: str, bases: tuple[type], namespace: dict[str, t.Any]):
if not mcs._is_base(namespace) and "__annotations__" in namespace:
namespace["_repositories"] = Repositories(
{k: v for k, v in namespace["__annotations__"].items() if mcs._is_repository(v)}
)

async def end(self, *, rollback: bool = False) -> None:
"""End a unit of work.
namespace["__annotations__"] = {
k: v for k, v in namespace["__annotations__"].items() if k not in namespace["_repositories"]
}

:param rollback: If the unit of work should be rolled back.
"""
...
return super().__new__(mcs, name, bases, namespace)

async def commit(self) -> None:
"""Commit the unit of work."""
...
@staticmethod
def _is_base(namespace: dict[str, t.Any]) -> bool:
return namespace.get("__module__") == "flama.ddd.workers" and namespace.get("__qualname__") == "BaseWorker"

async def rollback(self) -> None:
"""Rollback the unit of work."""
...
@staticmethod
def _is_repository(obj: t.Any) -> bool:
try:
return issubclass(obj, BaseRepository)
except TypeError:
return False


class BaseWorker(AbstractWorker, metaclass=WorkerType):
"""Base class for workers.
It will be used to define the workers for the application. A worker consists of a set of repositories that will be
used to interact with entities and a mechanism for isolate a single unit of work.
It will gather all the repositories defined in the class as class attributes as a single dictionary under the name
`_repositories` and remove them from the class annotations.
"""

_repositories: t.ClassVar[dict[str, type[BaseRepository]]]
4 changes: 2 additions & 2 deletions flama/ddd/workers/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing as t

from flama.ddd.workers.base import AbstractWorker
from flama.ddd.workers.base import BaseWorker

if t.TYPE_CHECKING:
from flama import Flama
Expand All @@ -9,7 +9,7 @@
__all__ = ["HTTPWorker"]


class HTTPWorker(AbstractWorker):
class HTTPWorker(BaseWorker):
"""Worker for HTTP client.
It will provide a flama Client and create the repositories for the corresponding resources.
Expand Down
27 changes: 27 additions & 0 deletions flama/ddd/workers/noop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from flama.ddd.workers.base import BaseWorker


class NoopWorker(BaseWorker):
"""Worker that does not apply any specific behavior.
A basic implementation of the worker class that does not apply any specific behavior.
"""

async def begin(self) -> None:
"""Start a unit of work."""
...

async def end(self, *, rollback: bool = False) -> None:
"""End a unit of work.
:param rollback: If the unit of work should be rolled back.
"""
...

async def commit(self) -> None:
"""Commit the unit of work."""
...

async def rollback(self) -> None:
"""Rollback the unit of work."""
...
4 changes: 2 additions & 2 deletions flama/ddd/workers/sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

from flama import exceptions
from flama.ddd.workers.base import AbstractWorker
from flama.ddd.workers.base import BaseWorker

try:
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncTransaction
Expand All @@ -16,7 +16,7 @@
logger = logging.getLogger(__name__)


class SQLAlchemyWorker(AbstractWorker):
class SQLAlchemyWorker(BaseWorker):
"""Worker for SQLAlchemy.
It will provide a connection and a transaction to the database and create the repositories for the entities.
Expand Down
21 changes: 20 additions & 1 deletion flama/resources/modules.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import inspect
import typing as t

from flama import exceptions
from flama.modules import Module
from flama.resources.resource import Resource
from flama.resources.routing import ResourceRoute

if t.TYPE_CHECKING:
try:
from flama.ddd.repositories.sqlalchemy import SQLAlchemyTableRepository
from flama.resources.workers import FlamaWorker
except AssertionError:
except exceptions.DependencyNotInstalled:
...


Expand Down Expand Up @@ -60,3 +62,20 @@ def decorator(resource: type[Resource]) -> type[Resource]:
return resource

return decorator

def add_repository(self, name: str, repository: type["SQLAlchemyTableRepository"]) -> None:
"""Register a repository.
:param name: The name of the repository.
:param repository: The repository class.
"""
if self.worker:
self.worker.add_repository(name, repository)

def remove_repository(self, name: str) -> None:
"""Deregister a repository.
:param name: The name of the repository.
"""
if self.worker:
self.worker.remove_repository(name)
7 changes: 4 additions & 3 deletions flama/resources/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ def build(self, app: t.Optional["Flama"] = None) -> None:
super().build(app)

if (root := (self.app if isinstance(self.app, Flama) else app)) and "ddd" in self.resource._meta.namespaces:
root.resources.worker._repositories[self.resource._meta.name] = self.resource._meta.namespaces["ddd"][
"repository"
]
root.resources.add_repository(
name=self.resource._meta.name,
repository=self.resource._meta.namespaces["ddd"]["repository"],
)


def resource_method(
Expand Down
Loading

0 comments on commit 89fcb87

Please sign in to comment.