Skip to content

Commit

Permalink
Merge pull request #50 from JoshYuJump/3.0-stable
Browse files Browse the repository at this point in the history
Async Resource
  • Loading branch information
JoshYuJump authored Feb 24, 2022
2 parents fc3445d + fc64dc5 commit 0080987
Show file tree
Hide file tree
Showing 22 changed files with 492 additions and 136 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@

## [3.0.0] UNRELEASED
### Added
- Upgraded to sql-wrapper v5.0.0dev
- Upgraded to sql-wrapper v5.0.0
- Supported uvicorn 0.15
- Model support asynchronous
- Resource support asynchronous
### Changed
- Removed main.py default launch behavior

## [2.1.3] 2021-11-19
### Added
- timezone added `localtime`/`localdate`

## [2.1.0] 2021-10-14
### Changed
- Adjusted version range dependency packages

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

Simplify FastAPI integrate gRPC services development

## Requirements

1. Python 3.8+
2. FastAPI 0.63
3. grpcio>=1.32.0,<1.42


## Install

```
Expand Down Expand Up @@ -145,7 +152,7 @@ Generic HTTP/RPC support actions:
--- |--- | --- | --- | ---
|get |/{id} |GET |Get{Resource} |Get an existing resource matching the given id |
|list |/ |GET |List{Resource} |Get all the resources |
|create |/ |GET |Create{Resource} |Create a new resource |
|create |/ |POST |Create{Resource} |Create a new resource |
|update |/{id} |PATCH |Update{Resource} |Update an existing resource matching the given id |
|delete |/{id} |DELETE |Delete{Resource} |Delete an existing resource matching the given id |

Expand All @@ -154,9 +161,6 @@ Generic Actions examples:
```python

# 1. import `Resource` base class
## before 2.1
from bali.resource import Resource # deprecated in 3.0
## New from 2.1
from bali.resources import Resource


Expand Down
2 changes: 1 addition & 1 deletion bali/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.0.0-rc.1'
__version__ = '3.0.0-rc.2'
19 changes: 19 additions & 0 deletions bali/db/comparators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from collections import defaultdict

from sqlalchemy import case
from sqlalchemy.ext.hybrid import Comparator
from sqlalchemy.util.langhelpers import dictlike_iteritems


class CaseComparator(Comparator):
def __init__(self, whens, expression):
super().__init__(expression)
self.whens, self.reversed_whens = dictlike_iteritems(whens), defaultdict(list)
for k, v in self.whens:
self.reversed_whens[v].append(k)

def __clause_element__(self):
return case(self.whens, self.expression)

def __eq__(self, other):
return super().__clause_element__().in_(self.reversed_whens[other])
15 changes: 13 additions & 2 deletions bali/db/connection.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Dict, Optional, Any, Type, TypeVar, Union, List
from typing import Dict, Optional, Any, Type, TypeVar, Union, List, Tuple

from sqlalchemy.orm.query import Query
from sqlalchemy.orm.session import Session
Expand All @@ -13,11 +13,16 @@ _BooleanField = Union[bool, Column[Optional[bool]]]

class BaseModel:
__abstract__ = True
__asdict_include_hybrid_properties__ = False
created_time: _DateTimeField
updated_time: _DateTimeField
is_active: _BooleanField

def to_dict(self) -> Dict[str, Any]: ...
def _asdict(self, **kwargs) -> Dict[str, Any]: ...

def to_dict(self, **kwargs) -> Dict[str, Any]: ...

def dict(self, **kwargs) -> Dict[str, Any]: ...

@classmethod
def exists(cls: Type[_M], **attrs) -> bool: ...
Expand Down Expand Up @@ -51,6 +56,12 @@ class BaseModel:
@classmethod
def get_fields(cls: Type[_M]) -> List[str]: ...

@classmethod
def get_or_create(cls: Type[_M], defaults: Dict[str, Any], **attrs) -> Tuple[_M, bool]: ...

@classmethod
def update_or_create(cls: Type[_M], defaults: Dict[str, Any], **attrs) -> Tuple[_M, bool]: ...


class DB(SQLAlchemy):
BaseModel: BaseModel
Expand Down
23 changes: 19 additions & 4 deletions bali/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import pytz
from sqlalchemy import Column, DateTime, Boolean
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.sql.functions import func
from sqlalchemy.types import TypeDecorator
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.inspection import inspect

from ..utils import timezone

Expand Down Expand Up @@ -38,6 +41,7 @@ def process_bind_param(self, value, _):
def get_base_model(db):
class BaseModel(db.Model):
__abstract__ = True
__asdict_include_hybrid_properties__ = False

created_time = Column(DateTime, default=datetime.utcnow)
updated_time = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
Expand Down Expand Up @@ -95,11 +99,22 @@ def delete(self):
db.s.delete(self)
db.s.commit() if context_auto_commit.get() else db.s.flush()

def to_dict(self):
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns}
def _asdict(self, **kwargs):
include_hybrid_properties = kwargs.setdefault(
"include_hybrid_properties",
self.__asdict_include_hybrid_properties__
)

def dict(self):
return self.to_dict()
output_fields = []
for i in inspect(type(self)).all_orm_descriptors:
if isinstance(i, InstrumentedAttribute):
output_fields.append(i.key)
elif isinstance(i, hybrid_property) and include_hybrid_properties:
output_fields.append(i.__name__)

return {i: getattr(self, i, None) for i in output_fields}

dict = to_dict = _asdict

