Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add python-gino support #71

Merged
merged 34 commits into from
Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
376f502
add python-gino support
turalpb May 31, 2021
3644965
fix comments
turalpb Jun 2, 2021
8570e58
add test for gino backend
turalpb Jun 25, 2021
b5ef8fa
update dev requiremenets
turalpb Jun 26, 2021
5149ff4
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
8f416c6
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
84d760c
Merge remote-tracking branch 'Turall/master' into Turall_master
awtkns Jul 3, 2021
164a416
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
fa2f7ec
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
0c4d70f
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
09b7d70
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
da68952
:sparkles: Gino Implementation Test Env
awtkns Jul 3, 2021
5c06f62
:sparkles: Gino Implementation Test Env
awtkns Jul 4, 2021
860dab0
:sparkles: Gino Implementation Test Env
awtkns Jul 4, 2021
f87532b
:sparkles: Gino Implementation Test Env
awtkns Jul 4, 2021
8234853
Merge remote-tracking branch 'Turall/master' into Turall_master
awtkns Jul 4, 2021
2e0ce9c
Merge remote-tracking branch 'Turall/master' into Turall_master
awtkns Jul 4, 2021
5e9dc3d
Merge remote-tracking branch 'Turall/master' into Turall_master
awtkns Jul 4, 2021
138e044
Merge remote-tracking branch 'Turall/master' into Turall_master
awtkns Jul 4, 2021
92773f3
DB PORT
awtkns Jul 4, 2021
2bb069c
DB PORT
awtkns Jul 4, 2021
708670f
DB PORT
awtkns Jul 4, 2021
73c1a3d
DB PORT
awtkns Jul 4, 2021
c8f01b4
DB PORT
awtkns Jul 4, 2021
484034d
DB PORT
awtkns Jul 4, 2021
d6939b4
DB Tests working
awtkns Jul 4, 2021
e13e00a
Fix typing
awtkns Jul 4, 2021
dca8ab7
:pushpin: pin mypy version
awtkns Jul 4, 2021
a9fcc34
:pushpin: pin mypy version
awtkns Jul 4, 2021
48e3806
:bug: fix typing
awtkns Jul 4, 2021
9950045
:bug: fix typing
awtkns Jul 4, 2021
a6709c8
:bug: fix typing
awtkns Jul 4, 2021
80c8a68
:sparkles: Gino Ready for Release
awtkns Jul 6, 2021
6a23912
:bookmark: Bump Version to v0.8 releasing Gino
awtkns Jul 6, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@ on:
branches: [ master ]

jobs:
build:

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST: postgres
ports:
- 5432/tcp
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v2
Expand All @@ -25,5 +38,11 @@ jobs:
python -m pip install --upgrade pip
pip install -r tests/dev.requirements.txt
- name: Test with pytest
env:
POSTGRES_DB: test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST: localhost
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
run: |
pytest
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ __pycache__/

# C extensions
*.so

