Skip to content

Commit

Permalink
Merge pull request #17 from LuisLuii/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
LuisLuii authored Mar 6, 2022
2 parents 9e73d07 + dffcc2d commit 85b8759
Show file tree
Hide file tree
Showing 49 changed files with 2,512 additions and 806 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ tests/htmlcov
/pyproject.toml
*.xml
*.pyc
workspace.xml
*.iml
*.xml
*.sh
68 changes: 65 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- [Path Parameter](#path-parameter)
- [Query Parameter](#query-parameter)
- [Request Body](#request-body)
- [Foreign Tree](#foreign-tree)
- [Upsert](#upsert)
- [Add description into docs](#add-description-into-docs)
- [Relationship](#relationship)
Expand All @@ -43,9 +44,9 @@ I believe that many people who work with FastApi to build RESTful CRUD services
`FastAPI Quick CRUD` can generate CRUD methods in FastApi from an SQLAlchemy schema:

- Get one
- Get one with foreign key (IN DEVELOPMENT)
- Get one with foreign key
- Get many
- Get many with foreign key (IN DEVELOPMENT)
- Get many with foreign key
- Update one
- Update many
- Patch one
Expand Down Expand Up @@ -156,12 +157,67 @@ app.include_router(crud_route_1)
app.include_router(crud_route_2)
```

### Foreign Tree With Relationship
```python
from fastapi import FastAPI
from fastapi_quickcrud import crud_router_builder
from sqlalchemy import *
from sqlalchemy.orm import *
from fastapi_quickcrud.crud_router import generic_sql_crud_router_builder

Base = declarative_base()

class Account(Base):
__tablename__ = "account"
id = Column(Integer, primary_key=True, autoincrement=True)
blog_post = relationship("BlogPost", back_populates="account")


class BlogPost(Base):
__tablename__ = "blog_post"
id = Column(Integer, primary_key=True, autoincrement=True)
account_id = Column(Integer, ForeignKey("account.id"), nullable=False)
account = relationship("Account", back_populates="blog_post")
blog_comment = relationship("BlogComment", back_populates="blog_post")


class BlogComment(Base):
__tablename__ = "blog_comment"
id = Column(Integer, primary_key=True, autoincrement=True)
blog_id = Column(Integer, ForeignKey("blog_post.id"), nullable=False)
blog_post = relationship("BlogPost", back_populates="blog_comment")


crud_route_parent = crud_router_builder(
db_model=Account,
prefix="/account",
tags=["account"],
foreign_include=[BlogComment, BlogPost]

)
crud_route_child1 = generic_sql_crud_router_builder(
db_model=BlogPost,
prefix="/blog_post",
tags=["blog_post"],
foreign_include=[BlogComment]

)
crud_route_child2 = generic_sql_crud_router_builder(
db_model=BlogComment,
prefix="/blog_comment",
tags=["blog_comment"]

)

app = FastAPI()
[app.include_router(i) for i in [crud_route_parent, crud_route_child1, crud_route_child2]]

```

### SQLAlchemy to Pydantic Model Converter And Build your own API([example](https://github.com/LuisLuii/FastAPIQuickCRUD/blob/main/tutorial/basic_usage/quick_usage_with_async_SQLALchemy_Base.py))
```python
import uvicorn
from fastapi import FastAPI, Depends
from sqlalchemy.orm import declarative_base
from fastapi_quickcrud import CrudMethods
from fastapi_quickcrud import sqlalchemy_to_pydantic
from fastapi_quickcrud.misc.memory_sql import sync_memory_db
Expand Down Expand Up @@ -437,6 +493,12 @@ In the basic request body in the api generated by this tool, some fields are opt
* [x] it is not a primary key, but the `server_default` or `default` is True
* [x] The field is nullable


### Foreign Tree
TBC



## Upsert

** Upsert supports PosgreSQL only yet
Expand Down
127 changes: 88 additions & 39 deletions src/fastapi_quickcrud/crud_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
Depends, APIRouter
from pydantic import \
BaseModel
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql.schema import Table

from . import sqlalchemy_to_pydantic
Expand All @@ -23,7 +22,7 @@
from .misc.crud_model import CRUDModel
from .misc.memory_sql import async_memory_db, sync_memory_db
from .misc.type import CrudMethods, SqlType
from .misc.utils import convert_table_to_model
from .misc.utils import convert_table_to_model, Base

CRUDModelType = TypeVar("CRUDModelType", bound=BaseModel)
CompulsoryQueryModelType = TypeVar("CompulsoryQueryModelType", bound=BaseModel)
Expand All @@ -40,33 +39,20 @@ def crud_router_builder(
dependencies: Optional[List[callable]] = None,
crud_models: Optional[CRUDModel] = None,
async_mode: Optional[bool] = None,
foreign_include: Optional[Base] = None,
sql_type: Optional[SqlType] = None,
**router_kwargs: Any) -> APIRouter:
"""
:param db_session: Callable function
db_session should be a callable function, and return a session generator.
Also you can handle commit by yourelf or othe business logic
SQLAlchemy based example(SQLAlchemy was supported async since 1.4 version):
async:
async def get_transaction_session() -> AsyncSession:
async with async_session() as session:
async with session.begin():
yield session
sync:
def get_transaction_session():
try:
db = sync_session()
yield db
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
:param crud_methods: List[CrudMethods]
@param db_model:
The Sqlalchemy Base model/Table you want to use it to build api.
@param db_session:
The callable variable and return a session generator that will be used to get database connection session for fastapi.
@param autocommit:
set False if you handle commit in your db_session.
@param crud_methods:
Fastapi Quick CRUD supports a few of crud methods, and they save into the Enum class,
get it by : from fastapi_quickcrud import CrudMethods
example:
Expand All @@ -76,21 +62,33 @@ def get_transaction_session():
specific resource, such as GET_ONE, UPDATE_ONE, DELETE_ONE, PATCH_ONE AND POST_REDIRECT_GET
this is because POST_REDIRECT_GET need to redirect to GET_ONE api
:param exclude_columns: List[str]
@param exclude_columns:
Fastapi Quick CRUD will get all the columns in you table to generate a CRUD router,
it is allow you exclude some columns you dont want it expose to operated by API
note:
if the column in exclude list but is it not nullable or no default_value, it may throw error
when you do insert
:param crud_models:
:param db_model:
SQLAlchemy model,
:param dependencies:
:param async_mode:
:param autocommit:
:param router_kwargs: Optional arguments that ``APIRouter().include_router`` takes.
:return:
@param dependencies:
A variable that will be added to the path operation decorators.
@param crud_models:
You can use the sqlalchemy_to_pydantic() to build your own Pydantic model CRUD set
@param async_mode:
As your database connection
@param foreign_include: BaseModel
Used to build foreign tree api
@param sql_type:
You sql database type
@param router_kwargs:
other argument for FastApi's views
@return:
APIRouter for fastapi
"""

db_model, NO_PRIMARY_KEY = convert_table_to_model(db_model)
Expand All @@ -108,18 +106,19 @@ def get_transaction_session():

if async_mode is None:
async_mode = inspect.isasyncgen(db_session())

if sql_type is None:
async def async_runner(f):
return [i.bind.name async for i in f()]

try:
if async_mode:
sql_type, = asyncio.get_event_loop().run_until_complete(async_runner(db_session))
else:
sql_type, = [i.bind.name for i in db_session()]
except Exception:
raise RuntimeError("Some unknown problem occurred error, maybe you are uvicorn.run with reload=True. "
"Try declaring sql_type for crud_router_builder yourself using from fastapi_quickcrud.misc.type import SqlType")
"Try declaring sql_type for crud_router_builder yourself using from fastapi_quickcrud.misc.type import SqlType")

if not crud_methods and NO_PRIMARY_KEY == False:
crud_methods = CrudMethods.get_declarative_model_full_crud_method()
Expand All @@ -144,9 +143,15 @@ async def async_runner(f):
crud_methods=crud_methods,
exclude_columns=exclude_columns,
sql_type=sql_type,
foreign_include=foreign_include,
exclude_primary_key=NO_PRIMARY_KEY)

crud_service = query_service(model=db_model, async_mode=async_mode)
foreign_table_mapping = {db_model.__tablename__: db_model}
if foreign_include:
for i in foreign_include:
model , _= convert_table_to_model(i)
foreign_table_mapping[model.__tablename__] = i
crud_service = query_service(model=db_model, async_mode=async_mode, foreign_table_mapping=foreign_table_mapping)
# else:
# crud_service = SQLAlchemyPostgreQueryService(model=db_model, async_mode=async_mode)

Expand Down Expand Up @@ -372,6 +377,48 @@ def put_many_api(request_response_model: dict, dependencies):
async_mode=async_mode,
response_model=_response_model)

def find_one_foreign_tree_api(request_response_model: dict, dependencies):
_foreign_list_model = request_response_model.get('foreignListModel', None)
for i in _foreign_list_model:
_request_query_model = i["request_query_model"]
_response_model = i["response_model"]
_path = i["path"]
_function_name = i["function_name"]
request_url_param_model = i["primary_key_dataclass_model"]
routes_source.find_one_foreign_tree(path=_path,
request_query_model=_request_query_model,
response_model=_response_model,
request_url_param_model=request_url_param_model,
db_session=db_session,
query_service=crud_service,
parsing_service=result_parser,
execute_service=execute_service,
dependencies=dependencies,
api=api,
function_name=_function_name,
async_mode=async_mode)

def find_many_foreign_tree_api(request_response_model: dict, dependencies):
_foreign_list_model = request_response_model.get('foreignListModel', None)
for i in _foreign_list_model:
_request_query_model = i["request_query_model"]
_response_model = i["response_model"]
_path = i["path"]
_function_name = i["function_name"]
request_url_param_model = i["primary_key_dataclass_model"]
routes_source.find_many_foreign_tree(path=_path,
request_query_model=_request_query_model,
response_model=_response_model,
request_url_param_model=request_url_param_model,
db_session=db_session,
query_service=crud_service,
parsing_service=result_parser,
execute_service=execute_service,
dependencies=dependencies,
api=api,
async_mode=async_mode,
function_name=_function_name)

api_register = {
CrudMethods.FIND_ONE.value: find_one_api,
CrudMethods.FIND_MANY.value: find_many_api,
Expand All @@ -385,7 +432,9 @@ def put_many_api(request_response_model: dict, dependencies):
CrudMethods.PATCH_ONE.value: patch_one_api,
CrudMethods.PATCH_MANY.value: patch_many_api,
CrudMethods.UPDATE_ONE.value: put_one_api,
CrudMethods.UPDATE_MANY.value: put_many_api
CrudMethods.UPDATE_MANY.value: put_many_api,
CrudMethods.FIND_ONE_WITH_FOREIGN_TREE.value: find_one_foreign_tree_api,
CrudMethods.FIND_MANY_WITH_FOREIGN_TREE.value: find_many_foreign_tree_api
}
api = APIRouter(**router_kwargs)

Expand Down
Loading

0 comments on commit 85b8759

Please sign in to comment.