@classmethod
def count(cls, **attrs) -> int:
Expand Down
13 changes: 13 additions & 0 deletions bali/db/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..exceptions import OperatorModelError

OPERATOR_SPLITTER = '__'
REVERSER = "-"

OPERATORS = {
'isnull': lambda c, v: (c == None) if v else (c != None), # noqa
Expand Down Expand Up @@ -68,3 +69,15 @@ def get_filters_expr(cls, **filters):
expressions.append(op(column, value))

return expressions


def dj_lookup_to_sqla(expression: str) -> Tuple:
col_name, op_name = expression, "exact"
if OPERATOR_SPLITTER in col_name:
col_name, op_name = col_name.rsplit(OPERATOR_SPLITTER, 1)
return OPERATORS[op_name], col_name


def dj_ordering_to_sqla(expression: str):
wrapper = desc if expression.startswith(REVERSER) else asc
return wrapper(expression.lstrip(REVERSER))
2 changes: 1 addition & 1 deletion bali/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async def wrapper_async(self, *args, **kwargs):
# Convert response data to gRPC response
return ParseDict(response_data, self._response_message(), ignore_unknown_fields=True)

return func(self, *args, **kwargs)
return await func(self, *args, **kwargs)

return wrapper_async if inspect.iscoroutinefunction(func) else wrapper

Expand Down
2 changes: 0 additions & 2 deletions bali/resource.py

This file was deleted.

183 changes: 183 additions & 0 deletions bali/resources/generic_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""Generic Routes
Generic routes include list/get/create/update/delete
Used by `resource.RouterGenerator`
# TODO: Optimized boilerplate code
"""

import inspect
import logging
import typing
from typing import Callable

from fastapi import Request

from ..paginate import paginate
from ..schemas import ListRequest


# --- Utilities functions for route handler ---
def pick_route(func, async_route, route):
"""Pick coroutine or generic function for route"""
return async_route if inspect.iscoroutinefunction(func) else route


# noinspection PyUnresolvedReferences,PyProtectedMember
def list_(router_generator) -> Callable:
"""
list action default using fastapi-pagination to process paginate
"""
resource = router_generator.cls()
action_func = getattr(resource, 'list')

# noinspection PyProtectedMember,PyShadowingNames,PyUnresolvedReferences
def route(request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
params = request.query_params._dict

# parse filters
filters = {}
for k, v in params.items():
if k not in router_generator._ordered_filters:
continue
# noinspection PyBroadException
try:
# Convert param and retrieve param
params_converter = router_generator._ordered_filters.get(k)
if isinstance(params_converter, typing._GenericAlias):
params_converter = params_converter.__args__[0]
filters[k] = params_converter(v)
except Exception as ex:
logging.warning(
'Query params `%s`(value: %s) type convert failed, '
'exception: %s', k, v, ex
)
continue

schema_in = ListRequest(**params, filters=filters)

result = action_func(schema_in)
return paginate(result)

# noinspection PyProtectedMember,PyShadowingNames,PyUnresolvedReferences
async def async_route(request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
params = request.query_params._dict

# parse filters
filters = {}
for k, v in params.items():
if k not in router_generator._ordered_filters:
continue
# noinspection PyBroadException
try:
# Convert param and retrieve param
params_converter = router_generator._ordered_filters.get(k)
if isinstance(params_converter, typing._GenericAlias):
params_converter = params_converter.__args__[0]
filters[k] = params_converter(v)
except Exception as ex:
logging.warning(
'Query params `%s`(value: %s) type convert failed, '
'exception: %s', k, v, ex
)
continue

schema_in = ListRequest(**params, filters=filters)

result = await action_func(schema_in)
return paginate(result)

# update route's signatures
parameters = []
for k, v in router_generator._ordered_filters.items():
default = inspect.Parameter.empty
if isinstance(v, typing._GenericAlias):
default = None if type(None) in v.__args__ else default
parameters.append(
inspect.Parameter(
name=k,
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
default=default,
annotation=v,
)
)
sig = inspect.signature(route)
parameters.extend(sig.parameters.values())
route.__signature__ = sig.replace(parameters=parameters)

return pick_route(action_func, async_route, route)


def get_(router_generator) -> Callable:
resource = router_generator.cls()
action_func = getattr(resource, 'get')

def route(id: int, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return action_func(pk=id)

async def async_route(id: int, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return await action_func(pk=id)

return pick_route(action_func, async_route, route)


def create_(router_generator) -> Callable:
resource = router_generator.cls()
action_func = getattr(resource, 'create')

def route(schema_in: resource.schema, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return action_func(schema_in)

async def async_route(schema_in: resource.schema, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return await action_func(schema_in)

return pick_route(action_func, async_route, route)


def update_(router_generator) -> Callable:
resource = router_generator.cls()
action_func = getattr(resource, 'update')

def route(schema_in: resource.schema, id: int, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return action_func(schema_in, pk=id)

async def async_route(
schema_in: resource.schema, id: int, request: Request = None
):
resource._request = request
router_generator.check_permissions(resource)
return await action_func(schema_in, pk=id)

return pick_route(action_func, async_route, route)


def delete_(router_generator) -> Callable:
resource = router_generator.cls()
action_func = getattr(resource, 'delete')

def route(id: int, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return action_func(pk=id)

async def async_route(id: int, request: Request = None):
resource._request = request
router_generator.check_permissions(resource)
return await action_func(pk=id)

return pick_route(action_func, async_route, route)
Loading

0 comments on commit 0080987

Please sign in to comment.