Skip to content

Commit

Permalink
✨ Enhanced operations for CRUD (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
perdy authored and migduroli committed Sep 3, 2024
1 parent ea74449 commit 070fb4f
Show file tree
Hide file tree
Showing 36 changed files with 787 additions and 700 deletions.
32 changes: 8 additions & 24 deletions examples/resource.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import databases
import sqlalchemy
from pydantic import BaseModel
from sqlalchemy import create_engine

import flama
from flama import Flama
from flama.resources.crud import CRUDListResourceType
from flama.resources.crud import CRUDResource
from flama.sqlalchemy import SQLAlchemyModule

DATABASE_URL = "sqlite:///resource.db"

database = databases.Database(DATABASE_URL)
DATABASE_URL = "sqlite+aiosqlite://"

metadata = sqlalchemy.MetaData()

Expand All @@ -27,9 +24,7 @@ class PuppySchema(BaseModel):
)


class PuppyResource(metaclass=CRUDListResourceType):
database = database

class PuppyResource(CRUDResource):
name = "puppy"
verbose_name = "Puppy"

Expand All @@ -39,24 +34,13 @@ class PuppyResource(metaclass=CRUDListResourceType):

app = Flama(
title="Puppy Register", # API title
version="0.1", # API version
version="0.1.0", # API version
description="A register of puppies", # API description
modules=[SQLAlchemyModule(database=DATABASE_URL)],
)

app.add_resource("/", PuppyResource)


@app.on_event("startup")
async def startup():
engine = create_engine(DATABASE_URL)
metadata.create_all(engine) # Create the tables.
await database.connect()


@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.resources.add_resource("/", PuppyResource)


if __name__ == "__main__":
flama.run(app, host="0.0.0.0", port=8000)
flama.run(flama_app=app, server_host="0.0.0.0", server_port=8080)
1 change: 0 additions & 1 deletion flama/authentication/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def _token_from_cookies(self, cookies: Cookies) -> bytes:
try:
token = cookies[self.cookie_key]["value"]
except KeyError:
print(cookies)
logger.debug("'%s' not found in cookies", self.cookie_key)
raise exceptions.Unauthorized()

Expand Down
40 changes: 23 additions & 17 deletions flama/ddd/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __eq__(self, other):
and self.table == other.table
)

async def create(self, *data: t.Union[t.Dict[str, t.Any], types.Schema]) -> t.List[t.Tuple[t.Any, ...]]:
async def create(self, *data: t.Union[t.Dict[str, t.Any], types.Schema]) -> t.List[types.Schema]:
"""Creates new elements in the table.
If the element already exists, it raises an `IntegrityError`. If the element is created, it returns
Expand All @@ -57,10 +57,10 @@ async def create(self, *data: t.Union[t.Dict[str, t.Any], types.Schema]) -> t.Li
:raises IntegrityError: If the element already exists or cannot be inserted.
"""
try:
result = await self._connection.execute(sqlalchemy.insert(self.table), data)
result = await self._connection.execute(sqlalchemy.insert(self.table).values(data).returning(self.table))
except sqlalchemy.exc.IntegrityError as e:
raise exceptions.IntegrityError(str(e))
return [tuple(x) for x in result.inserted_primary_key_rows]
return [types.Schema(element._asdict()) for element in result]

