Skip to content

Commit

Permalink
Merge branch 'release/0.20.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
ri-gilfanov committed Jul 5, 2021
2 parents 4c989bf + 233f9c8 commit ff0a994
Show file tree
Hide file tree
Showing 32 changed files with 630 additions and 498 deletions.
14 changes: 11 additions & 3 deletions .flake8
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
[flake8]
count = True
exclude = .venv
max-complexity = 8
max-line-length = 88
exclude =
__pycache__,
dist,
.git,
.github,
.mypy_cache,
.pytest_cache,
.venv,
.vscode
max-complexity = 10
max-line-length = 80
show_source = True
statistics = True
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.9]
python-version: [3.7, 3.8, 3.9]

steps:
- uses: actions/checkout@v2
Expand Down
16 changes: 0 additions & 16 deletions .travis.yml

This file was deleted.

39 changes: 19 additions & 20 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
==================
aiohttp-sqlalchemy
==================
|Python versions| |PyPI release| |PyPI downloads| |License| |ReadTheDocs| |GitHub CI| |Codecov| |Codacy|
|ReadTheDocs| |PyPI release| |PyPI downloads| |License| |Python versions| |GitHub CI| |Codecov| |Codacy|

.. |Python versions| image:: https://img.shields.io/badge/Python-3.7%20%7C%203.8%20%7C%203.9-blue
:target: https://pypi.org/project/aiohttp-sqlalchemy/
:alt: Python version support
.. |ReadTheDocs| image:: https://readthedocs.org/projects/aiohttp-sqlalchemy/badge/?version=latest
:target: https://aiohttp-sqlalchemy.readthedocs.io/en/latest/?badge=latest
:alt: Read The Docs build

.. |PyPI release| image:: https://badge.fury.io/py/aiohttp-sqlalchemy.svg
:target: https://pypi.org/project/aiohttp-sqlalchemy/
Expand All @@ -19,9 +19,9 @@ aiohttp-sqlalchemy
:target: https://github.com/ri-gilfanov/aiohttp-sqlalchemy/blob/master/LICENSE
:alt: MIT License

.. |ReadTheDocs| image:: https://readthedocs.org/projects/aiohttp-sqlalchemy/badge/?version=latest
:target: https://aiohttp-sqlalchemy.readthedocs.io/en/latest/?badge=latest
:alt: Read The Docs build
.. |Python versions| image:: https://img.shields.io/badge/Python-3.7%20%7C%203.8%20%7C%203.9-blue
:target: https://pypi.org/project/aiohttp-sqlalchemy/
:alt: Python version support

.. |GitHub CI| image:: https://github.com/ri-gilfanov/aiohttp-sqlalchemy/actions/workflows/ci.yml/badge.svg?branch=master
:target: https://github.com/ri-gilfanov/aiohttp-sqlalchemy/actions/workflows/ci.yml
Expand Down Expand Up @@ -76,26 +76,25 @@ Copy and paste this code in a file and run:
from aiohttp import web
from sqlalchemy import orm
import aiohttp_sqlalchemy
from aiohttp_sqlalchemy import sa_session
import aiohttp_sqlalchemy as ahsa
metadata = sa.MetaData()
Base = orm.declarative_base(metadata=metadata)
class MyModel(Base):
__tablename__ = "my_table"
__tablename__ = 'my_table'
pk = sa.Column(sa.Integer, primary_key=True)
timestamp = sa.Column(sa.DateTime(), default=datetime.now)
async def main(request):
db_session = sa_session(request)
sa_session = ahsa.get_session(request)
async with db_session.begin():
db_session.add(MyModel())
result = await db_session.execute(sa.select(MyModel))
async with sa_session.begin():
sa_session.add(MyModel())
result = await sa_session.execute(sa.select(MyModel))
result = result.scalars()
data = {
Expand All @@ -108,14 +107,14 @@ Copy and paste this code in a file and run:
async def app_factory():
app = web.Application()
bind = aiohttp_sqlalchemy.bind("sqlite+aiosqlite:///")
aiohttp_sqlalchemy.setup(app, [bind])
await aiohttp_sqlalchemy.init_db(app, metadata)
app.add_routes([web.get("/", main)])
ahsa.setup(app, [
ahsa.bind('sqlite+aiosqlite:///'),
])
await ahsa.init_db(app, metadata)
app.add_routes([web.get('/', main)])
return app
if __name__ == "__main__":
if __name__ == '__main__':
web.run_app(app_factory())
105 changes: 65 additions & 40 deletions aiohttp_sqlalchemy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,115 @@

from aiohttp.web import Application
from sqlalchemy.engine import Engine
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
create_async_engine,
)
from sqlalchemy.orm import Session, sessionmaker

