Skip to content

Commit

Permalink
Framework extensions (#14)
Browse files Browse the repository at this point in the history
* Allow creation outside loop

* Add Quart

* Make pretty

* pyproject additions

* Add checks for when framework not installed

* Simplify exception

* Make pretty
  • Loading branch information
ahopkins authored Jul 7, 2022
1 parent f663d98 commit 57f4b6a
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 18 deletions.
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,49 @@ asyncio.run(run())

The docs: [ahopkins.github.io/mayim](https://ahopkins.github.io/mayim/guide/)

## Coming soon: Sanic support
## Framework support

In v22.6, [Sanic Extensions](https://sanic.dev/en/plugins/sanic-ext/getting-started.html) will introduce a simplified API for adding custom extensions to your [Sanic app](https://sanic.dev). It will look something like this:

### Quart

Mayim can attach to Quart using the customary `init_app` pattern and will handle setting up Mayim and the lifecycle events.

```python
from quart import Quart
from dataclasses import asdict
from typing import List
from mayim import PostgresExecutor
from model import City
from mayim.extension import QuartMayimExtension

app = Quart(__name__)


class CityExecutor(PostgresExecutor):
async def select_all_cities(
self, limit: int = 4, offset: int = 0
) -> List[City]:
...


ext = QuartMayimExtension(
executors=[CityExecutor],
dsn="postgres://postgres:postgres@localhost:5432/world",
)
ext.init_app(app)


@app.route("/")
async def handler():
executor = CityExecutor()
cities = await executor.select_all_cities()
return {"cities": [asdict(city) for city in cities]}
```


### Sanic

Mayim uses [Sanic Extensions](https://sanic.dev/en/plugins/sanic-ext/getting-started.html) v22.6+ to extend your [Sanic app](https://sanic.dev). It starts Mayim and provides dependency injections into your routes of all of the executors

```python
from typing import List
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ include_trailing_comma = true

[[tool.mypy.overrides]]
module = [
"psycopg.*",
"asyncmy.*",
"psycopg_pool.*",
"psycopg.*",
"quart.*",
"sanic_ext.*",
"asyncmy.*"
"sanic.*"
]
ignore_missing_imports = true

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = mayim
version = 0.2
version = 0.3.0
description = The NOT ORM hydraroe
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
5 changes: 5 additions & 0 deletions src/mayim/executor/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from mayim.convert import convert_sql_params
from mayim.exception import MayimError, RecordNotFound
from mayim.interface.lazy import LazyPool
from mayim.query.sql import ParamType, SQLQuery
from mayim.registry import LazyHydratorRegistry, LazySQLRegistry

Expand Down Expand Up @@ -202,6 +203,10 @@ def _setup(func):
def decorator(f):
@wraps(f)
async def decorated_function(self: SQLExecutor, *args, **kwargs):
if isinstance(self.pool, LazyPool):
raise MayimError(
"Connection pool to your database has not been setup. "
)
self._context.set((model, name))
if auto_exec:
query = self._queries[name]
Expand Down
6 changes: 5 additions & 1 deletion src/mayim/extension/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from .quart_extension import QuartMayimExtension
from .sanic_extension import SanicMayimExtension

__all__ = ("SanicMayimExtension",)
__all__ = (
"QuartMayimExtension",
"SanicMayimExtension",
)
57 changes: 57 additions & 0 deletions src/mayim/extension/quart_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from __future__ import annotations

from typing import Optional, Sequence, Type, Union

from mayim import Executor, Hydrator, Mayim
from mayim.exception import MayimError
from mayim.interface.base import BaseInterface
from mayim.registry import InterfaceRegistry, Registry

try:
from quart import Quart

QUART_INSTALLED = True
except ModuleNotFoundError:
QUART_INSTALLED = False
Quart = type("Quart", (), {}) # type: ignore


class QuartMayimExtension:
name = "mayim"

def __init__(
self,
*,
executors: Optional[Sequence[Union[Type[Executor], Executor]]] = None,
dsn: str = "",
hydrator: Optional[Hydrator] = None,
pool: Optional[BaseInterface] = None,
app: Optional[Quart] = None,
):
if not QUART_INSTALLED:
raise MayimError(
"Could not locate Quart. It must be installed to use "
"QuartMayimExtension. Try: pip install quart"
)
self.executors = executors or []
for executor in self.executors:
Registry().register(executor)
self.mayim_kwargs = {
"dsn": dsn,
"hydrator": hydrator,
"pool": pool,
}
if app is not None:
self.init_app(app)

def init_app(self, app: Quart) -> None:
@app.while_serving
async def lifespan():
Mayim(executors=self.executors, **self.mayim_kwargs)
for interface in InterfaceRegistry():
await interface.open()

yield

for interface in InterfaceRegistry():
await interface.close()
39 changes: 30 additions & 9 deletions src/mayim/extension/sanic_extension.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
from typing import Optional, Sequence, Type, Union

from sanic_ext import Extend
from sanic_ext.extensions.base import Extension

from mayim import Executor, Hydrator, Mayim
from mayim.exception import MayimError
from mayim.interface.base import BaseInterface
from mayim.registry import Registry
from mayim.registry import InterfaceRegistry, Registry

try:
from sanic.log import logger
from sanic_ext import Extend
from sanic_ext.extensions.base import Extension

SANIC_INSTALLED = True
except ModuleNotFoundError:
SANIC_INSTALLED = False
logger = object() # type: ignore
Extension = type("Extension", (), {}) # type: ignore
Extend = type("Extend", (), {}) # type: ignore


class SanicMayimExtension(Extension):
Expand All @@ -19,10 +29,12 @@ def __init__(
hydrator: Optional[Hydrator] = None,
pool: Optional[BaseInterface] = None,
):
# raise NotImplementedError(
# "This is a placeholder feature and will not be released until "
# "sometime after June 2022."
# )
if not SANIC_INSTALLED:
raise MayimError(
"Could not locate either Sanic or Sanic Extensions. "
"Both libraries must be installed to use SanicMayimExtension. "
"Try: pip install sanic[ext]"
)
self.executors = executors or []
for executor in self.executors:
Registry().register(executor)
Expand All @@ -34,8 +46,17 @@ def __init__(

def startup(self, bootstrap: Extend) -> None:
@self.app.before_server_start
async def setup(app):
async def setup(_):
Mayim(executors=self.executors, **self.mayim_kwargs)
for interface in InterfaceRegistry():
logger.info(f"Opening {interface}")
await interface.open()

@self.app.after_server_stop
async def shutdown(_):
for interface in InterfaceRegistry():
logger.info(f"Closing {interface}")
await interface.close()

for executor in Registry().values():
if isinstance(executor, Executor):
Expand Down
2 changes: 2 additions & 0 deletions src/mayim/interface/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from urllib.parse import urlparse

from mayim.exception import MayimError
from mayim.registry import InterfaceRegistry

UrlMapping = namedtuple("UrlMapping", ("key", "cast"))

Expand Down Expand Up @@ -89,6 +90,7 @@ def __init__(
self._populate_connection_args()
self._populate_dsn()
self._setup_pool()
InterfaceRegistry.add(self)

def __str__(self) -> str:
return f"<{self.__class__.__name__} {self.dsn}>"
Expand Down
13 changes: 12 additions & 1 deletion src/mayim/interface/lazy.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from typing import AsyncContextManager, Optional
from typing import AsyncContextManager, Optional, Type

from psycopg import AsyncConnection

from mayim.exception import MayimError
from mayim.interface.base import BaseInterface


class LazyPool(BaseInterface):
_singleton = None
_derivative: Optional[Type[BaseInterface]]

def __new__(cls, *args, **kwargs):
if cls._singleton is None:
cls._singleton = super().__new__(cls)
cls._singleton._derivative = None
return cls._singleton

def _setup_pool(self):
Expand All @@ -32,3 +35,11 @@ def connection(
self, timeout: Optional[float] = None
) -> AsyncContextManager[AsyncConnection]:
...

def set_derivative(self, interface_class: Type[BaseInterface]) -> None:
self._derivative = interface_class

def derive(self) -> BaseInterface:
if not self._derivative:
raise MayimError("No interface available to derive")
return self._derivative(dsn=self.full_dsn)
19 changes: 18 additions & 1 deletion src/mayim/mayim.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from asyncio import get_running_loop
from inspect import isclass
from typing import Optional, Sequence, Type, TypeVar, Union

Expand Down Expand Up @@ -25,7 +26,13 @@ def __init__(
raise MayimError("Conflict with pool and DSN")

if not pool and dsn:
pool = PostgresPool(dsn)
try:
get_running_loop()
except RuntimeError:
pool = LazyPool(dsn=dsn)
pool.set_derivative(PostgresPool)
else:
pool = PostgresPool(dsn=dsn)

if not executors:
executors = []
Expand Down Expand Up @@ -88,3 +95,13 @@ def load(
continue

executor._load()

async def connect(self) -> None:
registry = Registry()
to_derive = {
executor
for executor in registry.values()
if isinstance(executor.pool, LazyPool)
}
for executor in to_derive:
executor._pool = executor.pool.derive()
22 changes: 21 additions & 1 deletion src/mayim/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

from collections import defaultdict
from inspect import isclass
from typing import TYPE_CHECKING, DefaultDict, Dict, Optional, Type, Union
from typing import TYPE_CHECKING, DefaultDict, Dict, Optional, Set, Type, Union

if TYPE_CHECKING:
from mayim.executor import Executor
from mayim.hydrator import Hydrator
from mayim.interface.base import BaseInterface


class Registry(dict):
Expand All @@ -27,6 +28,25 @@ def reset(cls):
cls._singleton = super().__new__(cls) # type: ignore


class InterfaceRegistry:
_singleton = None
_interfaces: Set[BaseInterface]

def __new__(cls, *args, **kwargs):
if cls._singleton is None:
cls._singleton = super().__new__(cls)
cls._singleton._interfaces = set()
return cls._singleton

@classmethod
def add(cls, interface: BaseInterface) -> None:
instance = cls()
instance._interfaces.add(interface)

def __iter__(self):
return iter(self._interfaces)


class LazySQLRegistry:
_singleton = None
_queries: DefaultDict[str, Dict[str, str]]
Expand Down

0 comments on commit 57f4b6a

Please sign in to comment.