Skip to content

Commit 1711d00

Browse files
authored
Merge pull request #71 from Turall/master
add python-gino support
2 parents 2f1c593 + 6a23912 commit 1711d00

File tree

21 files changed

+438
-19
lines changed

21 files changed

+438
-19
lines changed

.github/workflows/pytest.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,25 @@ on:
77
branches: [ master ]
88

99
jobs:
10-
build:
11-
10+
test:
1211
runs-on: ubuntu-latest
1312
strategy:
1413
matrix:
1514
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
15+
services:
16+
postgres:
17+
image: postgres
18+
env:
19+
POSTGRES_USER: postgres
20+
POSTGRES_PASSWORD: postgres
21+
POSTGRES_HOST: postgres
22+
ports:
23+
- 5432/tcp
24+
options: >-
25+
--health-cmd pg_isready
26+
--health-interval 10s
27+
--health-timeout 5s
28+
--health-retries 5
1629
1730
steps:
1831
- uses: actions/checkout@v2
@@ -25,5 +38,11 @@ jobs:
2538
python -m pip install --upgrade pip
2639
pip install -r tests/dev.requirements.txt
2740
- name: Test with pytest
41+
env:
42+
POSTGRES_DB: test
43+
POSTGRES_USER: postgres
44+
POSTGRES_PASSWORD: postgres
45+
POSTGRES_HOST: localhost
46+
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
2847
run: |
2948
pytest

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ __pycache__/
55

66
# C extensions
77
*.so
8-
8+
.vscode
99
# Distribution / packaging
1010
.Python
1111
build/