async def retrieve(self, *clauses, **filters) -> types.Schema:
"""Retrieves an element from the table.
Expand All @@ -71,7 +71,7 @@ async def retrieve(self, *clauses, **filters) -> types.Schema:
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements
using exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param id: The primary key of the element.
Expand All @@ -90,7 +90,9 @@ async def retrieve(self, *clauses, **filters) -> types.Schema:

return types.Schema(element._asdict())

async def update(self, data: t.Union[t.Dict[str, t.Any], types.Schema], *clauses, **filters) -> int:
async def update(
self, data: t.Union[t.Dict[str, t.Any], types.Schema], *clauses, **filters
) -> t.List[types.Schema]:
"""Updates elements in the table.
Using clauses and filters, it filters the elements to update. If no clauses or filters are given, it updates
Expand All @@ -102,14 +104,16 @@ async def update(self, data: t.Union[t.Dict[str, t.Any], types.Schema], *clauses
:return: The number of elements updated.
:raises IntegrityError: If the element cannot be updated.
"""
query = self._filter_query(sqlalchemy.update(self.table), *clauses, **filters).values(**data)
query = (
self._filter_query(sqlalchemy.update(self.table), *clauses, **filters).values(**data).returning(self.table)
)

try:
result = await self._connection.execute(query)
except sqlalchemy.exc.IntegrityError:
raise exceptions.IntegrityError

return result.rowcount
return [types.Schema(element._asdict()) for element in result]

async def delete(self, *clauses, **filters) -> None:
"""Delete elements from the table.
Expand All @@ -119,7 +123,7 @@ async def delete(self, *clauses, **filters) -> None:
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements using
exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param clauses: Clauses to filter the elements.
Expand All @@ -144,7 +148,7 @@ async def list(
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements using
exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Order example: `order_by="id", order_direction="desc"`
Filter example: `id=1`
Expand Down Expand Up @@ -175,7 +179,7 @@ async def drop(self, *clauses, **filters) -> int:
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements using
exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param clauses: Clauses to filter the elements.
Expand All @@ -197,7 +201,7 @@ def _filter_query(self, query, *clauses, **filters):
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements using
exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param query: The query to filter.
Expand All @@ -223,7 +227,7 @@ def __init__(self, connection: "AsyncConnection"):
def __eq__(self, other):
return isinstance(other, SQLAlchemyTableRepository) and self._table == other._table and super().__eq__(other)

async def create(self, *data: t.Union[t.Dict[str, t.Any], types.Schema]) -> t.List[t.Tuple[t.Any, ...]]:
async def create(self, *data: t.Union[t.Dict[str, t.Any], types.Schema]) -> t.List[types.Schema]:
"""Creates new elements in the repository.
If the element already exists, it raises an `exceptions.IntegrityError`. If the element is created, it returns
Expand All @@ -244,7 +248,7 @@ async def retrieve(self, *clauses, **filters) -> types.Schema:
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements
using exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param clauses: Clauses to filter the elements.
Expand All @@ -255,7 +259,9 @@ async def retrieve(self, *clauses, **filters) -> types.Schema:
"""
return await self._table_manager.retrieve(*clauses, **filters)

async def update(self, data: t.Union[t.Dict[str, t.Any], types.Schema], *clauses, **filters) -> int:
async def update(
self, data: t.Union[t.Dict[str, t.Any], types.Schema], *clauses, **filters
) -> t.List[types.Schema]:
"""Updates an element in the repository.
If the element does not exist, it raises a `NotFoundError`. If the element is updated, it returns the updated
Expand All @@ -275,7 +281,7 @@ async def delete(self, *clauses, **filters) -> None:
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements
using exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param id: The primary key of the element.
Expand All @@ -297,7 +303,7 @@ def list(
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements using
exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Order example: `order_by="id", order_direction="desc"`
Filter example: `id=1`
Expand All @@ -318,7 +324,7 @@ async def drop(self, *clauses, **filters) -> int:
Clauses are used to filter the elements using sqlalchemy clauses. Filters are used to filter the elements using
exact values to specific columns. Clauses and filters can be combined.
Clause example: `table.c["id"]._in((1, 2, 3))`
Clause example: `table.c["id"].in_((1, 2, 3))`
Filter example: `id=1`
:param clauses: Clauses to filter the elements.
Expand Down
6 changes: 3 additions & 3 deletions flama/ddd/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class WorkerType(abc.ABCMeta):
"""

def __new__(mcs, name: str, bases: t.Tuple[type], namespace: t.Dict[str, t.Any]):
if mcs._is_abstract_worker(namespace) and "__annotations__" in namespace:
if not mcs._is_abstract(namespace) and "__annotations__" in namespace:
namespace["_repositories"] = types.Repositories(
{
k: v
Expand All @@ -41,8 +41,8 @@ def __new__(mcs, name: str, bases: t.Tuple[type], namespace: t.Dict[str, t.Any])
return super().__new__(mcs, name, bases, namespace)

@staticmethod
def _is_abstract_worker(namespace: t.Dict[str, t.Any]) -> bool:
return namespace.get("__module__") != "flama.ddd.workers" or namespace.get("__qualname__") != "AbstractWorker"
def _is_abstract(namespace: t.Dict[str, t.Any]) -> bool:
return namespace.get("__module__") == "flama.ddd.workers" and namespace.get("__qualname__") == "AbstractWorker"


class AbstractWorker(abc.ABC, metaclass=WorkerType):
Expand Down
47 changes: 27 additions & 20 deletions flama/models/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import flama.schemas
from flama import types
from flama.models.components import ModelComponentBuilder
from flama.resources import BaseResource, data_structures
from flama.resources import data_structures
from flama.resources.exceptions import ResourceAttributeError
from flama.resources.resource import ResourceType
from flama.resources.resource import Resource, ResourceType
from flama.resources.routing import resource_method

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -70,11 +70,6 @@ async def predict(
return {"_predict": predict}


class ModelResource(BaseResource):
component: "ModelComponent"
model_path: t.Union[str, os.PathLike]


class ModelResourceType(ResourceType, InspectMixin, PredictMixin):
METHODS = ("inspect", "predict")

Expand All @@ -87,22 +82,29 @@ def __new__(mcs, name: str, bases: t.Tuple[type], namespace: t.Dict[str, t.Any])
:param bases: List of superclasses.
:param namespace: Variables namespace used to create the class.
"""
try:
# Get model component
component = mcs._get_model_component(bases, namespace)
namespace["component"] = component
namespace["model"] = component.model
except AttributeError as e:
raise ResourceAttributeError(str(e), name)

namespace.setdefault("_meta", data_structures.Metadata()).namespaces["model"] = {
"component": component,
"model": component.model,
"model_type": component.get_model_type(),
}
if not mcs._is_abstract(namespace):
try:
# Get model component
component = mcs._get_model_component(bases, namespace)
namespace["component"] = component
namespace["model"] = component.model
except AttributeError as e:
raise ResourceAttributeError(str(e), name)

namespace.setdefault("_meta", data_structures.Metadata()).namespaces["model"] = {
"component": component,
"model": component.model,
"model_type": component.get_model_type(),
}

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

@staticmethod
def _is_abstract(namespace: t.Dict[str, t.Any]) -> bool:
return (
namespace.get("__module__") == "flama.models.resource" and namespace.get("__qualname__") == "ModelResource"
)

@classmethod
def _get_model_component(cls, bases: t.Sequence[t.Any], namespace: t.Dict[str, t.Any]) -> "ModelComponent":
try:
Expand All @@ -119,3 +121,8 @@ def _get_model_component(cls, bases: t.Sequence[t.Any], namespace: t.Dict[str, t
...

raise AttributeError(ResourceAttributeError.MODEL_NOT_FOUND)


class ModelResource(Resource, metaclass=ModelResourceType):
component: "ModelComponent"
model_path: t.Union[str, os.PathLike]
Loading

0 comments on commit 070fb4f

Please sign in to comment.