Skip to content

Commit

Permalink
Merge pull request #71 from Turall/master
Browse files Browse the repository at this point in the history
add python-gino support
  • Loading branch information
awtkns authored Jul 6, 2021
2 parents 2f1c593 + 6a23912 commit 1711d00
Show file tree
Hide file tree
Showing 21 changed files with 438 additions and 19 deletions.
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

1 comment on commit 1711d00

@vercel
Copy link

@vercel vercel bot commented on 1711d00 Jul 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.