.vscode
# Distribution / packaging
.Python
build/
Expand Down
21 changes: 21 additions & 0 deletions docs/en/docs/backends/gino.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Asynchronous routes will be automatically generated when using the `GinoCRUDRouter`. To use it, you must pass a
[pydantic](https://pydantic-docs.helpmanual.io/) model, your SQLAlchemy Table, and the databases database.
This CRUDRouter is intended to be used with the python [Gino](https://python-gino.org/) library. An example
of how to use [Gino](https://python-gino.org/) with FastAPI can be found both
[here](https://python-gino.org/docs/en/1.0/tutorials/fastapi.html) and below.

!!! warning
To use the `GinoCRUDRouter`, Databases **and** SQLAlchemy must be first installed.

## Minimal Example
Below is a minimal example assuming that you have already imported and created
all the required models and database connections.

```python
router = GinoCRUDRouter(
schema=MyPydanticModel,
db=db,
db_model=MyModel
)
app.include_router(router)
```
1 change: 1 addition & 0 deletions docs/en/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ nav:
- In Memory: backends/memory.md
- SQLAlchemy: backends/sqlalchemy.md
- Databases (async): backends/async.md
- Gino (async): backends/gino.md
- Ormar (async): backends/ormar.md
- Tortoise (async): backends/tortoise.md
- Routing: routing.md
Expand Down
2 changes: 2 additions & 0 deletions fastapi_crudrouter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .core import (
DatabasesCRUDRouter,
GinoCRUDRouter,
MemoryCRUDRouter,
OrmarCRUDRouter,
SQLAlchemyCRUDRouter,
Expand All @@ -12,4 +13,5 @@
"DatabasesCRUDRouter",
"TortoiseCRUDRouter",
"OrmarCRUDRouter",
"GinoCRUDRouter",
]
4 changes: 3 additions & 1 deletion fastapi_crudrouter/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from . import _utils
from ._base import CRUDGenerator, NOT_FOUND
from ._base import NOT_FOUND, CRUDGenerator
from .databases import DatabasesCRUDRouter
from .gino_starlette import GinoCRUDRouter
from .mem import MemoryCRUDRouter
from .ormar import OrmarCRUDRouter
from .sqlalchemy import SQLAlchemyCRUDRouter
Expand All @@ -15,4 +16,5 @@
"DatabasesCRUDRouter",
"TortoiseCRUDRouter",
"OrmarCRUDRouter",
"GinoCRUDRouter",
]
134 changes: 134 additions & 0 deletions fastapi_crudrouter/core/gino_starlette.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from typing import Any, Callable, List, Optional, Type, Union, Coroutine

from fastapi import HTTPException

from . import NOT_FOUND, CRUDGenerator, _utils
from ._types import DEPENDENCIES, PAGINATION
from ._types import PYDANTIC_SCHEMA as SCHEMA

try:
from asyncpg.exceptions import UniqueViolationError
from gino import Gino
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import DeclarativeMeta as Model
except ImportError:
Model: Any = None # type: ignore
gino_installed = False
else:
gino_installed = True

CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]]


class GinoCRUDRouter(CRUDGenerator[SCHEMA]):
def __init__(
self,
schema: Type[SCHEMA],
db_model: Model,
db: "Gino",
create_schema: Optional[Type[SCHEMA]] = None,
update_schema: Optional[Type[SCHEMA]] = None,
prefix: Optional[str] = None,
tags: Optional[List[str]] = None,
paginate: Optional[int] = None,
get_all_route: Union[bool, DEPENDENCIES] = True,
get_one_route: Union[bool, DEPENDENCIES] = True,
create_route: Union[bool, DEPENDENCIES] = True,
update_route: Union[bool, DEPENDENCIES] = True,
delete_one_route: Union[bool, DEPENDENCIES] = True,
delete_all_route: Union[bool, DEPENDENCIES] = True,
**kwargs: Any
) -> None:
assert gino_installed, "Gino must be installed to use the GinoCRUDRouter."

self.db_model = db_model
self.db = db
self._pk: str = db_model.__table__.primary_key.columns.keys()[0]
self._pk_type: type = _utils.get_pk_type(schema, self._pk)

super().__init__(
schema=schema,
create_schema=create_schema,
update_schema=update_schema,
prefix=prefix or db_model.__tablename__,
tags=tags,
paginate=paginate,
get_all_route=get_all_route,
get_one_route=get_one_route,
create_route=create_route,
update_route=update_route,
delete_one_route=delete_one_route,
delete_all_route=delete_all_route,
**kwargs
)

def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
async def route(
pagination: PAGINATION = self.pagination,
) -> List[Model]:
skip, limit = pagination.get("skip"), pagination.get("limit")

db_models: List[Model] = (
await self.db_model.query.limit(limit).offset(skip).gino.all()
)
return db_models

return route

def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
async def route(item_id: self._pk_type) -> Model: # type: ignore
model: Model = await self.db_model.get(item_id)

if model:
return model
else:
raise NOT_FOUND

return route

def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
async def route(
model: self.create_schema, # type: ignore
) -> Model:
try:
async with self.db.transaction():
db_model: Model = await self.db_model.create(**model.dict())
return db_model
except (IntegrityError, UniqueViolationError):
raise HTTPException(422, "Key already exists")

return route

def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
async def route(
item_id: self._pk_type, # type: ignore
model: self.update_schema, # type: ignore
) -> Model:
try:
db_model: Model = await self._get_one()(item_id)
async with self.db.transaction():
model = model.dict(exclude={self._pk})
await db_model.update(**model).apply()

return db_model
except (IntegrityError, UniqueViolationError) as e:
raise HTTPException(422, ", ".join(e.args))

return route

def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
async def route() -> List[Model]:
await self.db_model.delete.gino.status()
return await self._get_all()(pagination={"skip": 0, "limit": None})

return route

def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
async def route(item_id: self._pk_type) -> Model: # type: ignore
db_model: Model = await self._get_one()(item_id)
await db_model.delete()

return db_model

return route
2 changes: 1 addition & 1 deletion fastapi_crudrouter/core/ormar.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def route(
query = self.schema.objects.offset(cast(int, skip))
if limit:
query = query.limit(limit)
return await query.all()
return await query.all() # type: ignore

return route

Expand Down
6 changes: 5 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[flake8]
max-line-length = 88
select = C,E,F,W,B,B9
ignore = B008, E203, W503, CFQ001, CFQ002
ignore = B008, E203, W503, CFQ001, CFQ002, ECE001
import-order-style = pycharm

[mypy]
Expand Down Expand Up @@ -34,4 +34,8 @@ ignore_missing_imports = True
[mypy-uvicorn.*]
ignore_missing_imports = True

[mypy-gino.*]
ignore_missing_imports = True

[mypy-asyncpg.*]
ignore_missing_imports = True
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages

VERSION = "0.7.1"
VERSION = "0.8.0"

setup(
name="fastapi-crudrouter",
Expand Down
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from pydantic import BaseModel

from .conf import config

PAGINATION_SIZE = 10
CUSTOM_TAGS = ["Tag1", "Tag2"]

Expand Down
3 changes: 3 additions & 0 deletions tests/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .config import BaseConfig

config = BaseConfig()
36 changes: 36 additions & 0 deletions tests/conf/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os
import pathlib


ENV_FILE_PATH = pathlib.Path(__file__).parent / "dev.env"
assert ENV_FILE_PATH.exists()


class BaseConfig:
POSTGRES_HOST = ""
POSTGRES_USER = ""
POSTGRES_PASSWORD = ""
POSTGRES_DB = ""
POSTGRES_PORT = ""

def __init__(self):
self._apply_dot_env()
self._apply_env_vars()
self.POSTGRES_URI = f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
print(self.POSTGRES_URI)

def _apply_dot_env(self):
with open(ENV_FILE_PATH) as fp:
for line in fp.readlines():
line = line.strip(" \n")

if not line.startswith("#"):
k, v = line.split("=", 1)

if hasattr(self, k) and not getattr(self, k):
setattr(self, k, v)

def _apply_env_vars(self):
for k, v in os.environ.items():
if hasattr(self, k):
setattr(self, k, v)
10 changes: 10 additions & 0 deletions tests/conf/dev.docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: "3.9"

services:
db:
image: postgres
restart: always
env_file:
- dev.env
ports:
- 5432:5432
5 changes: 5 additions & 0 deletions tests/conf/dev.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
POSTGRES_HOST=localhost
POSTGRES_DB=test
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_PORT=5432
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import inspect
from fastapi.testclient import TestClient

from .implementations import *
Expand Down Expand Up @@ -32,6 +33,7 @@ def client(request):
sqlalchemy_implementation_custom_ids,
databases_implementation_custom_ids,
ormar_implementation_custom_ids,
gino_implementation_custom_ids,
]
)
def custom_id_client(request):
Expand All @@ -43,6 +45,7 @@ def custom_id_client(request):
sqlalchemy_implementation_string_pk,
databases_implementation_string_pk,
ormar_implementation_string_pk,
gino_implementation_string_pk,
],
scope="function",
)
Expand All @@ -54,6 +57,7 @@ def string_pk_client(request):
params=[
sqlalchemy_implementation_integrity_errors,
ormar_implementation_integrity_errors,
gino_implementation_integrity_errors,
],
scope="function",
)
Expand Down
4 changes: 3 additions & 1 deletion tests/dev.requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ databases
aiosqlite
sqlalchemy==1.3.22
sqlalchemy_utils==0.36.8
gino-starlette==0.1.1

# Testing
pytest
pytest-virtualenv
requests
asynctest
psycopg2

# Linting
flake8
Expand All @@ -29,4 +31,4 @@ flake8-functions
flake8-expression-complexity

# Typing
mypy
mypy==0.910
Loading