docs/en/docs/backends/gino.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Asynchronous routes will be automatically generated when using the `GinoCRUDRouter`. To use it, you must pass a
2+
[pydantic](https://pydantic-docs.helpmanual.io/) model, your SQLAlchemy Table, and the databases database.
3+
This CRUDRouter is intended to be used with the python [Gino](https://python-gino.org/) library. An example
4+
of how to use [Gino](https://python-gino.org/) with FastAPI can be found both
5+
[here](https://python-gino.org/docs/en/1.0/tutorials/fastapi.html) and below.
6+
7+
!!! warning
8+
To use the `GinoCRUDRouter`, Databases **and** SQLAlchemy must be first installed.
9+
10+
## Minimal Example
11+
Below is a minimal example assuming that you have already imported and created
12+
all the required models and database connections.
13+
14+
```python
15+
router = GinoCRUDRouter(
16+
schema=MyPydanticModel,
17+
db=db,
18+
db_model=MyModel
19+
)
20+
app.include_router(router)
21+
```

docs/en/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ nav:
3232
- In Memory: backends/memory.md
3333
- SQLAlchemy: backends/sqlalchemy.md
3434
- Databases (async): backends/async.md
35+
- Gino (async): backends/gino.md
3536
- Ormar (async): backends/ormar.md
3637
- Tortoise (async): backends/tortoise.md
3738
- Routing: routing.md

fastapi_crudrouter/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .core import (
22
DatabasesCRUDRouter,
3+
GinoCRUDRouter,
34
MemoryCRUDRouter,
45
OrmarCRUDRouter,
56
SQLAlchemyCRUDRouter,
@@ -12,4 +13,5 @@
1213
"DatabasesCRUDRouter",
1314
"TortoiseCRUDRouter",
1415
"OrmarCRUDRouter",
16+
"GinoCRUDRouter",
1517
]

fastapi_crudrouter/core/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from . import _utils
2-
from ._base import CRUDGenerator, NOT_FOUND
2+
from ._base import NOT_FOUND, CRUDGenerator
33
from .databases import DatabasesCRUDRouter
4+
from .gino_starlette import GinoCRUDRouter
45
from .mem import MemoryCRUDRouter
56
from .ormar import OrmarCRUDRouter
67
from .sqlalchemy import SQLAlchemyCRUDRouter
@@ -15,4 +16,5 @@
1516
"DatabasesCRUDRouter",
1617
"TortoiseCRUDRouter",
1718
"OrmarCRUDRouter",
19+
"GinoCRUDRouter",
1820
]
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from typing import Any, Callable, List, Optional, Type, Union, Coroutine
2+
3+
from fastapi import HTTPException
4+
5+
from . import NOT_FOUND, CRUDGenerator, _utils
6+
from ._types import DEPENDENCIES, PAGINATION
7+
from ._types import PYDANTIC_SCHEMA as SCHEMA
8+
9+
try:
10+
from asyncpg.exceptions import UniqueViolationError
11+
from gino import Gino
12+
from sqlalchemy.exc import IntegrityError
13+
from sqlalchemy.ext.declarative import DeclarativeMeta as Model
14+
except ImportError:
15+
Model: Any = None # type: ignore
16+
gino_installed = False
17+
else:
18+
gino_installed = True
19+
20+
CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
21+
CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]]
22+
23+
24+
class GinoCRUDRouter(CRUDGenerator[SCHEMA]):
25+
def __init__(
26+
self,
27+
schema: Type[SCHEMA],
28+
db_model: Model,
29+
db: "Gino",
30+
create_schema: Optional[Type[SCHEMA]] = None,
31+
update_schema: Optional[Type[SCHEMA]] = None,
32+
prefix: Optional[str] = None,
33+
tags: Optional[List[str]] = None,
34+
paginate: Optional[int] = None,
35+
get_all_route: Union[bool, DEPENDENCIES] = True,
36+
get_one_route: Union[bool, DEPENDENCIES] = True,
37+
create_route: Union[bool, DEPENDENCIES] = True,
38+
update_route: Union[bool, DEPENDENCIES] = True,
39+
delete_one_route: Union[bool, DEPENDENCIES] = True,
40+
delete_all_route: Union[bool, DEPENDENCIES] = True,
41+
**kwargs: Any
42+
) -> None:
43+
assert gino_installed, "Gino must be installed to use the GinoCRUDRouter."
44+
45+
self.db_model = db_model
46+
self.db = db
47+
self._pk: str = db_model.__table__.primary_key.columns.keys()[0]
48+
self._pk_type: type = _utils.get_pk_type(schema, self._pk)
49+
50+
super().__init__(
51+
schema=schema,
52+
create_schema=create_schema,
53+
update_schema=update_schema,
54+
prefix=prefix or db_model.__tablename__,
55+
tags=tags,
56+
paginate=paginate,
57+
get_all_route=get_all_route,
58+
get_one_route=get_one_route,
59+
create_route=create_route,
60+
update_route=update_route,
61+
delete_one_route=delete_one_route,
62+
delete_all_route=delete_all_route,
63+
**kwargs
64+
)
65+
66+
def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
67+
async def route(
68+
pagination: PAGINATION = self.pagination,
69+
) -> List[Model]:
70+
skip, limit = pagination.get("skip"), pagination.get("limit")
71+
72+
db_models: List[Model] = (
73+
await self.db_model.query.limit(limit).offset(skip).gino.all()
74+
)
75+
return db_models
76+
77+
return route
78+
79+
def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
80+
async def route(item_id: self._pk_type) -> Model: # type: ignore
81+
model: Model = await self.db_model.get(item_id)
82+
83+
if model:
84+
return model
85+
else:
86+
raise NOT_FOUND
87+
88+
return route
89+
90+
def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
91+
async def route(
92+
model: self.create_schema, # type: ignore
93+
) -> Model:
94+
try:
95+
async with self.db.transaction():
96+
db_model: Model = await self.db_model.create(**model.dict())
97+
return db_model
98+
except (IntegrityError, UniqueViolationError):
99+
raise HTTPException(422, "Key already exists")
100+
101+
return route
102+
103+
def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
104+
async def route(
105+
item_id: self._pk_type, # type: ignore
106+
model: self.update_schema, # type: ignore
107+
) -> Model:
108+
try:
109+
db_model: Model = await self._get_one()(item_id)
110+
async with self.db.transaction():
111+
model = model.dict(exclude={self._pk})
112+
await db_model.update(**model).apply()
113+
114+
return db_model
115+
except (IntegrityError, UniqueViolationError) as e:
116+
raise HTTPException(422, ", ".join(e.args))
117+
118+
return route
119+
120+
def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
121+
async def route() -> List[Model]:
122+
await self.db_model.delete.gino.status()
123+
return await self._get_all()(pagination={"skip": 0, "limit": None})
124+
125+
return route
126+
127+
def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
128+
async def route(item_id: self._pk_type) -> Model: # type: ignore
129+
db_model: Model = await self._get_one()(item_id)
130+
await db_model.delete()
131+
132+
return db_model
133+
134+
return route

fastapi_crudrouter/core/ormar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async def route(
7474
query = self.schema.objects.offset(cast(int, skip))
7575
if limit:
7676
query = query.limit(limit)
77-
return await query.all()
77+
return await query.all() # type: ignore
7878

7979
return route
8080

setup.cfg

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[flake8]
22
max-line-length = 88
33
select = C,E,F,W,B,B9
4-
ignore = B008, E203, W503, CFQ001, CFQ002
4+
ignore = B008, E203, W503, CFQ001, CFQ002, ECE001
55
import-order-style = pycharm
66

77
[mypy]
@@ -34,4 +34,8 @@ ignore_missing_imports = True
3434
[mypy-uvicorn.*]
3535
ignore_missing_imports = True
3636

37+
[mypy-gino.*]
38+
ignore_missing_imports = True
3739

40+
[mypy-asyncpg.*]
41+
ignore_missing_imports = True

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from setuptools import setup, find_packages
22

3-
VERSION = "0.7.1"
3+
VERSION = "0.8.0"
44

55
setup(
66
name="fastapi-crudrouter",

tests/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pydantic import BaseModel
22

3+
from .conf import config
4+
35
PAGINATION_SIZE = 10
46
CUSTOM_TAGS = ["Tag1", "Tag2"]
57

tests/conf/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .config import BaseConfig
2+
3+
config = BaseConfig()

tests/conf/config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
import pathlib
3+
4+
5+
ENV_FILE_PATH = pathlib.Path(__file__).parent / "dev.env"
6+
assert ENV_FILE_PATH.exists()
7+
8+
9+
class BaseConfig:
10+
POSTGRES_HOST = ""
11+
POSTGRES_USER = ""
12+
POSTGRES_PASSWORD = ""
13+
POSTGRES_DB = ""
14+
POSTGRES_PORT = ""
15+
16+
def __init__(self):
17+
self._apply_dot_env()
18+
self._apply_env_vars()
19+
self.POSTGRES_URI = f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
20+
print(self.POSTGRES_URI)
21+
22+
def _apply_dot_env(self):
23+
with open(ENV_FILE_PATH) as fp:
24+
for line in fp.readlines():
25+
line = line.strip(" \n")
26+
27+
if not line.startswith("#"):
28+
k, v = line.split("=", 1)
29+
30+
if hasattr(self, k) and not getattr(self, k):
31+
setattr(self, k, v)
32+
33+
def _apply_env_vars(self):
34+
for k, v in os.environ.items():
35+
if hasattr(self, k):
36+
setattr(self, k, v)

tests/conf/dev.docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: "3.9"
2+
3+
services:
4+
db:
5+
image: postgres
6+
restart: always
7+
env_file:
8+
- dev.env
9+
ports:
10+
- 5432:5432

tests/conf/dev.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
POSTGRES_HOST=localhost
2+
POSTGRES_DB=test
3+
POSTGRES_USER=postgres
4+
POSTGRES_PASSWORD=password
5+
POSTGRES_PORT=5432

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import inspect
23
from fastapi.testclient import TestClient
34

45
from .implementations import *
@@ -32,6 +33,7 @@ def client(request):
3233
sqlalchemy_implementation_custom_ids,
3334
databases_implementation_custom_ids,
3435
ormar_implementation_custom_ids,
36+
gino_implementation_custom_ids,
3537
]
3638
)
3739
def custom_id_client(request):
@@ -43,6 +45,7 @@ def custom_id_client(request):
4345
sqlalchemy_implementation_string_pk,
4446
databases_implementation_string_pk,
4547
ormar_implementation_string_pk,
48+
gino_implementation_string_pk,
4649
],
4750
scope="function",
4851
)
@@ -54,6 +57,7 @@ def string_pk_client(request):
5457
params=[
5558
sqlalchemy_implementation_integrity_errors,
5659
ormar_implementation_integrity_errors,
60+
gino_implementation_integrity_errors,
5761
],
5862
scope="function",
5963
)

tests/dev.requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ databases
99
aiosqlite
1010
sqlalchemy==1.3.22
1111
sqlalchemy_utils==0.36.8
12+
gino-starlette==0.1.1
1213

1314
# Testing
1415
pytest
1516
pytest-virtualenv
1617
requests
1718
asynctest
19+
psycopg2
1820

1921
# Linting
2022
flake8
@@ -29,4 +31,4 @@ flake8-functions
2931
flake8-expression-complexity
3032

3133
# Typing
32-
mypy
34+
mypy==0.910

0 commit comments

Comments
 (0)