from aiohttp_sqlalchemy.constants import DEFAULT_KEY, SA_DEFAULT_KEY
from aiohttp_sqlalchemy.decorators import sa_decorator
from aiohttp_sqlalchemy.exceptions import DuplicateAppKeyError, DuplicateRequestKeyError
from aiohttp_sqlalchemy.exceptions import (
DuplicateAppKeyError,
DuplicateRequestKeyError,
)
from aiohttp_sqlalchemy.middlewares import sa_middleware
from aiohttp_sqlalchemy.typedefs import TBinding, TBindings, TBindTo, TSessionFactory
from aiohttp_sqlalchemy.utils import init_db, sa_init_db, sa_session, sa_session_factory
from aiohttp_sqlalchemy.typedefs import TBind, TBinds, TSessionFactory, TTarget
from aiohttp_sqlalchemy.utils import (
get_session,
get_session_factory,
init_db,
sa_init_db,
sa_session,
sa_session_factory,
)
from aiohttp_sqlalchemy.views import (
SAAbstractView,
SABaseView,
SAItemMixin,
SAItemView,
SAMixin,
SAModelMixin,
SAModelView,
SAView,
)

__version__ = "0.17.3"
__version__ = '0.20.0'

__all__ = [
"SA_DEFAULT_KEY",
"DuplicateAppKeyError",
"DuplicateRequestKeyError",
"SABaseView",
"SAMixin",
"SAModelMixin",
"SAView",
"bind",
"init_db",
"sa_decorator",
"sa_middleware",
"sa_session",
"sa_session_factory",
"setup",
'SA_DEFAULT_KEY',
'DuplicateAppKeyError',
'DuplicateRequestKeyError',
'SABaseView',
'SAItemMixin',
'SAItemView',
'SAMixin',
'SAModelMixin',
'SAModelView',
'bind',
'get_session',
'get_session_factory',
'init_db',
'sa_decorator',
'sa_middleware',
'setup',
# Synonyms
"DEFAULT_KEY",
"SAAbstractView",
"sa_bind",
"sa_init_db",
'DEFAULT_KEY',
'SAAbstractView',
'SAView',
'sa_bind',
'sa_init_db',
'sa_session',
'sa_session_factory',
]


def bind(
bind_to: TBindTo, key: str = SA_DEFAULT_KEY, *, middleware: bool = True
) -> "TBinding":
target: TTarget,
key: str = SA_DEFAULT_KEY,
*,
middleware: bool = True,
) -> 'TBind':
"""Function wrapper for binding.
:param bind_to: target for SQLAlchemy binding. Argument can be database connection
url, asynchronous engine or asynchronous session factory.
:param target: argument can be database connection url, asynchronous engine
or asynchronous session factory.
:param key: key of SQLAlchemy binding.
:param middleware: `bool` for enable middleware. True by default.
"""
if isinstance(bind_to, str):
bind_to = cast(AsyncEngine, create_async_engine(bind_to))
if isinstance(target, str):
target = cast(AsyncEngine, create_async_engine(target))

if isinstance(bind_to, AsyncEngine):
bind_to = cast(
if isinstance(target, AsyncEngine):
target = cast(
TSessionFactory,
sessionmaker(
bind=bind_to,
bind=target,
class_=AsyncSession,
expire_on_commit=False,
),
)

for type_ in (AsyncSession, Engine, Session):
if isinstance(bind_to, type_):
msg = f"{type_} is unsupported type of argument `bind_to`."
if isinstance(target, type_):
msg = f'{type_} is unsupported type of argument `target`.'
raise TypeError(msg)

if not callable(bind_to):
msg = f"{bind_to} is unsupported type of argument `bind_to`."
if not callable(target):
msg = f'{target} is unsupported type of argument `target`.'
raise TypeError(msg)

return bind_to, key, middleware
return target, key, middleware


def setup(app: Application, bindings: "TBindings") -> None:
def setup(app: Application, binds: "TBinds") -> None:
"""Setup function for SQLAlchemy binding to AIOHTTP application.
:param app: your AIOHTTP application.
:param bindings: iterable of `aiohttp_sqlalchemy.bind()` calls.
:param binds: iterable of `aiohttp_sqlalchemy.bind()` calls.
"""
for factory, key, middleware in bindings:
for factory, key, middleware in binds:
if key in app:
raise DuplicateAppKeyError(key)

Expand Down
2 changes: 1 addition & 1 deletion aiohttp_sqlalchemy/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
SA_DEFAULT_KEY = "sa_main"
SA_DEFAULT_KEY = 'sa_main'

DEFAULT_KEY = SA_DEFAULT_KEY # synonym
17 changes: 14 additions & 3 deletions aiohttp_sqlalchemy/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ def sa_decorator(key: str = SA_DEFAULT_KEY) -> THandlerWrapper:
def wrapper(handler: THandler) -> THandler:
@wraps(handler)
async def wrapped(*args: Any, **kwargs: Any) -> StreamResponse:
request = args[0].request if isinstance(args[0], AbstractView) else args[-1]
request = args[0].request \
if isinstance(args[0], AbstractView) \
else args[-1]

if key in request:
raise DuplicateRequestKeyError(key)

# TODO: after dropped Python 3.7
# if session_factory := request.config_dict.get(key):
session_factory = request.config_dict.get(key)
async with session_factory() as request[key]:
return await handler(*args, **kwargs)
if session_factory:
async with session_factory() as request[key]:
return await handler(*args, **kwargs)
else:
raise KeyError(
f'Session factory not found by {key}.'
'Check `key` argument of `sa_decorator()`'
'or arguments of `aiohttp_sqlalchemy.setup()`.'
)

return wrapped

Expand Down
8 changes: 4 additions & 4 deletions aiohttp_sqlalchemy/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ class AbstractDuplicateKeyError(ValueError):
class DuplicateAppKeyError(AbstractDuplicateKeyError):
def __init__(self, key: str):
msg = (
f"Duplicated app key `{key}`. Check `bindings` argument "
f"in `aiohttp_sqlalchemy.setup()` call."
f'Duplicated app key `{key}`. Check `bindings` argument '
f'in `aiohttp_sqlalchemy.setup()` call.'
)
super().__init__(msg)


class DuplicateRequestKeyError(AbstractDuplicateKeyError):
def __init__(self, key: str):
msg = (
f"Duplicated request key `{key}`. Check middlewares and "
f"decorators from `aiohttp_sqlalchemy`."
f'Duplicated request key `{key}`. Check middlewares and '
f'decorators from `aiohttp_sqlalchemy`.'
)
super().__init__(msg)
17 changes: 14 additions & 3 deletions aiohttp_sqlalchemy/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@ def sa_middleware(key: str = SA_DEFAULT_KEY) -> THandler:
"""

@middleware
async def sa_middleware_(request: Request, handler: THandler) -> StreamResponse:
async def sa_middleware_(
request: Request,
handler: THandler,
) -> StreamResponse:
if key in request:
raise DuplicateRequestKeyError(key)

# TODO: after dropped Python 3.7
# if session_factory := request.config_dict.get(key):
session_factory = request.config_dict.get(key)
async with session_factory() as request[key]:
return await handler(request)
if session_factory:
async with session_factory() as request[key]:
return await handler(request)
else:
raise KeyError(
f'Session factory not found by {key}.'
'Check `aiohttp_sqlalchemy.setup()`.'
)

return sa_middleware_
11 changes: 6 additions & 5 deletions aiohttp_sqlalchemy/typedefs.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Awaitable, Callable, Iterable, Tuple, Union
from typing import Awaitable, Callable, Iterable, Optional, Tuple, Union

from aiohttp.web import StreamResponse
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession

TSessionFactory = Callable[..., AsyncSession]
THandler = Callable[..., Awaitable[StreamResponse]]
THandlerWrapper = Callable[..., THandler]
TSessionFactory = Callable[..., AsyncSession]
TOptSessionFactory = Optional[TSessionFactory]

TBindTo = Union[str, AsyncEngine, TSessionFactory]
TBinding = Tuple[TSessionFactory, str, bool]
TBindings = Iterable[TBinding]
TTarget = Union[str, AsyncEngine, TSessionFactory]
TBind = Tuple[TSessionFactory, str, bool]
TBinds = Iterable[TBind]

0 comments on commit ff0a994

Please sign in to comment.