From 83c255369c47155971e3388a06f219daa336af3b Mon Sep 17 00:00:00 2001 From: LuisLuii Date: Mon, 17 Oct 2022 09:12:09 +0800 Subject: [PATCH 1/2] add code gen --- MANIFEST.in | 5 + sample_test.py | 87 ++ setup.py | 11 +- src/fastapi_quickcrud_codegen/__init__.py | 5 + .../crud_generator.py | 162 +++ .../generator/__init__.py | 0 .../common_module_template_generator.py | 110 ++ .../generator/crud_template_generator.py | 53 + .../generator/hardcode_template_generator.py | 88 ++ .../generator/model_template_generator.py | 50 + .../misc/__init__.py | 0 .../misc/abstract_execute.py | 34 + .../misc/abstract_parser.py | 375 +++++ .../misc/abstract_query.py | 412 ++++++ .../misc/abstract_route.py | 1281 +++++++++++++++++ .../misc/constant.py | 4 + .../misc/covert_model.py | 24 + .../misc/crud_model.py | 46 + .../misc/exceptions.py | 85 ++ .../misc/get_table_name.py | 16 + .../misc/memory_sql.py | 70 + .../misc/schema_builder.py | 1124 +++++++++++++++ src/fastapi_quickcrud_codegen/misc/type.py | 162 +++ src/fastapi_quickcrud_codegen/misc/utils.py | 349 +++++ .../model/__init__.py | 0 .../model/common_builder.py | 134 ++ .../model/crud_builder.py | 65 + .../model/model_builder.py | 133 ++ .../model/template/Constant.jinja2 | 9 + .../model/template/Enum.jinja2 | 12 + .../model/template/__init__.py | 0 .../model/template/common/__init__.py | 0 .../model/template/common/api_route.jinja2 | 3 + .../model/template/common/app.jinja2 | 15 + .../model/template/common/db.jinja2 | 4 + .../template/common/http_exception.jinja2 | 71 + .../template/common/memory_sql_session.jinja2 | 91 ++ .../template/common/route_container.jinja2 | 3 + .../model/template/common/typing.jinja2 | 154 ++ .../model/template/common/utils.jinja2 | 103 ++ .../model/template/pydantic/BaseModel.jinja2 | 38 + .../template/pydantic/BaseModel_root.jinja2 | 31 + .../model/template/pydantic/Config.jinja2 | 4 + .../model/template/pydantic/__init__.py | 0 .../model/template/pydantic/dataclass.jinja2 | 38 + .../template/pydantic/dataclass_method.jinja2 | 4 + .../pydantic/exclude_unset_baseModel.jinja2 | 6 + .../model/template/route/__init__.py | 0 .../model/template/route/sync_find_one.jinja2 | 30 + .../model/template/sqlalchemy/__init__.py | 0 .../template/sqlalchemy/dataclass.jinja2 | 24 + .../parse/__init__.py | 0 .../parse/parse_import.py | 24 + src/sample_test.py | 86 ++ tutorial/foreign_tree/async_m2m.py | 1 + 55 files changed, 5633 insertions(+), 3 deletions(-) create mode 100644 MANIFEST.in create mode 100644 sample_test.py create mode 100644 src/fastapi_quickcrud_codegen/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/crud_generator.py create mode 100644 src/fastapi_quickcrud_codegen/generator/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py create mode 100644 src/fastapi_quickcrud_codegen/generator/crud_template_generator.py create mode 100644 src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py create mode 100644 src/fastapi_quickcrud_codegen/generator/model_template_generator.py create mode 100644 src/fastapi_quickcrud_codegen/misc/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_execute.py create mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_parser.py create mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_query.py create mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_route.py create mode 100644 src/fastapi_quickcrud_codegen/misc/constant.py create mode 100644 src/fastapi_quickcrud_codegen/misc/covert_model.py create mode 100644 src/fastapi_quickcrud_codegen/misc/crud_model.py create mode 100644 src/fastapi_quickcrud_codegen/misc/exceptions.py create mode 100644 src/fastapi_quickcrud_codegen/misc/get_table_name.py create mode 100644 src/fastapi_quickcrud_codegen/misc/memory_sql.py create mode 100644 src/fastapi_quickcrud_codegen/misc/schema_builder.py create mode 100644 src/fastapi_quickcrud_codegen/misc/type.py create mode 100644 src/fastapi_quickcrud_codegen/misc/utils.py create mode 100644 src/fastapi_quickcrud_codegen/model/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/model/common_builder.py create mode 100644 src/fastapi_quickcrud_codegen/model/crud_builder.py create mode 100644 src/fastapi_quickcrud_codegen/model/model_builder.py create mode 100644 src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/route/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/model/template/sqlalchemy/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 create mode 100644 src/fastapi_quickcrud_codegen/parse/__init__.py create mode 100644 src/fastapi_quickcrud_codegen/parse/parse_import.py create mode 100644 src/sample_test.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e526c8a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include src/fastapi_quickcrud_codegen/model/template/common/*.jinja2 +include src/fastapi_quickcrud_codegen/model/template/pydantic/*.jinja2 +include src/fastapi_quickcrud_codegen/model/template/route/*.jinja2 +include src/fastapi_quickcrud_codegen/model/template/sqlalchemy/*.jinja2 +include src/fastapi_quickcrud_codegen/model/template/*.jinja2 \ No newline at end of file diff --git a/sample_test.py b/sample_test.py new file mode 100644 index 0000000..4034d38 --- /dev/null +++ b/sample_test.py @@ -0,0 +1,87 @@ +import os + +from fastapi import FastAPI +from sqlalchemy import ARRAY, BigInteger, Boolean, CHAR, Column, Date, DateTime, Float, Integer, \ + JSON, Numeric, SmallInteger, String, Text, Time, UniqueConstraint, text +from sqlalchemy.dialects.postgresql import INTERVAL, JSONB, UUID +from sqlalchemy.orm import declarative_base, sessionmaker + +from fastapi_quickcrud_codegen import crud_router_builder, CrudMethods + +TEST_DATABASE_URL = os.environ.get('TEST_DATABASE_URL', 'postgresql://postgres:1234@127.0.0.1:5432/postgres') + +app = FastAPI() + +Base = declarative_base() +metadata = Base.metadata + +from sqlalchemy import create_engine + +engine = create_engine(TEST_DATABASE_URL, future=True, echo=True, + pool_use_lifo=True, pool_pre_ping=True, pool_recycle=7200) +async_session = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_transaction_session(): + try: + db = async_session() + yield db + finally: + db.close() + + +class UntitledTable256(Base): + primary_key_of_table = "primary_key" + unique_fields = ['primary_key', 'int4_value', 'float4_value'] + __tablename__ = 'test_build_myself' + __table_args__ = ( + UniqueConstraint('primary_key', 'int4_value', 'float4_value'), + ) + primary_key = Column(Integer, primary_key=True, info={'alias_name': 'primary_key'}, autoincrement=True, + server_default="nextval('test_build_myself_id_seq'::regclass)") + bool_value = Column(Boolean, nullable=False, server_default=text("false")) + # bytea_value = Column(LargeBinary) + char_value = Column(CHAR(10)) + date_value = Column(Date, server_default=text("now()")) + float4_value = Column(Float, nullable=False) + float8_value = Column(Float(53), nullable=False, server_default=text("10.10")) + int2_value = Column(SmallInteger, nullable=False) + int4_value = Column(Integer, nullable=True) + int8_value = Column(BigInteger, server_default=text("99")) + interval_value = Column(INTERVAL) + json_value = Column(JSON) + jsonb_value = Column(JSONB(astext_type=Text())) + numeric_value = Column(Numeric) + text_value = Column(Text) + time_value = Column(Time) + timestamp_value = Column(DateTime) + timestamptz_value = Column(DateTime(True)) + timetz_value = Column(Time(True)) + uuid_value = Column(UUID(as_uuid=True)) + varchar_value = Column(String) + # xml_value = Column(NullType) + array_value = Column(ARRAY(Integer())) + array_str__value = Column(ARRAY(String())) + # box_valaue = Column(NullType) + + +crud_route_child2 = crud_router_builder( + db_model=UntitledTable256, + prefix="/blog_comment", + tags=["blog_comment"], + db_session=get_transaction_session, + crud_methods=[CrudMethods.FIND_ONE] +) + +app = FastAPI() +[app.include_router(i) for i in [crud_route_child2]] + + +@app.get("/", tags=["child"]) +async def root(): + return {"message": "Hello World"} + + +import uvicorn + +uvicorn.run(app, host="0.0.0.0", port=8002, debug=False) diff --git a/setup.py b/setup.py index 72c2860..a04c1b1 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = '0.2.1' +VERSION = '0.0.7' print(""" @@ -16,7 +16,7 @@ if __name__ == '__main__': setup( - name='fastapi_quickcrud', + name='fastapi_quickcrud_code_generator_beta', version=VERSION, install_requires=["fastapi<=0.68.2","pydantic<=1.8.2","SQLAlchemy<=1.4.30","StrEnum==0.4.7","starlette==0.14.2", "aiosqlite==0.17.0","uvicorn==0.17.0","greenlet==1.1.2","anyio==3.5.0"], @@ -29,7 +29,11 @@ url='https://github.com/LuisLuii/FastAPIQuickCRUD', license="MIT License", keywords=["fastapi", "crud", "restful", "routing","SQLAlchemy", "generator", "crudrouter","postgresql","builder"], - packages=find_packages('src'), + packages=find_packages("src", include="*.jinja2"), + package_data={ + '': ['*.jinja2'], + 'src.fastapi_quickcrud_codegen.model.template.common': ['*.jinja2'], + }, package_dir={'': 'src'}, setup_requires=["setuptools>=31.6.0"], classifiers=[ @@ -60,3 +64,4 @@ ], include_package_data=True, ) + print(find_packages("src")) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/__init__.py b/src/fastapi_quickcrud_codegen/__init__.py new file mode 100644 index 0000000..8d796ff --- /dev/null +++ b/src/fastapi_quickcrud_codegen/__init__.py @@ -0,0 +1,5 @@ +from .misc.utils import sqlalchemy_to_pydantic +from .crud_generator import crud_router_builder +from .misc.type import CrudMethods + + diff --git a/src/fastapi_quickcrud_codegen/crud_generator.py b/src/fastapi_quickcrud_codegen/crud_generator.py new file mode 100644 index 0000000..4bbe1f2 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/crud_generator.py @@ -0,0 +1,162 @@ +from typing import \ + List, \ + TypeVar, Union, Optional + +from fastapi import \ + APIRouter +from pydantic import \ + BaseModel +from sqlalchemy.sql.schema import Table + +from . import sqlalchemy_to_pydantic +from .generator.common_module_template_generator import CommonModuleTemplateGenerator +from .generator.crud_template_generator import CrudTemplateGenerator +from .misc.crud_model import CRUDModel +from .misc.get_table_name import get_table_name +from .misc.type import CrudMethods, SqlType +from .misc.utils import convert_table_to_model +from .model.common_builder import CommonCodeGen +from .model.crud_builder import CrudCodeGen + +CRUDModelType = TypeVar("CRUDModelType", bound=BaseModel) +CompulsoryQueryModelType = TypeVar("CompulsoryQueryModelType", bound=BaseModel) +OnConflictModelType = TypeVar("OnConflictModelType", bound=BaseModel) + + +def crud_router_builder( + *, + db_model_list: Union[Table, 'DeclarativeBaseModel'], + async_mode: Optional[bool], + sql_type: Optional[SqlType], + crud_methods: Optional[List[CrudMethods]] = None, + exclude_columns: Optional[List[str]] = None, + # foreign_include: Optional[Base] = None +) -> APIRouter: + """ + @param db_model: + The Sqlalchemy Base model/Table you want to use it to build api. + + @param async_mode: + As your database connection + + @param sql_type: + You sql database type + + @param db_session: + The callable variable and return a session generator that will be used to get database connection session for fastapi. + + @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_codegen_codegen import CrudMethods + example: + [CrudMethods.GET_MANY,CrudMethods.ONE] + note: + if there is no primary key in your SQLAlchemy model, it dose not support request with + 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: + 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 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 foreign_include: BaseModel + Used to build foreign tree api + + @return: + APIRouter for fastapi + """ + model_list = [] + for db_model_info in db_model_list: + + db_model = db_model_info["db_model"] + prefix = db_model_info["prefix"] + tags = db_model_info["tags"] + + table_name = db_model.__name__ + model_name = get_table_name(db_model) + + model_list.append({"model_name": model_name, "file_name": table_name}) + + + db_model, NO_PRIMARY_KEY = convert_table_to_model(db_model) + + # code gen + crud_code_generator = CrudCodeGen(model_name, model_name=table_name, tags=tags, prefix=prefix) + # create a file + crud_template_generator = CrudTemplateGenerator() + + constraints = db_model.__table__.constraints + + common_module_template_generator = CommonModuleTemplateGenerator() + + # type + common_code_builder = CommonCodeGen() + common_code_builder.build_type() + common_code_builder.gen(common_module_template_generator.add_type) + + # module + common_utils_code_builder = CommonCodeGen() + common_utils_code_builder.build_utils() + common_utils_code_builder.gen(common_module_template_generator.add_utils) + + # http_exception + common_http_exception_code_builder = CommonCodeGen() + common_http_exception_code_builder.build_http_exception() + common_http_exception_code_builder.gen(common_module_template_generator.add_http_exception) + + # db + common_db_code_builder = CommonCodeGen() + common_db_code_builder.build_db() + common_db_code_builder.gen(common_module_template_generator.add_db) + + if not crud_methods and NO_PRIMARY_KEY == False: + crud_methods = CrudMethods.get_declarative_model_full_crud_method() + if not crud_methods and NO_PRIMARY_KEY == True: + crud_methods = CrudMethods.get_table_full_crud_method() + + crud_models_builder: CRUDModel = sqlalchemy_to_pydantic + crud_models: CRUDModel = crud_models_builder(db_model=db_model, + constraints=constraints, + crud_methods=crud_methods, + exclude_columns=exclude_columns, + sql_type=sql_type, + exclude_primary_key=NO_PRIMARY_KEY) + + methods_dependencies = crud_models.get_available_request_method() + primary_name = crud_models.PRIMARY_KEY_NAME + if primary_name: + path = '/{' + primary_name + '}' + else: + path = "" + + def find_one_api(): + crud_code_generator.build_find_one_route(async_mode=async_mode, path=path) + + api_register = { + CrudMethods.FIND_ONE.value: find_one_api, + } + for request_method in methods_dependencies: + value_of_dict_crud_model = crud_models.get_model_by_request_method(request_method) + crud_model_of_this_request_methods = value_of_dict_crud_model.keys() + for crud_model_of_this_request_method in crud_model_of_this_request_methods: + api_register[crud_model_of_this_request_method.value]() + crud_code_generator.gen(crud_template_generator) + + # sql session + common_db_session_code_builder = CommonCodeGen() + common_db_session_code_builder.build_db_session(model_list=model_list) + common_db_session_code_builder.gen(common_module_template_generator.add_memory_sql_session) + + # app py + common_app_code_builder = CommonCodeGen() + common_app_code_builder.build_app(model_list=model_list) + common_app_code_builder.gen(common_module_template_generator.add_app) diff --git a/src/fastapi_quickcrud_codegen/generator/__init__.py b/src/fastapi_quickcrud_codegen/generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py b/src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py new file mode 100644 index 0000000..3be7edd --- /dev/null +++ b/src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py @@ -0,0 +1,110 @@ +import inspect +import os +import shutil +import sys + +from sqlalchemy import Table + +from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, ROUTE, COMMON + + +class CommonModuleTemplateGenerator: + def __init__(self): + dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) + self.current_directory = dirname + self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) + self.module_path_map = {} + + + def __create_root_template_folder(self): + if not os.path.exists(self.template_root_directory): + os.makedirs(self.template_root_directory) + + def __create_folder(self, path): + if not os.path.exists(path): + os.makedirs(path) + + def __create_module_folder(self): + if not os.path.exists(self.template_model_directory): + os.makedirs(self.template_model_directory) + + def add_resolver(self, model_name, code): + template_module_directory = os.path.join(self.template_root_directory, model_name) + template_model_directory = os.path.join(template_module_directory) + + path = f'{template_model_directory}/__init__.py' + self.add_to_model_file(path, "") + + self.__create_model_folder(template_model_directory) + path = f'{template_model_directory}/{model_name}.py' + self.add_to_model_file(path, code) + self.module_path_map[model_name] = {'model': path} + + def add_type(self, code): + template_module_directory = os.path.join(self.template_root_directory, COMMON) + self.__create_folder(template_module_directory) + + path = f'{template_module_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + path = f'{template_module_directory}/typing.py' + self.create_file_and_add_code_into_there(path, code) + + def add_utils(self, code): + template_module_directory = os.path.join(self.template_root_directory, COMMON) + self.__create_folder(template_module_directory) + + path = f'{template_module_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + path = f'{template_module_directory}/utils.py' + self.create_file_and_add_code_into_there(path, code) + + def add_http_exception(self, code): + template_module_directory = os.path.join(self.template_root_directory, COMMON) + self.__create_folder(template_module_directory) + + path = f'{template_module_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + path = f'{template_module_directory}/http_exception.py' + self.create_file_and_add_code_into_there(path, code) + + def add_db(self, code): + template_module_directory = os.path.join(self.template_root_directory, COMMON) + self.__create_folder(template_module_directory) + + path = f'{template_module_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + path = f'{template_module_directory}/db.py' + self.create_file_and_add_code_into_there(path, code) + + def add_memory_sql_session(self, code): + + template_module_directory = os.path.join(self.template_root_directory, COMMON) + self.__create_folder(template_module_directory) + + path = f'{template_module_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + path = f'{template_module_directory}/sql_session.py' + self.create_file_and_add_code_into_there(path, code) + + + def add_app(self, code): + + template_module_directory = os.path.join(self.template_root_directory) + self.__create_folder(template_module_directory) + + path = f'{template_module_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + path = f'{template_module_directory}/app.py' + self.create_file_and_add_code_into_there(path, code) + + @staticmethod + def create_file_and_add_code_into_there(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + diff --git a/src/fastapi_quickcrud_codegen/generator/crud_template_generator.py b/src/fastapi_quickcrud_codegen/generator/crud_template_generator.py new file mode 100644 index 0000000..5fd1aea --- /dev/null +++ b/src/fastapi_quickcrud_codegen/generator/crud_template_generator.py @@ -0,0 +1,53 @@ +import os +import sys + +from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, ROUTE + + +class CrudTemplateGenerator: + def __init__(self): + dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) + self.current_directory = dirname + self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) + self.module_path_map = {} + + + def __create_root_template_folder(self): + if not os.path.exists(self.template_root_directory): + os.makedirs(self.template_root_directory) + + def __create_model_folder(self, path): + if not os.path.exists(path): + os.makedirs(path) + + def __create_module_folder(self): + if not os.path.exists(self.template_model_directory): + os.makedirs(self.template_model_directory) + + def add_route(self, model_name, code): + template_model_directory = os.path.join(self.template_root_directory, ROUTE) + + self.__create_model_folder(template_model_directory) + + path = f'{template_model_directory}/__init__.py' + self.add_code_to_file(path, "") + + path = f'{template_model_directory}/{model_name}.py' + self.add_code_to_file(path, code) + # self.module_path_map[model_name] = {'model': path} + + + + @staticmethod + def add_code_to_file(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + + @staticmethod + def add_to_controller_file(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + + + + diff --git a/src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py b/src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py new file mode 100644 index 0000000..29d8246 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py @@ -0,0 +1,88 @@ +import inspect +import os +import shutil +import sys + +from sqlalchemy import Table + +from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, ROUTE + + +class HardCodeTemplateGenerator: + def __init__(self): + dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) + self.current_directory = dirname + self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) + self.module_path_map = {} + + + def __create_root_template_folder(self): + if not os.path.exists(self.template_root_directory): + os.makedirs(self.template_root_directory) + + def __create_model_folder(self, path): + if not os.path.exists(path): + os.makedirs(path) + + def __create_module_folder(self): + if not os.path.exists(self.template_model_directory): + os.makedirs(self.template_model_directory) + + def add_resolver(self, model_name, code): + template_module_directory = os.path.join(self.template_root_directory, model_name) + template_model_directory = os.path.join(template_module_directory, ROUTE) + + path = f'{template_model_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + self.__create_model_folder(template_model_directory) + path = f'{template_model_directory}/{model_name}.py' + self.create_file_and_add_code_into_there(path, code) + self.module_path_map[model_name] = {'model': path} + + def add_type(self, code): + template_module_directory = os.path.join(self.template_root_directory, "typing") + template_model_directory = os.path.join(template_module_directory, ROUTE) + + path = f'{template_model_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + self.__create_model_folder(template_model_directory) + path = f'{template_model_directory}/typing.py' + self.create_file_and_add_code_into_there(path, code) + + def add_utils(self, code): + template_module_directory = os.path.join(self.template_root_directory, "find_query_builder") + template_model_directory = os.path.join(template_module_directory, ROUTE) + + path = f'{template_model_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + self.__create_model_folder(template_model_directory) + path = f'{template_model_directory}/find_query_builder.py' + self.create_file_and_add_code_into_there(path, code) + + def add_http_exception(self, code): + template_module_directory = os.path.join(self.template_root_directory, "http_exception") + template_model_directory = os.path.join(template_module_directory, ROUTE) + + path = f'{template_model_directory}/__init__.py' + self.create_file_and_add_code_into_there(path, "") + + self.__create_model_folder(template_model_directory) + path = f'{template_model_directory}/http_exception.py' + self.create_file_and_add_code_into_there(path, code) + + @staticmethod + def create_file_and_add_code_into_there(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + + @staticmethod + def add_to_controller_file(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + + + + diff --git a/src/fastapi_quickcrud_codegen/generator/model_template_generator.py b/src/fastapi_quickcrud_codegen/generator/model_template_generator.py new file mode 100644 index 0000000..174bef8 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/generator/model_template_generator.py @@ -0,0 +1,50 @@ +import os +import sys + +from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, MODEL + + +class ModelTemplateGenerator: + def __init__(self): + dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) + self.current_directory = dirname + self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) + self.module_path_map = {} + + def __create_root_template_folder(self): + if not os.path.exists(self.template_root_directory): + os.makedirs(self.template_root_directory) + + def __create_model_folder(self, path): + if not os.path.exists(path): + os.makedirs(path) + + def __create_module_folder(self): + if not os.path.exists(self.template_model_directory): + os.makedirs(self.template_model_directory) + + def add_model(self, model_name, code): + template_model_directory = os.path.join(self.template_root_directory, MODEL) + self.__create_model_folder(template_model_directory) + + path = f'{template_model_directory}/__init__.py' + self.add_code_to_file(path, "") + + path = f'{template_model_directory}/{model_name}.py' + self.add_code_to_file(path, code) + self.module_path_map[model_name] = {'model': path} + + @staticmethod + def add_code_to_file(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + + @staticmethod + def add_to_controller_file(path, code): + with open(path, 'a') as model_file: + model_file.write(code) + + +model_template_gen = ModelTemplateGenerator() + + diff --git a/src/fastapi_quickcrud_codegen/misc/__init__.py b/src/fastapi_quickcrud_codegen/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_execute.py b/src/fastapi_quickcrud_codegen/misc/abstract_execute.py new file mode 100644 index 0000000..864fcc3 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/abstract_execute.py @@ -0,0 +1,34 @@ +from typing import Any + +from sqlalchemy.sql.elements import BinaryExpression + + +class SQLALchemyExecuteService(object): + + def __init__(self): + pass + + @staticmethod + def add(session, model) -> Any: + session.add(model) + + @staticmethod + def add_all(session, model) -> Any: + session.add_all(model) + + @staticmethod + async def async_flush(session) -> Any: + await session.flush() + + @staticmethod + def flush(session) -> Any: + session.flush() + + @staticmethod + async def async_execute(session, stmt: BinaryExpression) -> Any: + return await session.execute(stmt) + + @staticmethod + def execute(session, stmt: BinaryExpression) -> Any: + return session.execute(stmt) + diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_parser.py b/src/fastapi_quickcrud_codegen/misc/abstract_parser.py new file mode 100644 index 0000000..b58de4d --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/abstract_parser.py @@ -0,0 +1,375 @@ +import copy +from http import HTTPStatus +from urllib.parse import urlencode +from pydantic import parse_obj_as +from starlette.responses import Response, RedirectResponse + +from .utils import group_find_many_join +from .exceptions import FindOneApiNotRegister + + +class SQLAlchemyGeneralSQLeResultParse(object): + + def __init__(self, async_model, crud_models): + + """ + :param async_model: bool + :param crud_models: pre ready + :param autocommit: bool + """ + + self.async_mode = async_model + self.crud_models = crud_models + self.primary_name = crud_models.PRIMARY_KEY_NAME + + async def async_commit(self, session): + await session.commit() + + def commit(self, session): + session.commit() + + async def async_delete(self, session, data): + await session.delete(data) + + def delete(self, session, data): + session.delete(data) + + def update_data_model(self, data, update_args): + for update_arg_name, update_arg_value in update_args.items(): + setattr(data, update_arg_name, update_arg_value) + return data + + @staticmethod + async def async_rollback(session): + await session.rollback() + + @staticmethod + def rollback(session): + session.rollback() + + @staticmethod + def _response_builder(sql_execute_result, fastapi_response, response_model): + result = parse_obj_as(response_model, sql_execute_result) + fastapi_response.headers["x-total-count"] = str(len(sql_execute_result) if isinstance(sql_execute_result, list) + else '1') + return result + + # async def async_update_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + # result = self._response_builder(sql_execute_result, fastapi_response, response_model) + # await self.async_commit(kwargs.get('session')) + # return result + # + # async def async_patch_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + # result = self._response_builder(sql_execute_result, fastapi_response, response_model) + # await self.async_commit(kwargs.get('session')) + # return result + # + # def patch_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + # result = self._response_builder(sql_execute_result, fastapi_response, response_model) + # self.commit(kwargs.get('session')) + # return result + + def update_func(self, response_model, sql_execute_result, fastapi_response, update_args, update_one): + if not isinstance(sql_execute_result, list): + sql_execute_result = [sql_execute_result] + tmp = [] + for i in sql_execute_result: + tmp.append(self.update_data_model(i, update_args=update_args)) + + if not update_one: + sql_execute_result = tmp + else: + sql_execute_result, = tmp + return self._response_builder(response_model=response_model, + sql_execute_result=sql_execute_result, + fastapi_response=fastapi_response) + + def update(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): + session = kwargs.get('session') + update_one = kwargs.get('update_one') + result = self.update_func(response_model, sql_execute_result, fastapi_response, update_args, update_one) + self.commit(session) + return result + + async def async_update(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): + session = kwargs.get('session') + update_one = kwargs.get('update_one') + result = self.update_func(response_model, sql_execute_result, fastapi_response, update_args, update_one) + await self.async_commit(session) + return result + + @staticmethod + def find_one_sub_func(sql_execute_result, response_model, fastapi_response, **kwargs): + join = kwargs.get('join_mode', None) + + one_row_data = sql_execute_result.fetchall() + if not one_row_data: + return Response('specific data not found', status_code=HTTPStatus.NOT_FOUND) + response = [] + for i in one_row_data: + i = dict(i) + result__ = copy.deepcopy(i) + tmp = {} + for key_, value_ in result__.items(): + if '_____' in key_: + key, foreign_column = key_.split('_____') + if key not in tmp: + tmp[key] = {foreign_column: value_} + else: + tmp[key][foreign_column] = value_ + else: + tmp[key_] = value_ + response.append(tmp) + if join: + response = group_find_many_join(response) + if isinstance(response, list): + response = response[0] + fastapi_response.headers["x-total-count"] = str(1) + return response + + async def async_find_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.find_one_sub_func(sql_execute_result, response_model, fastapi_response, **kwargs) + await self.async_commit(kwargs.get('session')) + return result + + def find_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.find_one_sub_func(sql_execute_result, response_model, fastapi_response, **kwargs) + self.commit(kwargs.get('session')) + return result + + @staticmethod + def find_many_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs): + join = kwargs.get('join_mode', None) + result = sql_execute_result.fetchall() + if not result: + return Response(status_code=HTTPStatus.NO_CONTENT) + response = [] + for i in result: + i = dict(i) + result__ = copy.deepcopy(i) + tmp = {} + for key_, value_ in result__.items(): + if '_____' in key_: + key, foreign_column = key_.split('_____') + if key not in tmp: + tmp[key] = {foreign_column: value_} + else: + tmp[key][foreign_column] = value_ + else: + tmp[key_] = value_ + response.append(tmp) + + fastapi_response.headers["x-total-count"] = str(len(response)) + if join: + response = group_find_many_join(response) + response = parse_obj_as(response_model, response) + return response + + async def async_find_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.find_many_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) + await self.async_commit(kwargs.get('session')) + return result + + def find_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.find_many_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) + self.commit(kwargs.get('session')) + return result + + # @staticmethod + # def update_one_sub_func(response_model, sql_execute_result, fastapi_response): + # result = parse_obj_as(response_model, sql_execute_result) + # fastapi_response.headers["x-total-count"] = str(1) + # return result + # + # async def async_update_one(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): + # session = kwargs.get('session') + # if not sql_execute_result: + # return Response(status_code=HTTPStatus.NOT_FOUND) + # data = self.update_data_model(sql_execute_result, update_args=update_args) + # result = self.update_one_sub_func(response_model, data, fastapi_response) + # await self.commit(session) + # return result + # + # def update_one(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): + # session = kwargs.get('session') + # if not sql_execute_result: + # return Response(status_code=HTTPStatus.NOT_FOUND) + # data = self.update_data_model(sql_execute_result, update_args=update_args) + # result = self.update_one_sub_func(response_model, data, fastapi_response) + # self.commit(session) + # return result + + # async def async_patch_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + # result = self.update_one_sub_func(response_model, sql_execute_result, fastapi_response) + # await self.async_commit(kwargs.get('session')) + # return result + # + # def patch_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + # result = self.update_one_sub_func(response_model, sql_execute_result, fastapi_response) + # self.commit(kwargs.get('session')) + # return result + + @staticmethod + def create_one_sub_func(response_model, sql_execute_result, fastapi_response): + inserted_data, = sql_execute_result + result = parse_obj_as(response_model, inserted_data) + fastapi_response.headers["x-total-count"] = str(1) + return result + + async def async_create_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.create_one_sub_func(response_model, sql_execute_result, fastapi_response) + await self.async_commit(kwargs.get('session')) + return result + + def create_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.create_one_sub_func(response_model, sql_execute_result, fastapi_response) + self.commit(kwargs.get('session')) + return result + + @staticmethod + def create_many_sub_func(response_model, sql_execute_result, fastapi_response): + result = parse_obj_as(response_model, sql_execute_result) + fastapi_response.headers["x-total-count"] = str(len(sql_execute_result)) + return result + + async def async_create_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.create_many_sub_func(response_model, sql_execute_result, fastapi_response) + await self.async_commit(kwargs.get('session')) + return result + + def create_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.create_many_sub_func(response_model, sql_execute_result, fastapi_response) + self.commit(kwargs.get('session')) + return result + + @staticmethod + def upsert_one_sub_func(response_model, sql_execute_result, fastapi_response): + sql_execute_result = sql_execute_result.fetchone() + result = parse_obj_as(response_model, dict(sql_execute_result)) + fastapi_response.headers["x-total-count"] = str(1) + return result + + async def async_upsert_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.upsert_one_sub_func(response_model, sql_execute_result, fastapi_response) + await self.async_commit(kwargs.get('session')) + return result + + def upsert_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.upsert_one_sub_func(response_model, sql_execute_result, fastapi_response) + self.commit(kwargs.get('session')) + return result + + @staticmethod + def upsert_many_sub_func(response_model, sql_execute_result, fastapi_response): + insert_result_list = sql_execute_result.fetchall() + result = parse_obj_as(response_model, insert_result_list) + fastapi_response.headers["x-total-count"] = str(len(insert_result_list)) + return result + + async def async_upsert_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.upsert_many_sub_func(response_model, sql_execute_result, fastapi_response) + await self.async_commit(kwargs.get('session')) + return result + + def upsert_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + result = self.upsert_many_sub_func(response_model, sql_execute_result, fastapi_response) + self.commit(kwargs.get('session')) + return result + + def delete_one_sub_func(self, response_model, sql_execute_result, fastapi_response, **kwargs): + if not sql_execute_result: + return Response(status_code=HTTPStatus.NOT_FOUND) + result = parse_obj_as(response_model, sql_execute_result) + fastapi_response.headers["x-total-count"] = str(1) + return result + + def delete_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + session = kwargs.get('session') + if sql_execute_result: + self.delete(session, sql_execute_result) + result = self.delete_one_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) + self.commit(session) + return result + + async def async_delete_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): + session = kwargs.get('session') + if sql_execute_result: + self.delete(session, sql_execute_result) + result = self.delete_one_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) + await self.async_commit(session) + return result + + def delete_many_sub_func(self, response_model, sql_execute_result, fastapi_response): + if not sql_execute_result: + return Response(status_code=HTTPStatus.NO_CONTENT) + deleted_rows = sql_execute_result + result = parse_obj_as(response_model, deleted_rows) + fastapi_response.headers["x-total-count"] = str(len(deleted_rows)) + return result + + def delete_many(self, *, response_model, sql_execute_results, fastapi_response, **kwargs): + session = kwargs.get('session') + if sql_execute_results: + for sql_execute_result in sql_execute_results: + self.delete(session, sql_execute_result) + result = self.delete_many_sub_func(response_model, sql_execute_results, fastapi_response) + self.commit(session) + return result + + async def async_delete_many(self, *, response_model, sql_execute_results, fastapi_response, **kwargs): + session = kwargs.get('session') + if sql_execute_results: + for sql_execute_result in sql_execute_results: + await self.async_delete(session, sql_execute_result) + result = self.delete_many_sub_func(response_model, sql_execute_results, fastapi_response) + await self.async_commit(session) + return result + + def has_end_point(self, fastapi_request) -> bool: + redirect_end_point = fastapi_request.url.path + "/{" + self.primary_name + "}" + redirect_url_exist = False + for route in fastapi_request.app.routes: + if route.path == redirect_end_point: + route_request_method, = route.methods + if route_request_method.upper() == 'GET': + redirect_url_exist = True + return redirect_url_exist + + def post_redirect_get_sub_func(self, response_model, sql_execute_result, fastapi_request): + result = parse_obj_as(response_model, sql_execute_result) + primary_key_field = result.__dict__.pop(self.primary_name, None) + assert primary_key_field is not None + redirect_url = fastapi_request.url.path + "/" + str(primary_key_field) + return redirect_url + + def get_post_redirect_get_url(self, response_model, sql_execute_result, fastapi_request): + redirect_url = self.post_redirect_get_sub_func(response_model, sql_execute_result, fastapi_request) + header_dict = {i[0].decode("utf-8"): i[1].decode("utf-8") for i in fastapi_request.headers.__dict__['_list']} + redirect_url += f'?{urlencode(header_dict)}' + return redirect_url + + async def async_post_redirect_get(self, *, response_model, sql_execute_result, fastapi_request, **kwargs): + session = kwargs['session'] + if not self.has_end_point(fastapi_request): + await self.async_rollback(session) + raise FindOneApiNotRegister(404, + f'End Point {fastapi_request.url.path}/{ {self.primary_name} }' + f' with GET method not found') + redirect_url = self.get_post_redirect_get_url(response_model, sql_execute_result, fastapi_request) + await self.async_commit(session) + return RedirectResponse(redirect_url, + status_code=HTTPStatus.SEE_OTHER + ) + + def post_redirect_get(self, *, response_model, sql_execute_result, fastapi_request, **kwargs): + session = kwargs['session'] + if not self.has_end_point(fastapi_request): + self.rollback(session) + raise FindOneApiNotRegister(404, + f'End Point {fastapi_request.url.path}/{ {self.primary_name} }' + f' with GET method not found') + redirect_url = self.get_post_redirect_get_url(response_model, sql_execute_result, fastapi_request) + self.commit(session) + return RedirectResponse(redirect_url, + status_code=HTTPStatus.SEE_OTHER + ) diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_query.py b/src/fastapi_quickcrud_codegen/misc/abstract_query.py new file mode 100644 index 0000000..89252f4 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/abstract_query.py @@ -0,0 +1,412 @@ +from abc import ABC +from typing import List, Union + +from sqlalchemy import and_, select, text +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.sql.elements import BinaryExpression +from sqlalchemy.sql.schema import Table + +from .exceptions import UnknownOrderType, UnknownColumn, UpdateColumnEmptyException +from .type import Ordering +from .utils import clean_input_fields, path_query_builder +from .utils import find_query_builder + + +class SQLAlchemyGeneralSQLQueryService(ABC): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + + """ + :param model: declarative_base model + :param async_mode: bool + """ + + self.model = model + self.model_columns = model + self.async_mode = async_mode + self.foreign_table_mapping = foreign_table_mapping + + def get_many(self, *, + join_mode, + query, + target_model=None, + abstract_param=None + ) -> BinaryExpression: + + filter_args = query + limit = filter_args.pop('limit', None) + offset = filter_args.pop('offset', None) + order_by_columns = filter_args.pop('order_by_columns', None) + model = self.model + if target_model: + model = self.foreign_table_mapping[target_model] + filter_list: List[BinaryExpression] = find_query_builder(param=filter_args, + model=model) + path_filter_list: List[BinaryExpression] = path_query_builder(params=abstract_param, + model=self.foreign_table_mapping) + join_table_instance_list: list = self.get_join_select_fields(join_mode) + + + if not isinstance(self.model, Table): + model = model.__table__ + + stmt = select(*[model] + join_table_instance_list).filter(and_(*filter_list+path_filter_list)) + if order_by_columns: + order_by_query_list = [] + + for order_by_column in order_by_columns: + if not order_by_column: + continue + sort_column, order_by = (order_by_column.replace(' ', '').split(':') + [None])[:2] + if not hasattr(self.model_columns, sort_column): + raise UnknownColumn(f'column {sort_column} is not exited') + if not order_by: + order_by_query_list.append(getattr(self.model_columns, sort_column).asc()) + elif order_by.upper() == Ordering.DESC.upper(): + order_by_query_list.append(getattr(self.model_columns, sort_column).desc()) + elif order_by.upper() == Ordering.ASC.upper(): + order_by_query_list.append(getattr(self.model_columns, sort_column).asc()) + else: + raise UnknownOrderType(f"Unknown order type {order_by}, only accept DESC or ASC") + if order_by_query_list: + stmt = stmt.order_by(*order_by_query_list) + stmt = stmt.limit(limit).offset(offset) + stmt = self.get_join_by_excpression(stmt, join_mode=join_mode) + return stmt + + def get_one(self, *, + extra_args: dict, + filter_args: dict, + ) -> BinaryExpression: + filter_list: List[BinaryExpression] = find_query_builder(param=filter_args, + model=self.model_columns) + + extra_query_expression: List[BinaryExpression] = find_query_builder(param=extra_args, + model=self.model) + model = self.model + if not isinstance(self.model, Table): + model = model.__table__ + stmt = select(*[model]).where(and_(*filter_list + extra_query_expression)) + return stmt + + def create(self, *, + insert_arg, + create_one=True, + ) -> List[BinaryExpression]: + insert_arg_dict: Union[list, dict] = insert_arg + if not create_one: + insert_arg_list: list = insert_arg_dict.pop('insert', None) + insert_arg_dict = [] + for i in insert_arg_list: + insert_arg_dict.append(i.__dict__) + if not isinstance(insert_arg_dict, list): + insert_arg_dict = [insert_arg_dict] + + insert_arg_dict: list[dict] = [clean_input_fields(model=self.model_columns, param=insert_arg) + for insert_arg in insert_arg_dict] + if isinstance(insert_arg_dict, list): + new_data = [] + for i in insert_arg_dict: + new_data.append(self.model(**i)) + return new_data + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError + + def insert_one(self, *, + insert_args) -> BinaryExpression: + insert_args = insert_args + update_columns = clean_input_fields(insert_args, + self.model_columns) + inserted_instance = self.model(**update_columns) + return inserted_instance + + def get_join_select_fields(self, join_mode=None): + join_table_instance_list = [] + if not join_mode: + return join_table_instance_list + for _, table_instance in join_mode.items(): + for local_reference in table_instance['local_reference_pairs_set']: + if 'exclude' in local_reference and local_reference['exclude']: + continue + for column in local_reference['reference_table_columns']: + foreign_table_name = local_reference['reference']['reference_table'] + join_table_instance_list.append( + column.label(foreign_table_name + '_foreign_____' + str(column).split('.')[1])) + return join_table_instance_list + + def get_join_by_excpression(self, stmt: BinaryExpression, join_mode=None) -> BinaryExpression: + if not join_mode: + return stmt + for join_table, data in join_mode.items(): + for local_reference in data['local_reference_pairs_set']: + local = local_reference['local']['local_column'] + reference = local_reference['reference']['reference_column'] + local_column = getattr(local_reference['local_table_columns'], local) + reference_column = getattr(local_reference['reference_table_columns'], reference) + table = local_reference['reference_table'] + stmt = stmt.join(table, local_column == reference_column) + return stmt + + # def delete(self, + # *, + # delete_args: dict, + # session, + # primary_key: dict = None, + # ) -> BinaryExpression: + # filter_list: List[BinaryExpression] = find_query_builder(param=delete_args, + # model=self.model_columns) + # if primary_key: + # filter_list += find_query_builder(param=primary_key, + # model=self.model_columns) + # + # delete_instance = session.query(self.model).where(and_(*filter_list)) + # return delete_instance + + def model_query(self, + *, + session, + extra_args: dict = None, + filter_args: dict = None, + ) -> BinaryExpression: + + ''' + used for delette and update + ''' + + filter_list: List[BinaryExpression] = find_query_builder(param=filter_args, + model=self.model_columns) + if extra_args: + filter_list += find_query_builder(param=extra_args, + model=self.model_columns) + stmt = select(self.model).where(and_(*filter_list)) + return stmt + + def get_one_with_foreign_pk(self, *, + join_mode, + query, + target_model, + abstract_param=None + ) -> BinaryExpression: + model = self.foreign_table_mapping[target_model] + filter_list: List[BinaryExpression] = find_query_builder(param=query, + model=model) + path_filter_list: List[BinaryExpression] = path_query_builder(params=abstract_param, + model=self.foreign_table_mapping) + join_table_instance_list: list = self.get_join_select_fields(join_mode) + + if not isinstance(self.model, Table): + model = model.__table__ + + stmt = select(*[model] + join_table_instance_list).filter(and_(*filter_list + path_filter_list)) + + stmt = self.get_join_by_excpression(stmt, join_mode=join_mode) + return stmt + + + # def update(self, *, + # update_args, + # extra_query, + # session, + # primary_key=None, + # ) -> BinaryExpression: + # + # + # filter_list: List[BinaryExpression] = find_query_builder(param=extra_query, + # model=self.model_columns) + # if primary_key: + # primary_key = primary_key + # filter_list += find_query_builder(param=primary_key, model=self.model_columns) + # update_stmt = update(self.model).where(and_(*filter_list)).values(update_args) + # update_stmt = update_stmt.execution_options(synchronize_session=False) + # return update_stmt + + +class SQLAlchemyPGSQLQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + + """ + :param model: declarative_base model + :param async_mode: bool + """ + super(SQLAlchemyPGSQLQueryService, + self).__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + insert_arg_dict: Union[list, dict] = insert_arg + + insert_with_conflict_handle = insert_arg_dict.pop('on_conflict', None) + if not upsert_one: + insert_arg_list: list = insert_arg_dict.pop('insert', None) + insert_arg_dict = [] + for i in insert_arg_list: + insert_arg_dict.append(i.__dict__) + + if not isinstance(insert_arg_dict, list): + insert_arg_dict: list[dict] = [insert_arg_dict] + insert_arg_dict: list[dict] = [clean_input_fields(model=self.model_columns, param=insert_arg) + for insert_arg in insert_arg_dict] + insert_stmt = insert(self.model).values(insert_arg_dict) + + if unique_fields and insert_with_conflict_handle: + update_columns = clean_input_fields(insert_with_conflict_handle.__dict__.get('update_columns', None), + self.model_columns) + if not update_columns: + raise UpdateColumnEmptyException('update_columns parameter must be a non-empty list ') + conflict_update_dict = {} + for columns in update_columns: + conflict_update_dict[columns] = getattr(insert_stmt.excluded, columns) + + conflict_list = clean_input_fields(model=self.model_columns, param=unique_fields) + conflict_update_dict = clean_input_fields(model=self.model_columns, param=conflict_update_dict) + insert_stmt = insert_stmt.on_conflict_do_update(index_elements=conflict_list, + set_=conflict_update_dict + ) + insert_stmt = insert_stmt.returning(text('*')) + return insert_stmt + + +class SQLAlchemySQLITEQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + """ + :param model: declarative_base model + :param async_mode: bool + """ + super().__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError + + +class SQLAlchemyMySQLQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + """ + :param model: declarative_base model + :param async_mode: bool + """ + super().__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError + + +class SQLAlchemyMariaDBQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + """ + :param model: declarative_base model + :param async_mode: bool + """ + super().__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError + + +class SQLAlchemyOracleQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + """ + :param model: declarative_base model + :param async_mode: bool + """ + super().__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError + + +class SQLAlchemyMSSqlQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + """ + :param model: declarative_base model + :param async_mode: bool + """ + super().__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError + + +class SQLAlchemyNotSupportQueryService(SQLAlchemyGeneralSQLQueryService): + + def __init__(self, *, model, async_mode, foreign_table_mapping): + """ + :param model: declarative_base model + :param async_mode: bool + """ + super().__init__(model=model, + async_mode=async_mode, + foreign_table_mapping=foreign_table_mapping) + self.model = model + self.model_columns = model + self.async_mode = async_mode + + def upsert(self, *, + insert_arg, + unique_fields: List[str], + upsert_one=True, + ) -> BinaryExpression: + raise NotImplementedError diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_route.py b/src/fastapi_quickcrud_codegen/misc/abstract_route.py new file mode 100644 index 0000000..0f78d3a --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/abstract_route.py @@ -0,0 +1,1281 @@ +from abc import abstractmethod, ABC +from http import HTTPStatus +from typing import Union + +from fastapi import \ + Depends, \ + Response +from sqlalchemy.exc import IntegrityError +from starlette.requests import Request + + +class SQLAlchemyGeneralSQLBaseRouteSource(ABC): + """ This route will support the SQL SQLAlchemy dialects. """ + + @classmethod + def find_one(cls, api, + *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + dependencies, + request_url_param_model, + request_query_model, + db_session): + + if not async_mode: + @api.get(path, status_code=200, response_model=response_model, dependencies=dependencies) + def get_one_by_primary_key(response: Response, + request: Request, + url_param=Depends(request_url_param_model), + query=Depends(request_query_model), + session=Depends(db_session)): + + join = query.__dict__.pop('join_foreign_table', None) + stmt = query_service.get_one(filter_args=query.__dict__, + extra_args=url_param.__dict__, + join_mode=join) + query_result = execute_service.execute(session, stmt) + response_result = parsing_service.find_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session, + join_mode=join) + return response_result + else: + @api.get(path, status_code=200, response_model=response_model, dependencies=dependencies) + async def async_get_one_by_primary_key(response: Response, + request: Request, + url_param=Depends(request_url_param_model), + query=Depends(request_query_model), + session=Depends(db_session)): + + join = query.__dict__.pop('join_foreign_table', None) + stmt = query_service.get_one(filter_args=query.__dict__, + extra_args=url_param.__dict__, + join_mode=join) + query_result = await execute_service.async_execute(session, stmt) + + response_result = await parsing_service.async_find_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session, + join_mode=join) + return response_result + + @classmethod + def find_many(cls, api, *, + query_service, + parsing_service, + execute_service, + async_mode, + path, + response_model, + dependencies, + request_query_model, + db_session): + + if async_mode: + @api.get(path, dependencies=dependencies, response_model=response_model) + async def async_get_many(response: Response, + request: Request, + query=Depends(request_query_model), + session=Depends( + db_session) + ): + join = query.__dict__.pop('join_foreign_table', None) + stmt = query_service.get_many(query=query.__dict__, join_mode=join) + + query_result = await execute_service.async_execute(session, stmt) + + parsed_response = await parsing_service.async_find_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + join_mode=join, + session=session) + return parsed_response + else: + @api.get(path, dependencies=dependencies, response_model=response_model) + def get_many(response: Response, + request: Request, + query=Depends(request_query_model), + session=Depends( + db_session) + ): + join = query.__dict__.pop('join_foreign_table', None) + + stmt = query_service.get_many(query=query.__dict__, join_mode=join) + query_result = execute_service.execute(session, stmt) + parsed_response = parsing_service.find_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + join_mode=join, + session=session) + return parsed_response + + @abstractmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + raise NotImplementedError + + @abstractmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + + raise NotImplementedError + + @classmethod + def create_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + if async_mode: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + async def async_insert_one( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + # stmt = query_service.create(insert_arg=query) + + new_inserted_data = query_service.create(insert_arg=query.__dict__) + + execute_service.add_all(session, new_inserted_data) + try: + await execute_service.async_flush(session) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return await parsing_service.async_create_one(response_model=response_model, + sql_execute_result=new_inserted_data, + fastapi_response=response, + session=session) + else: + + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + def insert_one( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + + new_inserted_data = query_service.create(insert_arg=query.__dict__) + + execute_service.add_all(session, new_inserted_data) + + try: + execute_service.flush(session) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return parsing_service.create_one(response_model=response_model, + sql_execute_result=new_inserted_data, + fastapi_response=response, + session=session) + + @classmethod + def create_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + + if async_mode: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + async def async_insert_many( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + inserted_data = query_service.create(insert_arg=query.__dict__, + create_one=False) + + execute_service.add_all(session, inserted_data) + + try: + await execute_service.async_flush(session) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return await parsing_service.async_create_many(response_model=response_model, + sql_execute_result=inserted_data, + fastapi_response=response, + session=session) + else: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + def insert_many( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + + # inserted_data = query.__dict__['insert'] + update_list = query.__dict__ + inserted_data = query_service.create(insert_arg=update_list, + create_one=False) + + execute_service.add_all(session, inserted_data) + + try: + execute_service.flush(session) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return parsing_service.create_many(response_model=response_model, + sql_execute_result=inserted_data, + fastapi_response=response, + session=session) + + @classmethod + def delete_one(cls, api, *, + query_service, + parsing_service, + execute_service, + async_mode, + path, + response_model, + dependencies, + request_query_model, + request_url_model, + db_session, ): + + if async_mode: + @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) + async def async_delete_one_by_primary_key(response: Response, + request: Request, + query=Depends(request_query_model), + request_url_param_model=Depends(request_url_model), + session=Depends(db_session)): + # delete_instance = query_service.model_query( + # filter_args=request_url_param_model.__dict__, + # extra_args=query.__dict__, + # session=session) + filter_stmt = query_service.model_query(filter_args=request_url_param_model.__dict__, + extra_args=query.__dict__, + session=session) + + tmp = await session.execute(filter_stmt) + delete_instance = tmp.scalar() + + return await parsing_service.async_delete_one(response_model=response_model, + sql_execute_result=delete_instance, + fastapi_response=response, + session=session) + + else: + @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) + def delete_one_by_primary_key(response: Response, + request: Request, + query=Depends(request_query_model), + request_url_param_model=Depends(request_url_model), + session=Depends(db_session)): + filter_stmt = query_service.model_query(filter_args=request_url_param_model.__dict__, + extra_args=query.__dict__, + session=session) + delete_instance = session.execute(filter_stmt).scalar() + + return parsing_service.delete_one(response_model=response_model, + sql_execute_result=delete_instance, + fastapi_response=response, + session=session) + + @classmethod + def delete_many(cls, api, *, + query_service, + parsing_service, + execute_service, + async_mode, + path, + response_model, + dependencies, + request_query_model, + db_session): + if async_mode: + @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) + async def async_delete_many_by_query(response: Response, + request: Request, + query=Depends(request_query_model), + session=Depends(db_session)): + filter_stmt = query_service.model_query(filter_args=query.__dict__, + session=session) + + tmp = await session.execute(filter_stmt) + data_instance = [i for i in tmp.scalars()] + return await parsing_service.async_delete_many(response_model=response_model, + sql_execute_results=data_instance, + fastapi_response=response, + session=session) + else: + + @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) + def delete_many_by_query(response: Response, + request: Request, + query=Depends(request_query_model), + session=Depends(db_session)): + filter_stmt = query_service.model_query(filter_args=query.__dict__, + session=session) + + delete_instance = [i for i in session.execute(filter_stmt).scalars()] + + return parsing_service.delete_many(response_model=response_model, + sql_execute_results=delete_instance, + fastapi_response=response, + session=session) + + @classmethod + def post_redirect_get(cls, api, *, + dependencies, + request_body_model, + db_session, + crud_service, + result_parser, + execute_service, + async_mode, + response_model): + if async_mode: + @api.post("", status_code=303, response_class=Response, dependencies=dependencies) + async def async_create_one_and_redirect_to_get_one_api_with_primary_key( + request: Request, + insert_args: request_body_model = Depends(), + session=Depends(db_session), + ): + new_inserted_data = crud_service.insert_one(insert_args=insert_args.__dict__) + + execute_service.add(session, new_inserted_data) + try: + await execute_service.async_flush(session) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return await result_parser.async_post_redirect_get(response_model=response_model, + sql_execute_result=new_inserted_data, + fastapi_request=request, + session=session) + else: + @api.post("", status_code=303, response_class=Response, dependencies=dependencies) + def create_one_and_redirect_to_get_one_api_with_primary_key( + request: Request, + insert_args: request_body_model = Depends(), + session=Depends(db_session), + ): + + new_inserted_data = crud_service.insert_one(insert_args=insert_args.__dict__) + + execute_service.add(session, new_inserted_data) + try: + execute_service.flush(session) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + + return result_parser.post_redirect_get(response_model=response_model, + sql_execute_result=new_inserted_data, + fastapi_request=request, + session=session) + + @classmethod + def patch_one(cls, api, *, + path, + response_model, + dependencies, + request_url_param_model, + request_body_model, + request_query_model, + execute_service, + db_session, + crud_service, + result_parser, + async_mode): + if async_mode: + + @api.patch(path, + status_code=200, + response_model=Union[response_model], + dependencies=dependencies) + async def async_partial_update_one_by_primary_key( + response: Response, + primary_key: request_url_param_model = Depends(), + patch_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session), + ): + filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, + extra_args=extra_query.__dict__, + session=session) + + data_instance = await session.execute(filter_stmt) + data_instance = data_instance.scalar() + + try: + return await result_parser.async_update(response_model=response_model, + sql_execute_result=data_instance, + update_args=patch_data.__dict__, + fastapi_response=response, + session=session, + update_one=True) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + else: + @api.patch(path, + status_code=200, + response_model=Union[response_model], + dependencies=dependencies) + def partial_update_one_by_primary_key( + response: Response, + primary_key: request_url_param_model = Depends(), + patch_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session), + ): + filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, + extra_args=extra_query.__dict__, + session=session) + + update_instance = session.execute(filter_stmt).scalar() + + try: + return result_parser.update(response_model=response_model, + sql_execute_result=update_instance, + update_args=patch_data.__dict__, + fastapi_response=response, + session=session, + update_one=True) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + + @classmethod + def patch_many(cls, api, *, + path, + response_model, + dependencies, + request_body_model, + request_query_model, + db_session, + crud_service, + result_parser, + execute_service, + async_mode): + if async_mode: + @api.patch(path, + status_code=200, + response_model=response_model, + dependencies=dependencies) + async def async_partial_update_many_by_query( + response: Response, + patch_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session) + ): + + filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, + session=session) + + tmp = await session.execute(filter_stmt) + data_instance = [i for i in tmp.scalars()] + + if not data_instance: + return Response(status_code=HTTPStatus.NO_CONTENT) + try: + return await result_parser.async_update(response_model=response_model, + sql_execute_result=data_instance, + fastapi_response=response, + update_args=patch_data.__dict__, + session=session, + update_one=False) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + else: + @api.patch(path, + status_code=200, + response_model=response_model, + dependencies=dependencies) + def partial_update_many_by_query( + response: Response, + patch_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session) + ): + filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, + session=session) + + data_instance = [i for i in session.execute(filter_stmt).scalars()] + + if not data_instance: + return Response(status_code=HTTPStatus.NO_CONTENT) + try: + return result_parser.update(response_model=response_model, + sql_execute_result=data_instance, + fastapi_response=response, + update_args=patch_data.__dict__, + session=session, + update_one=False) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + + @classmethod + def put_one(cls, api, *, + path, + request_url_param_model, + request_body_model, + response_model, + dependencies, + request_query_model, + db_session, + crud_service, + result_parser, + execute_service, + async_mode): + if async_mode: + @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) + async def async_entire_update_by_primary_key( + response: Response, + primary_key: request_url_param_model = Depends(), + update_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session), + ): + filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, + extra_args=extra_query.__dict__, + session=session) + + data_instance = await session.execute(filter_stmt) + data_instance = data_instance.scalar() + + if not data_instance: + return Response(status_code=HTTPStatus.NOT_FOUND) + try: + return await result_parser.async_update(response_model=response_model, + sql_execute_result=data_instance, + fastapi_response=response, + update_args=update_data.__dict__, + session=session, + update_one=True) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + else: + @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) + def entire_update_by_primary_key( + response: Response, + primary_key: request_url_param_model = Depends(), + update_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session), + ): + filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, + extra_args=extra_query.__dict__, + session=session) + + data_instance = session.execute(filter_stmt).scalar() + + if not data_instance: + return Response(status_code=HTTPStatus.NOT_FOUND) + try: + return result_parser.update(response_model=response_model, + sql_execute_result=data_instance, + fastapi_response=response, + update_args=update_data.__dict__, + session=session, + update_one=True) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + + @classmethod + def put_many(cls, api, *, + path, + response_model, + dependencies, + request_query_model, + request_body_model, + db_session, + crud_service, + result_parser, + execute_service, + async_mode): + if async_mode: + @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) + async def async_entire_update_many_by_query( + response: Response, + update_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session), + ): + filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, + session=session) + tmp = await session.execute(filter_stmt) + data_instance = [i for i in tmp.scalars()] + + if not data_instance: + return Response(status_code=HTTPStatus.NO_CONTENT) + try: + return await result_parser.async_update(response_model=response_model, + sql_execute_result=data_instance, + fastapi_response=response, + update_args=update_data.__dict__, + session=session, + update_one=False) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + + else: + @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) + def entire_update_many_by_query( + response: Response, + update_data: request_body_model = Depends(), + extra_query: request_query_model = Depends(), + session=Depends(db_session), + ): + + filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, + session=session) + + data_instance = [i for i in session.execute(filter_stmt).scalars()] + + if not data_instance: + return Response(status_code=HTTPStatus.NO_CONTENT) + try: + return result_parser.update(response_model=response_model, + sql_execute_result=data_instance, + fastapi_response=response, + update_args=update_data.__dict__, + session=session, + update_one=False) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + + # return result_parser.update_many(response_model=response_model, + # sql_execute_result=query_result, + # fastapi_response=response, + # session=session) + + @classmethod + def find_one_foreign_tree(cls, api, *, + query_service, + parsing_service, + execute_service, + async_mode, + path, + response_model, + dependencies, + request_query_model, + request_url_param_model, + function_name, + db_session): + + if async_mode: + @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) + async def async_get_one_with_foreign_tree(response: Response, + request: Request, + url_param=Depends(request_url_param_model), + query=Depends(request_query_model), + session=Depends( + db_session) + ): + target_model = request.url.path.split("/")[-2] + join = query.__dict__.pop('join_foreign_table', None) + stmt = query_service.get_one_with_foreign_pk(query=query.__dict__, + join_mode=join, + abstract_param=url_param.__dict__, + target_model=target_model) + + query_result = await execute_service.async_execute(session, stmt) + + parsed_response = await parsing_service.async_find_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + join_mode=join, + session=session) + return parsed_response + else: + @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) + def get_one_with_foreign_tree(response: Response, + request: Request, + url_param=Depends(request_url_param_model), + query=Depends(request_query_model), + session=Depends( + db_session) + ): + target_model = request.url.path.split("/")[-2] + join = query.__dict__.pop('join_foreign_table', None) + + stmt = query_service.get_one_with_foreign_pk(query=query.__dict__, + join_mode=join, + abstract_param=url_param.__dict__, + target_model=target_model) + query_result = execute_service.execute(session, stmt) + parsed_response = parsing_service.find_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + join_mode=join, + session=session) + return parsed_response + + @classmethod + def find_many_foreign_tree(cls, api, *, + query_service, + parsing_service, + execute_service, + async_mode, + path, + response_model, + dependencies, + request_query_model, + request_url_param_model, + function_name, + db_session): + + if async_mode: + @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) + async def async_get_many_with_foreign_tree(response: Response, + request: Request, + url_param=Depends(request_url_param_model), + query=Depends(request_query_model), + session=Depends( + db_session) + ): + target_model = request.url.path.split("/")[-1] + join = query.__dict__.pop('join_foreign_table', None) + stmt = query_service.get_many(query=query.__dict__, join_mode=join, abstract_param=url_param.__dict__, + target_model=target_model) + + query_result = await execute_service.async_execute(session, stmt) + + parsed_response = await parsing_service.async_find_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + join_mode=join, + session=session) + return parsed_response + else: + @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) + def get_many_with_foreign_tree(response: Response, + request: Request, + url_param=Depends(request_url_param_model), + query=Depends(request_query_model), + session=Depends( + db_session) + ): + target_model = request.url.path.split("/")[-1] + join = query.__dict__.pop('join_foreign_table', None) + stmt = query_service.get_many(query=query.__dict__, join_mode=join, abstract_param=url_param.__dict__, + target_model=target_model) + query_result = execute_service.execute(session, stmt) + parsed_response = parsing_service.find_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + join_mode=join, + session=session) + + return parsed_response + + +class SQLAlchemyPGSQLRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + if async_mode: + + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + async def async_insert_one_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list) + + try: + query_result = await execute_service.async_execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) + return result + return await parsing_service.async_upsert_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + else: + + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + def insert_one_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list) + try: + query_result = execute_service.execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) + return result + return parsing_service.upsert_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + + if async_mode: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + async def async_insert_many_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list, + upsert_one=False) + try: + query_result = await execute_service.async_execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) + return result + return await parsing_service.async_upsert_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + else: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + def insert_many_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list, + upsert_one=False) + try: + query_result = execute_service.execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) + return result + return parsing_service.upsert_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + + +class SQLAlchemySQLLiteRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + if async_mode: + + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + async def async_insert_one_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list) + + try: + query_result = await execute_service.async_execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return await parsing_service.async_upsert_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + else: + + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + def insert_one_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list) + try: + query_result = execute_service.execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return parsing_service.upsert_one(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + + if async_mode: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + async def async_insert_many_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list, + upsert_one=False) + try: + query_result = await execute_service.async_execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return await parsing_service.async_upsert_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + else: + @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) + def insert_many_and_support_upsert( + response: Response, + request: Request, + query: request_body_model = Depends(request_body_model), + session=Depends(db_session) + ): + + stmt = query_service.upsert(insert_arg=query.__dict__, + unique_fields=unique_list, + upsert_one=False) + try: + query_result = execute_service.execute(session, stmt) + except IntegrityError as e: + err_msg, = e.orig.args + if 'unique constraint' not in err_msg.lower(): + raise e + result = Response(status_code=HTTPStatus.CONFLICT) + return result + return parsing_service.upsert_many(response_model=response_model, + sql_execute_result=query_result, + fastapi_response=response, + session=session) + + +class SQLAlchemyMySQLRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + raise NotImplementedError + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + raise NotImplementedError + + +class SQLAlchemyMariadbRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + raise NotImplementedError + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + raise NotImplementedError + + +class SQLAlchemyOracleRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + raise NotImplementedError + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + raise NotImplementedError + + +class SQLAlchemyMSSQLRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + raise NotImplementedError + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + raise NotImplementedError + + +class SQLAlchemyNotSupportRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): + ''' + This route will support the SQL SQLAlchemy dialects + ''' + + @classmethod + def upsert_one(cls, api, *, + path, + query_service, + parsing_service, + execute_service, + async_mode, + response_model, + request_body_model, + dependencies, + db_session, + unique_list): + raise NotImplementedError + + @classmethod + def upsert_many(cls, api, *, + query_service, + parsing_service, + async_mode, + path, + response_model, + dependencies, + request_body_model, + db_session, + unique_list, + execute_service): + raise NotImplementedError diff --git a/src/fastapi_quickcrud_codegen/misc/constant.py b/src/fastapi_quickcrud_codegen/misc/constant.py new file mode 100644 index 0000000..d243385 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/constant.py @@ -0,0 +1,4 @@ +GENERATION_FOLDER = "fastapi_quick_crud_template" +MODEL = "model" +ROUTE = "route" +COMMON = "common" diff --git a/src/fastapi_quickcrud_codegen/misc/covert_model.py b/src/fastapi_quickcrud_codegen/misc/covert_model.py new file mode 100644 index 0000000..f9a15b3 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/covert_model.py @@ -0,0 +1,24 @@ +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.sql.schema import Table + + +def convert_table_to_model(db_model): + NO_PRIMARY_KEY = False + if not isinstance(db_model, Table): + return db_model, NO_PRIMARY_KEY + db_name = str(db_model.fullname) + table_dict = {'__table__': db_model, + '__tablename__': db_name} + + if not db_model.primary_key: + table_dict['__mapper_args__'] = { + "primary_key": [i for i in db_model._columns] + } + NO_PRIMARY_KEY = True + + for i in db_model.c: + col, = i.expression.base_columns + table_dict[str(i.key)] = col + + return type(f'{db_name}DeclarativeBaseClass', (declarative_base(),), table_dict), NO_PRIMARY_KEY diff --git a/src/fastapi_quickcrud_codegen/misc/crud_model.py b/src/fastapi_quickcrud_codegen/misc/crud_model.py new file mode 100644 index 0000000..8019287 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/crud_model.py @@ -0,0 +1,46 @@ +from typing import (Optional, + Dict, + List) + +from pydantic import BaseModel +from pydantic.main import ModelMetaclass + +from .exceptions import (RequestMissing, + InvalidRequestMethod) +from .type import CrudMethods + + +class RequestResponseModel(BaseModel): + requestUrlParamModel: Optional[str] + requestRelationshipUrlParamField: Optional[List[str]] + requestQueryModel: Optional[str] + requestBodyModel: Optional[str] + responseModel: Optional[str] + jsonRequestFieldModel: Optional[str] + jsonbRequestFieldModel: Optional[str] + arrayRequestFieldModel: Optional[str] + foreignListModel: Optional[List[dict]] + + +class CRUDModel(BaseModel): + GET: Optional[Dict[CrudMethods, bool]] + POST: Optional[Dict[CrudMethods, bool]] + PUT: Optional[Dict[CrudMethods, bool]] + PATCH: Optional[Dict[CrudMethods, bool]] + DELETE: Optional[Dict[CrudMethods, bool]] + PRIMARY_KEY_NAME: Optional[str] + UNIQUE_LIST: Optional[List[str]] + + def get_available_request_method(self): + return [i for i in self.dict(exclude_unset=True, ).keys() if i in ["GET", "POST", "PUT", "PATCH", "DELETE"]] + + def get_model_by_request_method(self, request_method): + available_methods = self.dict() + if request_method not in available_methods.keys(): + raise InvalidRequestMethod(f'{request_method} is not an available request method') + if not available_methods[request_method]: + raise RequestMissing( + f'{request_method} is not available, ' + f'make sure the CRUDModel contains this request method') + _ = available_methods[request_method] + return _ diff --git a/src/fastapi_quickcrud_codegen/misc/exceptions.py b/src/fastapi_quickcrud_codegen/misc/exceptions.py new file mode 100644 index 0000000..a6b87a9 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/exceptions.py @@ -0,0 +1,85 @@ +from fastapi import HTTPException + + +class FindOneApiNotRegister(HTTPException): + pass + + +class CRUDBuilderException(BaseException): + pass + + +class RequestMissing(CRUDBuilderException): + pass + + +class PrimaryMissing(CRUDBuilderException): + pass + + +class UnknownOrderType(CRUDBuilderException): + pass + + +class UpdateColumnEmptyException(CRUDBuilderException): + pass + + +class UnknownColumn(CRUDBuilderException): + pass + + +class QueryOperatorNotFound(CRUDBuilderException): + pass + + +class UnknownError(CRUDBuilderException): + pass + + +class ConflictColumnsCannotHit(CRUDBuilderException): + pass + + +class MultipleSingleUniqueNotSupportedException(CRUDBuilderException): + pass + + +class SchemaException(CRUDBuilderException): + pass + + +class CompositePrimaryKeyConstraintNotSupportedException(CRUDBuilderException): + pass + + +class MultiplePrimaryKeyNotSupportedException(CRUDBuilderException): + pass + + +class ColumnTypeNotSupportedException(CRUDBuilderException): + pass + + +class InvalidRequestMethod(CRUDBuilderException): + pass + + +# +# class NotFoundError(MongoQueryError): +# def __init__(self, Collection: Type[ModelType], model: BaseModel): +# detail = "does not exist" +# super().__init__(Collection, model, detail) +# +# +# +# class DuplicatedError(MongoQueryError): +# def __init__(self, Collection: Type[ModelType], model: BaseModel): +# detail = "was already existed" +# super().__init__(Collection, model, detail) + +class FDDRestHTTPException(HTTPException): + """Baseclass for all HTTP exceptions in FDD Rest API. This exception can be called as WSGI + application to render a default error page or you can catch the subclasses + of it independently and render nicer error messages. + """ diff --git a/src/fastapi_quickcrud_codegen/misc/get_table_name.py b/src/fastapi_quickcrud_codegen/misc/get_table_name.py new file mode 100644 index 0000000..f404ead --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/get_table_name.py @@ -0,0 +1,16 @@ +from sqlalchemy import Table + + +def get_table_name_from_table(table): + return table.name + + +def get_table_name_from_model(table): + return table.__tablename__ + + +def get_table_name(table): + if isinstance(table, Table): + return get_table_name_from_table(table) + else: + return get_table_name_from_model(table) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/misc/memory_sql.py b/src/fastapi_quickcrud_codegen/misc/memory_sql.py new file mode 100644 index 0000000..6438b24 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/memory_sql.py @@ -0,0 +1,70 @@ +import asyncio +import string +import random +from typing import Generator + +from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.pool import StaticPool + + +class MemorySql(): + def __init__(self, async_mode: bool = False): + """ + + @type async_mode: bool + used to build sync or async memory sql connection + """ + self.async_mode = async_mode + SQLALCHEMY_DATABASE_URL = f"sqlite{'+aiosqlite' if async_mode else ''}://" + if not async_mode: + self.engine = create_engine(SQLALCHEMY_DATABASE_URL, + future=True, + echo=True, + pool_pre_ping=True, + pool_recycle=7200, + connect_args={"check_same_thread": False}, + poolclass=StaticPool) + self.sync_session = sessionmaker(bind=self.engine, + autocommit=False, ) + else: + self.engine = create_async_engine(SQLALCHEMY_DATABASE_URL, + future=True, + echo=True, + pool_pre_ping=True, + pool_recycle=7200, + connect_args={"check_same_thread": False}, + poolclass=StaticPool) + self.sync_session = sessionmaker(autocommit=False, + autoflush=False, + bind=self.engine, + class_=AsyncSession) + + def create_memory_table(self, Mode: 'declarative_base()'): + if not self.async_mode: + Mode.__table__.create(self.engine, checkfirst=True) + else: + async def create_table(engine, model): + async with engine.begin() as conn: + await conn.run_sync(model._sa_registry.metadata.create_all) + + loop = asyncio.get_event_loop() + loop.run_until_complete(create_table(self.engine, Mode)) + + def get_memory_db_session(self) -> Generator: + try: + db = self.sync_session() + yield db + except Exception as e: + db.rollback() + raise e + finally: + db.close() + + async def async_get_memory_db_session(self): + async with self.sync_session() as session: + yield session + +async_memory_db = MemorySql(True) +sync_memory_db = MemorySql() \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/misc/schema_builder.py b/src/fastapi_quickcrud_codegen/misc/schema_builder.py new file mode 100644 index 0000000..eda959c --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/schema_builder.py @@ -0,0 +1,1124 @@ +import uuid +import warnings +from copy import deepcopy +from dataclasses import (make_dataclass) +from enum import auto +from typing import (Optional, + Any) +from typing import (Type, + Dict, + List, + Tuple, + TypeVar, + NewType, + Union) + +import pydantic +from fastapi import (Body, + Query) +from pydantic import (BaseModel, + create_model, + BaseConfig) +from pydantic.dataclasses import dataclass as pydantic_dataclass +from sqlalchemy import UniqueConstraint, Table, Column +from sqlalchemy import inspect +from sqlalchemy.orm import DeclarativeMeta +from sqlalchemy.orm import declarative_base +from strenum import StrEnum + +from fastapi_quickcrud_codegen.generator.model_template_generator import model_template_gen +from fastapi_quickcrud_codegen.misc.covert_model import convert_table_to_model +from fastapi_quickcrud_codegen.misc.exceptions import (SchemaException, + ColumnTypeNotSupportedException) +from fastapi_quickcrud_codegen.misc.get_table_name import get_table_name +from fastapi_quickcrud_codegen.misc.type import (Ordering, + ExtraFieldTypePrefix, + ExtraFieldType, + SqlType, ) +from fastapi_quickcrud_codegen.model.model_builder import ModelCodeGen + +FOREIGN_PATH_PARAM_KEYWORD = "__pk__" +BaseModelT = TypeVar('BaseModelT', bound=BaseModel) +DataClassT = TypeVar('DataClassT', bound=Any) +DeclarativeClassT = NewType('DeclarativeClassT', declarative_base) +TableNameT = NewType('TableNameT', str) +ResponseModelT = NewType('ResponseModelT', BaseModel) +ForeignKeyName = NewType('ForeignKeyName', str) +TableInstance = NewType('TableInstance', Table) + + +class ExcludeUnsetBaseModel(BaseModel): + def dict(self, *args, **kwargs): + if kwargs and kwargs.get("exclude_none") is not None: + kwargs["exclude_unset"] = True + return BaseModel.dict(self, *args, **kwargs) + + +class OrmConfig(BaseConfig): + orm_mode = True + + +def _add_orm_model_config_into_pydantic_model(pydantic_model, **kwargs) -> BaseModelT: + validators = kwargs.get('validators', None) + config = kwargs.get('config', None) + field_definitions = { + name_: (field_.outer_type_, field_.field_info.default) + for name_, field_ in pydantic_model.__fields__.items() + } + return create_model(f'{pydantic_model.__name__}WithValidators', + **field_definitions, + __config__=config, + __validators__=validators) + +def _model_from_dataclass(kls: DataClassT) -> Type[BaseModel]: + """ Converts a stdlib dataclass to a pydantic BaseModel. """ + + return pydantic_dataclass(kls).__pydantic_model__ + + +def _to_require_but_default(model: Type[BaseModelT]) -> Type[BaseModelT]: + """ + Create a new BaseModel with the exact same fields as `model` + but making them all require but there are default value + """ + config = model.Config + field_definitions = {} + for name_, field_ in model.__fields__.items(): + field_definitions[name_] = (field_.outer_type_, field_.field_info.default) + return create_model(f'{model.__name__}RequireButDefault', **field_definitions, + __config__=config) # type: ignore[arg-type] + + +def _filter_none(request_or_response_object): + received_request = deepcopy(request_or_response_object.__dict__) + if 'insert' in received_request: + insert_item_without_null = [] + for received_insert in received_request['insert']: + received_insert_ = deepcopy(received_insert) + for received_insert_item, received_insert_value in received_insert_.__dict__.items(): + if hasattr(received_insert_value, '__module__'): + if received_insert_value.__module__ == 'fastapi.params' or received_insert_value is None: + delattr(received_insert, received_insert_item) + elif received_insert_value is None: + delattr(received_insert, received_insert_item) + + insert_item_without_null.append(received_insert) + setattr(request_or_response_object, 'insert', insert_item_without_null) + else: + for name, value in received_request.items(): + if hasattr(value, '__module__'): + if value.__module__ == 'fastapi.params' or value is None: + delattr(request_or_response_object, name) + elif value is None: + delattr(request_or_response_object, name) + + +class ApiParameterSchemaBuilder: + unsupported_data_types = ["BLOB"] + partial_supported_data_types = ["INTERVAL", "JSON", "JSONB"] + + def __init__(self, db_model: Type, sql_type, exclude_column=None, constraints=None, + exclude_primary_key=False + # ,foreign_include=False + ): + self.class_name = db_model.__name__ + self.root_table_name = get_table_name(db_model) + self.constraints = constraints + self.exclude_primary_key = exclude_primary_key + if exclude_column is None: + self._exclude_column = [] + else: + self._exclude_column = exclude_column + self.alias_mapper: Dict[str, str] = {} # Table not support alias + if self.exclude_primary_key: + self.__db_model: Table = db_model + self.__db_model_table: Table = db_model.__table__ + self.__columns = db_model.__table__.c + self.db_name: str = db_model.__tablename__ + else: + self.__db_model: DeclarativeClassT = db_model + self.__db_model_table: Table = db_model.__table__ + self.db_name: str = db_model.__tablename__ + self.__columns = db_model.__table__.c + model = self.__db_model + + self.code_gen = ModelCodeGen(self.root_table_name, sql_type) + self.code_gen.gen_model(db_model) + + self.primary_key_str, self._primary_key_dataclass_model, self._primary_key_field_definition \ + = self._extract_primary() + self.unique_fields: List[str] = self._extract_unique() + + self.code_gen.build_constant(constants= [("PRIMARY_KEY_NAME", self.primary_key_str), + ("UNIQUE_LIST", self.unique_fields)]) + self.uuid_type_columns = [] + self.str_type_columns = [] + self.number_type_columns = [] + self.datetime_type_columns = [] + self.timedelta_type_columns = [] + self.bool_type_columns = [] + self.json_type_columns = [] + self.array_type_columns = [] + self.foreign_table_response_model_sets: Dict[TableNameT, ResponseModelT] = {} + self.all_field: List[dict] = self._extract_all_field() + self.sql_type = sql_type + + def extra_foreign_table(self, db_model=None) -> Dict[ForeignKeyName, dict]: + if db_model is None: + db_model = self.__db_model + if self.exclude_primary_key: + return self._extra_foreign_table_from_table() + else: + return self._extra_foreign_table_from_declarative_base(db_model) + + def _extract_primary(self, db_model_table=None) -> Union[tuple, Tuple[Union[str, Any], + DataClassT, + Tuple[Union[str, Any], + Union[Type[uuid.UUID], Any], + Optional[Any]]]]: + if db_model_table == None: + db_model_table = self.__db_model_table + primary_list = db_model_table.primary_key.columns.values() + if not primary_list or self.exclude_primary_key: + return (None, None, None) + if len(primary_list) > 1: + raise SchemaException( + f'multiple primary key / or composite not supported; {self.db_name} ') + primary_key_column, = primary_list + column_type = str(primary_key_column.type) + try: + python_type = primary_key_column.type.python_type + if column_type in self.unsupported_data_types: + raise ColumnTypeNotSupportedException( + f'The type of column {primary_key_column.key} ({column_type}) not supported yet') + if column_type in self.partial_supported_data_types: + warnings.warn( + f'The type of column {primary_key_column.key} ({column_type}) ' + f'is not support data query (as a query parameters )') + + except NotImplementedError: + if column_type == "UUID": + python_type = uuid.UUID + else: + raise ColumnTypeNotSupportedException( + f'The type of column {primary_key_column.key} ({column_type}) not supported yet') + # handle if python type is UUID + if python_type.__name__ in ['str', + 'int', + 'float', + 'Decimal', + 'UUID', + 'bool', + 'date', + 'time', + 'datetime']: + column_type = python_type.__name__ + else: + raise ColumnTypeNotSupportedException( + f'The type of column {primary_key_column.key} ({column_type}) not supported yet') + + default = self._extra_default_value(primary_key_column) + description = self._get_field_description(primary_key_column) + if default is ...: + warnings.warn( + f'The column of {primary_key_column.key} has not default value ' + f'and it is not nullable and in exclude_list' + f'it may throw error when you insert data ') + primary_column_name = str(primary_key_column.key) + primary_field_definitions = (primary_column_name, column_type, default) + class_name = f'{self.class_name}PrimaryKeyModel' + self.code_gen.build_dataclass(class_name=class_name, fields=[(primary_field_definitions[0], + primary_field_definitions[1], + f'Query({primary_field_definitions[2]})')]) + primary_columns_model: DataClassT = make_dataclass(f'{self.class_name + str(uuid.uuid4())}_PrimaryKeyModel', + [(primary_field_definitions[0], + primary_field_definitions[1], + Query(primary_field_definitions[2], + description=description))], + namespace={ + '__post_init__': lambda + self_object: self._value_of_list_to_str( + self_object, self.uuid_type_columns) + }) + + assert primary_column_name and primary_columns_model and primary_field_definitions + return primary_column_name, primary_columns_model, primary_field_definitions + + def _extract_unique(self) -> List[str]: + unique_constraint = None + if not self.constraints: + return [] + for constraint in self.constraints: + if isinstance(constraint, UniqueConstraint): + if unique_constraint: + raise SchemaException( + "Only support one unique constraint/ Use unique constraint and composite unique constraint " + "at same time is not supported / Use composite unique constraint if there are more than one unique constraint") + unique_constraint = constraint + if unique_constraint: + unique_column_name_list = [] + for constraint_column in unique_constraint.columns: + column_name = str(constraint_column.key) + unique_column_name = column_name + unique_column_name_list.append(unique_column_name) + return unique_column_name_list + else: + return [] + + @staticmethod + def _get_field_description(column: Column) -> str: + if not hasattr(column, 'comment'): + return "" + return column.comment + + def _extract_all_field(self, columns=None) -> List[dict]: + fields: List[dict] = [] + if not columns: + columns = self.__columns + elif isinstance(columns, DeclarativeMeta): + columns = columns.__table__.c + elif isinstance(columns, Table): + columns = columns.c + for column in columns: + column_name = str(column.key) + column_foreign = [i.target_fullname for i in column.foreign_keys] + default = self._extra_default_value(column) + if column_name in self._exclude_column: + continue + column_type = str(column.type) + description = self._get_field_description(column) + try: + python_type = column.type.python_type + if column_type in self.unsupported_data_types: + raise ColumnTypeNotSupportedException( + f'The type of column {column_name} ({column_type}) not supported yet') + if column_type in self.partial_supported_data_types: + warnings.warn( + f'The type of column {column_name} ({column_type}) ' + f'is not support data query (as a query parameters )') + except NotImplementedError: + if column_type == "UUID": + python_type = uuid.UUID + else: + raise ColumnTypeNotSupportedException( + f'The type of column {column_name} ({column_type}) not supported yet') + # string filter + if python_type.__name__ in ['str']: + self.str_type_columns.append(column_name) + # uuid filter + elif python_type.__name__ in ['UUID']: + self.uuid_type_columns.append(column.name) + python_type.__name__ = "uuid.UUID" + # number filter + elif python_type.__name__ in ['int', 'float', 'Decimal']: + self.number_type_columns.append(column_name) + # date filter + elif python_type.__name__ in ['date', 'time', 'datetime']: + self.datetime_type_columns.append(column_name) + # timedelta filter + elif python_type.__name__ in ['timedelta']: + self.timedelta_type_columns.append(column_name) + # bool filter + elif python_type.__name__ in ['bool']: + self.bool_type_columns.append(column_name) + # json filter + elif python_type.__name__ in ['dict']: + self.json_type_columns.append(column_name) + # array filter + elif python_type.__name__ in ['list']: + self.array_type_columns.append(column_name) + base_column_detail, = column.base_columns + if hasattr(base_column_detail.type, 'item_type'): + item_type = base_column_detail.type.item_type.python_type + fields.append({'column_name': column_name, + 'column_type': f"List[{item_type.__name__}]", + 'column_default': default, + 'column_description': description}) + continue + else: + raise ColumnTypeNotSupportedException( + f'The type of column {column_name} ({column_type}) not supported yet') + + if column_type == "JSONB": + fields.append({'column_name': column_name, + 'column_type': f'Union[{python_type.__name__}, list]', + 'column_default': default, + 'column_description': description, + 'column_foreign': column_foreign}) + else: + fields.append({'column_name': column_name, + 'column_type': python_type.__name__, + 'column_default': default, + 'column_description': description, + 'column_foreign': column_foreign}) + + return fields + + @staticmethod + def _value_of_list_to_str(request_or_response_object, columns): + received_request = deepcopy(request_or_response_object.__dict__) + if isinstance(columns, str): + columns = [columns] + if 'insert' in request_or_response_object.__dict__: + insert_str_list = [] + for insert_item in request_or_response_object.__dict__['insert']: + for column in columns: + for insert_item_column, _ in insert_item.__dict__.items(): + if column in insert_item_column: + value_ = insert_item.__dict__[insert_item_column] + if value_ is not None: + if isinstance(value_, list): + str_value_ = [str(i) for i in value_] + else: + str_value_ = str(value_) + setattr(insert_item, insert_item_column, str_value_) + insert_str_list.append(insert_item) + setattr(request_or_response_object, 'insert', insert_str_list) + else: + for column in columns: + for received_column_name, _ in received_request.items(): + if column in received_column_name: + value_ = received_request[received_column_name] + if value_ is not None: + if isinstance(value_, list): + str_value_ = [str(i) for i in value_] + else: + str_value_ = str(value_) + setattr(request_or_response_object, received_column_name, str_value_) + + @staticmethod + def _assign_join_table_instance(request_or_response_object, join_table_mapping): + received_request = deepcopy(request_or_response_object.__dict__) + join_table_replace = {} + if 'join_foreign_table' in received_request: + for join_table in received_request['join_foreign_table']: + if join_table in join_table_mapping: + join_table_replace[str(join_table)] = join_table_mapping[join_table] + setattr(request_or_response_object, 'join_foreign_table', join_table_replace) + + @staticmethod + def _get_many_string_matching_patterns_description_builder(): + return '''
Composite string field matching pattern
+
Allow to select more than one pattern for string query +
https://www.postgresql.org/docs/9.3/functions-matching.html ''' + + @staticmethod + def _get_many_order_by_columns_description_builder(all_columns, regex_validation, primary_name): + return f'''
support column: +
{all_columns}

support ordering: +
{list(map(str, Ordering))} +
+
example: +
  {primary_name}:ASC +
  {primary_name}: DESC +
  {primary_name} : DESC +
  {primary_name} (default sort by ASC)''' + + @staticmethod + def _extra_default_value(column): + if not column.nullable: + if column.default is not None: + default = column.default.arg + elif column.server_default is not None: + default = "None" + elif column.primary_key and column.autoincrement == True: + default = "None" + else: + default = "..." + else: + if column.default is not None: + default = column.default.arg + else: + default = "None" + return default + + def _assign_str_matching_pattern(self, field_of_param: dict, result_: List[dict]) -> List[dict]: + if self.sql_type == SqlType.postgresql: + operator = "List[PGSQLMatchingPatternInString]" + else: + operator = "List[MatchingPatternInStringBase]" + + for i in [ + {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.Str + ExtraFieldType.Matching_pattern, + 'column_type': f'Optional[{operator}]', + 'column_default': f'[MatchingPatternInStringBase.case_sensitive]', + 'column_description': "None"}, + {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.Str, + 'column_type': f'Optional[List[{field_of_param["column_type"]}]]', + 'column_default': "None", + 'column_description': field_of_param['column_description']} + ]: + result_.append(i) + return result_ + + @staticmethod + def _assign_list_comparison(field_of_param, result_: List[dict]) -> List[dict]: + for i in [ + { + 'column_name': field_of_param[ + 'column_name'] + f'{ExtraFieldTypePrefix.List}{ExtraFieldType.Comparison_operator}', + 'column_type': 'Optional[ItemComparisonOperators]', + 'column_default': 'ItemComparisonOperators.In', + 'column_description': "None"}, + {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.List, + 'column_type': f'Optional[List[{field_of_param["column_type"]}]]', + 'column_default': 'None', + 'column_description': field_of_param['column_description']} + + ]: + result_.append(i) + return result_ + + @staticmethod + def _assign_range_comparison(field_of_param, result_: List[dict]) -> List[dict]: + for i in [ + {'column_name': field_of_param[ + 'column_name'] + f'{ExtraFieldTypePrefix.From}{ExtraFieldType.Comparison_operator}', + 'column_type': 'Optional[RangeFromComparisonOperators]', + 'column_default': 'RangeFromComparisonOperators.Greater_than_or_equal_to', + 'column_description': "None"}, + + {'column_name': field_of_param[ + 'column_name'] + f'{ExtraFieldTypePrefix.To}{ExtraFieldType.Comparison_operator}', + 'column_type': 'Optional[RangeToComparisonOperators]', + 'column_default': 'RangeToComparisonOperators.Less_than.Less_than_or_equal_to', + 'column_description': "None"}, + ]: + result_.append(i) + + for i in [ + {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.From, + 'column_type': f'Optional[NewType(ExtraFieldTypePrefix.From, {field_of_param["column_type"]})]', + 'column_default': "None", + 'column_description': field_of_param['column_description']}, + + {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.To, + 'column_type': f'Optional[NewType(ExtraFieldTypePrefix.To, {field_of_param["column_type"]})]', + 'column_default': "None", + 'column_description': field_of_param['column_description']} + ]: + result_.append(i) + return result_ + + def _get_fizzy_query_param(self, exclude_column: List[str] = None, fields=None) -> List[dict]: + if not fields: + fields = self.all_field + if not exclude_column: + exclude_column = [] + fields_: List[dict] = deepcopy(fields) + result = [] + for field_ in fields_: + if field_['column_name'] in exclude_column: + continue + if "column_foreign" in field_ and field_['column_foreign']: + jump = False + for foreign in field_['column_foreign']: + if foreign in exclude_column: + jump = True + if jump: + continue + field_['column_default'] = None + if field_['column_name'] in self.str_type_columns: + result = self._assign_str_matching_pattern(field_, result) + result = self._assign_list_comparison(field_, result) + + elif field_['column_name'] in self.uuid_type_columns or \ + field_['column_name'] in self.bool_type_columns: + result = self._assign_list_comparison(field_, result) + + elif field_['column_name'] in self.number_type_columns or \ + field_['column_name'] in self.datetime_type_columns: + result = self._assign_range_comparison(field_, result) + result = self._assign_list_comparison(field_, result) + + return result + + def _assign_pagination_param(self, result_: List[tuple]) -> List[Union[Tuple, Dict]]: + all_column_ = [i['column_name'] for i in self.all_field] + + regex_validation = "(?=(" + '|'.join(all_column_) + r")?\s?:?\s*?(?=(" + '|'.join( + list(map(str, Ordering))) + r"))?)" + columns_with_ordering = pydantic.constr(regex=regex_validation) + + for i in [ + ('limit', 'Optional[int]', "Query(None)"), + ('offset', 'Optional[int]', "Query(None)"), + ('order_by_columns', f'Optional[List[pydantic.constr(regex="{regex_validation}")]]', + f'''Query( + None, + description="""{self._get_many_order_by_columns_description_builder( + all_columns=all_column_, + regex_validation=regex_validation, + primary_name='any name of column')}""")''') + ]: + result_.append(i) + return result_ + + def upsert_one(self) -> Tuple: + request_validation = [lambda self_object: _filter_none(self_object)] + request_fields = [] + response_fields = [] + + # Create on_conflict Model + all_column_ = [i['column_name'] for i in self.all_field] + conflict_columns = ('update_columns', + "Optional[List[str]]", + f"Body({set(all_column_) - set(self.unique_fields)},description='update_columns should contain which columns you want to update when the unique columns got conflict')") + + self.code_gen.build_dataclass(class_name=self.class_name + "UpsertOneConflictModel", + fields=[conflict_columns]) + on_conflict_handle = [('on_conflict', f"Optional[{self.class_name + 'UpsertOneConflictModel'}]", + "Body(None)")] + + # Create Request and Response Model + all_field = deepcopy(self.all_field) + for i in all_field: + request_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']}, description={i['column_description']})")) + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']}, description={i['column_description']})")) + + self.code_gen.build_dataclass(class_name=self.class_name + "UpsertOneRequestBodyModel", + fields=request_fields + on_conflict_handle, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_dataclass(class_name=self.class_name + "UpsertOneResponseModel", + fields=response_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + return None, self.class_name + "UpsertOneRequestBodyModel", self.class_name + "UpsertOneResponseModel" + + def upsert_many(self) -> Tuple: + insert_fields = [] + response_fields = [] + + # Create on_conflict Model + all_column_ = [i['column_name'] for i in self.all_field] + conflict_columns = ('update_columns', + "Optional[List[str]]", + f"Body({set(all_column_) - set(self.unique_fields)},description='update_columns should contain which columns you want to update when the unique columns got conflict')") + + self.code_gen.build_dataclass(class_name=self.class_name + "UpsertManyConflictModel", + fields=[conflict_columns]) + on_conflict_handle = [('on_conflict', f"Optional[{self.class_name + 'UpsertManyConflictModel'}]", + "Body(None)")] + + # Ready the Request and Response Model + all_field = deepcopy(self.all_field) + + for i in all_field: + insert_fields.append((i['column_name'], + i['column_type'], + f'field(default=Body({i["column_default"]}, description={i["column_description"]}))')) + + if i["column_default"] == "None": + i["column_default"] = "..." + response_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]}, description={i["column_description"]})')) + + self.code_gen.build_dataclass(class_name=self.class_name + "UpsertManyItemRequestBodyModel", + fields=insert_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + + insert_list_field = [('insert', f"List[{self.class_name + 'UpsertManyItemRequestBodyModel'}]", "Body(...)")] + + self.code_gen.build_dataclass(class_name=self.class_name + "UpsertManyItemListRequestBodyModel", + fields=insert_list_field + on_conflict_handle, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True + ) + + self.code_gen.build_base_model(class_name=self.class_name + "UpsertManyItemResponseModel", + fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + + self.code_gen.build_base_model_root(class_name=self.class_name + "UpsertManyItemListResponseModel", + field=( + f'{f"{self.class_name}UpsertManyItemResponseModel"}', + None)) + + return None, self.class_name + "UpsertManyItemListRequestBodyModel", self.class_name + "UpsertManyItemListResponseModel" + + def create_one(self) -> Tuple: + request_validation = [lambda self_object: _filter_none(self_object)] + request_fields = [] + response_fields = [] + + # Create Request and Response Model + all_field = deepcopy(self.all_field) + for i in all_field: + request_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]}, description={i["column_description"]})')) + response_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]}, description={i["column_description"]})')) + + # Ready the uuid to str validator + if self.uuid_type_columns: + request_validation.append(lambda self_object: self._value_of_list_to_str(self_object, + self.uuid_type_columns)) + + self.code_gen.build_dataclass(class_name=self.class_name + "CreateOneRequestBodyModel", + fields=request_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + self.code_gen.build_base_model(class_name=self.class_name + "CreateOneResponseModel", + fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + + return None, self.class_name + "CreateOneRequestBodyModel", self.class_name + "CreateOneResponseModel" + + def create_many(self) -> Tuple: + insert_fields = [] + response_fields = [] + + all_field = deepcopy(self.all_field) + for i in all_field: + insert_fields.append((i['column_name'], + i['column_type'], + f'field(default=Body({i["column_default"]}, description={i["column_description"]}))')) + + if i["column_default"] == "None": + i["column_default"] = "..." + response_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]}, description={i["column_description"]})')) + + self.code_gen.build_dataclass(class_name=self.class_name + "CreateManyItemRequestModel", + fields=insert_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + + insert_list_field = [('insert', f"List[{self.class_name + 'CreateManyItemRequestModel'}]", "Body(...)")] + + self.code_gen.build_dataclass(class_name=self.class_name + "CreateManyItemListRequestModel", + fields=insert_list_field) + + self.code_gen.build_base_model(class_name=self.class_name + "CreateManyItemResponseModel", + fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + + self.code_gen.build_base_model_root(class_name=self.class_name + "CreateManyItemListResponseModel", + field=( + f'{f"{self.class_name}CreateManyItemResponseModel"}', + None)) + + return None, self.class_name + "CreateManyItemListRequestModel", self.class_name + "CreateManyItemListResponseModel" + + def find_many(self) -> Tuple: + + query_param: List[dict] = self._get_fizzy_query_param() + query_param: List[Tuple] = self._assign_pagination_param(query_param) + + response_fields = [] + all_field = deepcopy(self.all_field) + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + None)) + request_fields = [] + for i in query_param: + assert isinstance(i, Tuple) or isinstance(i, dict) + if isinstance(i, Tuple): + request_fields.append(i) + if isinstance(i, dict): + request_fields.append((i['column_name'], + i['column_type'], + f'Query({i["column_default"]}, description={i["column_description"]})')) + + self.code_gen.build_dataclass(class_name=self.class_name + "FindManyRequestBody", fields=request_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_dataclass(class_name=self.class_name + "FindManyResponseModel", fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model_root(class_name=self.class_name + "FindManyResponseRootModel", + field=( + f'{self.class_name + "FindManyResponseModel"}', + None), + base_model="ExcludeUnsetBaseModel") + + return self.class_name + "FindManyRequestBody", None, f'{self.class_name}FindManyResponseItemListModel' + + def _extra_relation_primary_key(self, relation_dbs): + primary_key_columns = [] + foreign_table_name = "" + primary_column_names = [] + for db_model_table in relation_dbs: + table_name = db_model_table.key + foreign_table_name += table_name + "_" + primary_list = db_model_table.primary_key.columns.values() + primary_key_column, = primary_list + column_type = str(primary_key_column.type) + try: + python_type = primary_key_column.type.python_type + if column_type in self.unsupported_data_types: + raise ColumnTypeNotSupportedException( + f'The type of column {primary_key_column.key} ({column_type}) not supported yet') + if column_type in self.partial_supported_data_types: + warnings.warn( + f'The type of column {primary_key_column.key} ({column_type}) ' + f'is not support data query (as a query parameters )') + + except NotImplementedError: + if column_type == "UUID": + python_type = uuid.UUID + else: + raise ColumnTypeNotSupportedException( + f'The type of column {primary_key_column.key} ({column_type}) not supported yet') + # handle if python type is UUID + if python_type.__name__ in ['str', + 'int', + 'float', + 'Decimal', + 'UUID', + 'bool', + 'date', + 'time', + 'datetime']: + column_type = python_type + else: + raise ColumnTypeNotSupportedException( + f'The type of column {primary_key_column.key} ({column_type}) not supported yet') + default = self._extra_default_value(primary_key_column) + if default is ...: + warnings.warn( + f'The column of {primary_key_column.key} has not default value ' + f'and it is not nullable and in exclude_list' + f'it may throw error when you insert data ') + description = self._get_field_description(primary_key_column) + primary_column_name = str(primary_key_column.key) + alias_primary_column_name = table_name + FOREIGN_PATH_PARAM_KEYWORD + str(primary_key_column.key) + primary_column_names.append(alias_primary_column_name) + primary_key_columns.append((alias_primary_column_name, column_type, Query(default, + description=description))) + + # TODO test foreign uuid key + primary_columns_model: DataClassT = make_dataclass(f'{foreign_table_name + str(uuid.uuid4())}_PrimaryKeyModel', + primary_key_columns, + namespace={ + '__post_init__': lambda + self_object: self._value_of_list_to_str( + self_object, self.uuid_type_columns) + }) + assert primary_column_names and primary_columns_model and primary_key_columns + return primary_column_names, primary_columns_model, primary_key_columns + + def find_one(self) -> Tuple: + query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) + response_fields = [] + all_field = deepcopy(self.all_field) + + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]})')) + + request_fields = [] + for i in query_param: + assert isinstance(i, dict) or isinstance(i, tuple) + if isinstance(i, Tuple): + request_fields.append(i) + else: + request_fields.append((i['column_name'], + i['column_type'], + f'Query({i["column_default"]})')) + self.code_gen.build_dataclass(class_name=self.class_name + "FindOneRequestBody", fields=request_fields, + value_of_list_to_str_columns=self.uuid_type_columns, filter_none=True) + + self.code_gen.build_dataclass(class_name=self.class_name + "FindOneResponseModel", fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + self.code_gen.build_base_model_root(class_name=self.class_name + "FindOneResponseRootModel", + field=( + f'{self.class_name + "FindOneResponseModel"}', + None), + base_model="ExcludeUnsetBaseModel") + + return self.class_name + "PrimaryKeyModel", self.class_name + "FindOneRequestBody", None, self.class_name + "FindOneResponseRootModel", None + + def delete_one(self) -> Tuple: + query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) + response_fields = [] + all_field = deepcopy(self.all_field) + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']})")) + + request_fields = [] + for i in query_param: + assert isinstance(i, dict) + request_fields.append((i['column_name'], + i['column_type'], + f"Query({i['column_default']}, description={i['column_description']})")) + + self.code_gen.build_dataclass(class_name=self.class_name + "DeleteOneRequestBodyModel", + fields=request_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model(class_name=self.class_name + "DeleteOneResponseModel", + fields=response_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + return self._primary_key_dataclass_model, self.class_name + "DeleteOneRequestBodyModel", None, self.class_name + "DeleteOneResponseModel" + + def delete_many(self) -> Tuple: + query_param: List[dict] = self._get_fizzy_query_param() + response_fields = [] + all_field = deepcopy(self.all_field) + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']})")) + + request_fields = [] + for i in query_param: + assert isinstance(i, dict) + request_fields.append((i['column_name'], + i['column_type'], + f"Query({i['column_default']}, description={i['column_description']})")) + + self.code_gen.build_dataclass(class_name=self.class_name + "DeleteManyRequestBodyModel", + fields=request_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model(class_name=self.class_name + "DeleteManyItemResponseModel", + fields=response_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model_root(class_name=self.class_name + "DeleteManyItemListResponseModel", + field=( + f'{self.class_name + "DeleteManyItemResponseModel"}', + None)) + + return None, self.class_name + "DeleteManyRequestBodyModel", None, self.class_name + "DeleteManyItemListResponseModel" + + def patch(self) -> Tuple: + query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) + + response_fields = [] + all_field = deepcopy(self.all_field) + request_body_fields = [] + + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']})")) + if i['column_name'] != self.primary_key_str: + request_body_fields.append((i['column_name'], + i['column_type'], + f"Body(None, description={i['column_description']})")) + + request_query_fields = [] + for i in query_param: + assert isinstance(i, dict) + request_query_fields.append((i['column_name'], + i['column_type'], + f"Query({i['column_default']}, description={i['column_description']})")) + + self.code_gen.build_dataclass(class_name=self.class_name + "PatchOneRequestQueryModel", + fields=request_query_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_dataclass(class_name=self.class_name + "PatchOneRequestBodyModel", + fields=request_body_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model(class_name=self.class_name + "PatchOneResponseModel", + fields=response_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + return self._primary_key_dataclass_model, self.class_name + "PatchOneRequestQueryModel", self.class_name + "PatchOneRequestBodyModel", self.class_name + "PatchOneResponseModel" + + def update_one(self) -> Tuple: + query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) + + response_fields = [] + all_field = deepcopy(self.all_field) + request_body_fields = [] + + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']})")) + if i['column_name'] not in [self.primary_key_str]: + request_body_fields.append((i['column_name'], + i['column_type'], + f"Body(..., description={i['column_description']})")) + + request_query_fields = [] + for i in query_param: + assert isinstance(i, dict) + request_query_fields.append((i['column_name'], + i['column_type'], + f"Query({i['column_default']}, description={i['column_description']})")) + + request_validation = [lambda self_object: _filter_none(self_object)] + if self.uuid_type_columns: + request_validation.append(lambda self_object: self._value_of_list_to_str(self_object, + self.uuid_type_columns)) + + self.code_gen.build_dataclass(class_name=self.class_name + "UpdateOneRequestQueryBody", + fields=request_query_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + + self.code_gen.build_dataclass(class_name=self.class_name + "UpdateOneRequestBodyBody", + fields=request_body_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + + self.code_gen.build_base_model(class_name=self.class_name + "UpdateOneResponseModel", + fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + return self.class_name + "PrimaryKeyModel", self.class_name + "UpdateOneRequestQueryBody", self.class_name + "UpdateOneRequestBodyBody", self.class_name + "UpdateOneResponseModel" + + def update_many(self) -> Tuple: + """ + In update many, it allow you update some columns into the same value in limit of a scope, + you can get the limit of scope by using request query. + And fill out the columns (except the primary key column and unique columns) you want to update + and the update value in the request body + + The response will show you the update result + :return: url param dataclass model + """ + query_param: List[dict] = self._get_fizzy_query_param() + + response_fields = [] + all_field = deepcopy(self.all_field) + request_body_fields = [] + + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']})")) + if i['column_name'] not in [self.primary_key_str]: + request_body_fields.append((i['column_name'], + i['column_type'], + f"Body(..., description={i['column_description']})")) + + request_query_fields = [] + for i in query_param: + assert isinstance(i, dict) + request_query_fields.append((i['column_name'], + i['column_type'], + f"Query({i['column_default']}, description={i['column_description']})")) + + self.code_gen.build_dataclass(class_name=self.class_name + "UpdateManyRequestQueryBody", + fields=request_query_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + self.code_gen.build_dataclass(class_name=self.class_name + "UpdateManyRequestBodyBody", + fields=request_body_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + self.code_gen.build_base_model(class_name=self.class_name + "UpdateManyResponseItemModel", + fields=response_fields, + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + self.code_gen.build_base_model_root(class_name=f'{self.class_name}UpdateManyResponseItemListModel', + field=( + f'Union[List[{f"{self.class_name}UpdateManyResponseItemModel"}]]', + None), + value_of_list_to_str_columns=self.uuid_type_columns, + filter_none=True) + # response_model = _add_orm_model_config_into_pydantic_model(response_model, config=OrmConfig) + + return None, self.class_name + "UpdateManyRequestQueryBody", self.class_name + "UpdateManyRequestBodyBody", f'{self.class_name}UpdateManyResponseItemListModel' + + def patch_many(self) -> Tuple: + """ + In update many, it allow you update some columns into the same value in limit of a scope, + you can get the limit of scope by using request query. + And fill out the columns (except the primary key column and unique columns) you want to update + and the update value in the request body + + The response will show you the update result + :return: url param dataclass model + """ + query_param: List[dict] = self._get_fizzy_query_param() + + response_fields = [] + all_field = deepcopy(self.all_field) + request_body_fields = [] + + for i in all_field: + response_fields.append((i['column_name'], + i['column_type'], + f"Body({i['column_default']})")) + if i['column_name'] not in [self.primary_key_str]: + request_body_fields.append((i['column_name'], + i['column_type'], + f"Body(None, description={i['column_description']})")) + + request_query_fields = [] + for i in query_param: + assert isinstance(i, dict) + request_query_fields.append((i['column_name'], + i['column_type'], + f"Query({i['column_default']}, description={i['column_description']})")) + + self.code_gen.build_dataclass(class_name=self.class_name + "PatchManyRequestQueryBody", + fields=request_query_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_dataclass(class_name=self.class_name + "PatchManyRequestBody", + fields=request_body_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model(class_name=self.class_name + "PatchManyItemResponseModel", + fields=response_fields, + filter_none=True, + value_of_list_to_str_columns=self.uuid_type_columns) + + self.code_gen.build_base_model_root(class_name=f'{self.class_name}PatchManyItemListResponseModel', + field=( + f'{f"{self.class_name}PatchManyItemResponseModel"}', + None)) + return None, self.class_name + "UpdateManyRequestQueryBody", self.class_name + "UpdateManyRequestBodyBody", f'{self.class_name}UpdateManyResponseItemListModel' + + def post_redirect_get(self) -> Tuple: + request_validation = [lambda self_object: _filter_none(self_object)] + request_body_fields = [] + response_body_fields = [] + + # Create Request and Response Model + all_field = deepcopy(self.all_field) + for i in all_field: + request_body_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]}, description={i["column_description"]})')) + response_body_fields.append((i['column_name'], + i['column_type'], + f'Body({i["column_default"]}, description={i["column_description"]})')) + + # Ready the uuid to str validator + if self.uuid_type_columns: + request_validation.append(lambda self_object: self._value_of_list_to_str(self_object, + self.uuid_type_columns)) + self.code_gen.build_dataclass(class_name=self.class_name + "PostAndRedirectRequestModel", + fields=request_body_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + self.code_gen.build_base_model(class_name=self.class_name + "PostAndRedirectResponseModel", + fields=response_body_fields, + value_of_list_to_str_columns=self.uuid_type_columns) + return None, self.class_name + "PostAndRedirectRequestModel", self.class_name + "PostAndRedirectResponseModel" + diff --git a/src/fastapi_quickcrud_codegen/misc/type.py b/src/fastapi_quickcrud_codegen/misc/type.py new file mode 100644 index 0000000..17416fc --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/type.py @@ -0,0 +1,162 @@ +from enum import Enum, auto +from itertools import chain + +from strenum import StrEnum + +from .exceptions import InvalidRequestMethod + + +class SqlType(StrEnum): + postgresql = auto() + mysql = auto() + mariadb = auto() + sqlite = auto() + oracle = auto() + mssql = auto() + +class Ordering(StrEnum): + DESC = auto() + ASC = auto() + + +class CrudMethods(Enum): + FIND_ONE = "FIND_ONE" + FIND_MANY = "FIND_MANY" + UPDATE_ONE = "UPDATE_ONE" + UPDATE_MANY = "UPDATE_MANY" + PATCH_ONE = "PATCH_ONE" + PATCH_MANY = "PATCH_MANY" + UPSERT_ONE = "UPSERT_ONE" + UPSERT_MANY = "UPSERT_MANY" + CREATE_ONE = "CREATE_ONE" + CREATE_MANY = "CREATE_MANY" + DELETE_ONE = "DELETE_ONE" + DELETE_MANY = "DELETE_MANY" + POST_REDIRECT_GET = "POST_REDIRECT_GET" + FIND_ONE_WITH_FOREIGN_TREE = "FIND_ONE_WITH_FOREIGN_TREE" + FIND_MANY_WITH_FOREIGN_TREE = "FIND_MANY_WITH_FOREIGN_TREE" + + @staticmethod + def get_table_full_crud_method(): + return [CrudMethods.FIND_MANY, CrudMethods.CREATE_MANY, CrudMethods.UPDATE_MANY, CrudMethods.PATCH_MANY, + CrudMethods.DELETE_MANY] + + @staticmethod + def get_declarative_model_full_crud_method(): + return [CrudMethods.FIND_MANY, CrudMethods.FIND_ONE, + CrudMethods.UPDATE_MANY, CrudMethods.UPDATE_ONE, + CrudMethods.PATCH_MANY, CrudMethods.PATCH_ONE, CrudMethods.CREATE_MANY, + CrudMethods.DELETE_MANY, CrudMethods.DELETE_ONE, CrudMethods.FIND_ONE_WITH_FOREIGN_TREE, + CrudMethods.FIND_MANY_WITH_FOREIGN_TREE] + + +class RequestMethods(Enum): + GET = "GET" + POST = "POST" + PUT = "PUT" + PATCH = "PATCH" + DELETE = "DELETE" + + +class CRUDRequestMapping(Enum): + FIND_ONE = RequestMethods.GET + FIND_ONE_WITH_FOREIGN_TREE = RequestMethods.GET + + FIND_MANY = RequestMethods.GET + FIND_MANY_WITH_FOREIGN_TREE = RequestMethods.GET + + UPDATE_ONE = RequestMethods.PUT + UPDATE_MANY = RequestMethods.PUT + + PATCH_ONE = RequestMethods.PATCH + PATCH_MANY = RequestMethods.PATCH + + CREATE_ONE = RequestMethods.POST + CREATE_MANY = RequestMethods.POST + + UPSERT_ONE = RequestMethods.POST + UPSERT_MANY = RequestMethods.POST + + DELETE_ONE = RequestMethods.DELETE + DELETE_MANY = RequestMethods.DELETE + + GET_VIEW = RequestMethods.GET + POST_REDIRECT_GET = RequestMethods.POST + + @classmethod + def get_request_method_by_crud_method(cls, value): + crud_methods = cls.__dict__ + if value not in crud_methods: + raise InvalidRequestMethod( + f'{value} is not an available request method, Please use CrudMethods to select available crud method') + return crud_methods[value].value + + +class ExtraFieldType(StrEnum): + Comparison_operator = '_____comparison_operator' + Matching_pattern = '_____matching_pattern' + + +class ExtraFieldTypePrefix(StrEnum): + List = '____list' + From = '____from' + To = '____to' + Str = '____str' + + +class RangeFromComparisonOperators(StrEnum): + Greater_than = auto() + Greater_than_or_equal_to = auto() + + +class RangeToComparisonOperators(StrEnum): + Less_than = auto() + Less_than_or_equal_to = auto() + + +class ItemComparisonOperators(StrEnum): + Equal = auto() + Not_equal = auto() + In = auto() + Not_in = auto() + + +class MatchingPatternInStringBase(StrEnum): + case_insensitive = auto() + case_sensitive = auto() + not_case_insensitive = auto() + not_case_sensitive = auto() + contains = auto() + + +class PGSQLMatchingPattern(StrEnum): + match_regex_with_case_sensitive = auto() + match_regex_with_case_insensitive = auto() + does_not_match_regex_with_case_sensitive = auto() + does_not_match_regex_with_case_insensitive = auto() + similar_to = auto() + not_similar_to = auto() + + +PGSQLMatchingPatternInString = StrEnum('PGSQLMatchingPatternInString', + {Pattern: auto() for Pattern in + chain(MatchingPatternInStringBase, PGSQLMatchingPattern)}) + + +class JSONMatchingMode(str, Enum): + match_the_key_value = 'match_the_key_value' + match_the_value_if_not_null_by_key = 'match_the_value_if_not_null_by_key' + custom_query = 'custom_query' + + +class JSONBMatchingMode(str, Enum): + match_the_key_value = 'match_the_key_value' + match_the_value_if_not_null_by_key = 'match_the_value_if_not_null_by_key' + custom_query = 'custom_query' + + +class SessionObject(StrEnum): + sqlalchemy = auto() + databases = auto() + +FOREIGN_PATH_PARAM_KEYWORD = "__pk__" \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/misc/utils.py b/src/fastapi_quickcrud_codegen/misc/utils.py new file mode 100644 index 0000000..08780f4 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/misc/utils.py @@ -0,0 +1,349 @@ +from itertools import groupby +from typing import Type, List, Union, TypeVar, Optional + +from pydantic import BaseModel, BaseConfig +from sqlalchemy import Column, Integer +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql.elements import \ + or_, \ + BinaryExpression +from sqlalchemy.sql.schema import Table + +from .covert_model import convert_table_to_model +from .crud_model import CRUDModel +from .exceptions import QueryOperatorNotFound, PrimaryMissing, UnknownColumn +from .schema_builder import ApiParameterSchemaBuilder +from .type import \ + CrudMethods, \ + CRUDRequestMapping, \ + MatchingPatternInStringBase, \ + ExtraFieldType, \ + RangeFromComparisonOperators, \ + ExtraFieldTypePrefix, \ + RangeToComparisonOperators, \ + ItemComparisonOperators, PGSQLMatchingPatternInString, SqlType, FOREIGN_PATH_PARAM_KEYWORD + +Base = TypeVar("Base", bound=declarative_base) + +BaseModelT = TypeVar('BaseModelT', bound=BaseModel) + +__all__ = [ + 'sqlalchemy_to_pydantic', + # 'sqlalchemy_table_to_pydantic', + 'find_query_builder', + 'Base', + 'clean_input_fields', + 'group_find_many_join', + 'convert_table_to_model'] + +unsupported_data_types = ["BLOB"] +partial_supported_data_types = ["INTERVAL", "JSON", "JSONB"] + + +def clean_input_fields(param: Union[dict, list], model: Base): + assert isinstance(param, dict) or isinstance(param, list) or isinstance(param, set) + + if isinstance(param, dict): + stmt = {} + for column_name, value in param.items(): + if column_name == '__initialised__': + continue + column = getattr(model, column_name) + actual_column_name = column.expression.key + stmt[actual_column_name] = value + return stmt + if isinstance(param, list) or isinstance(param, set): + stmt = [] + for column_name in param: + if not hasattr(model, column_name): + raise UnknownColumn(f'column {column_name} is not exited') + column = getattr(model, column_name) + actual_column_name = column.expression.key + stmt.append(actual_column_name) + return stmt + + +def find_query_builder(param: dict, model: Base) -> List[Union[BinaryExpression]]: + query = [] + for column_name, value in param.items(): + if ExtraFieldType.Comparison_operator in column_name or ExtraFieldType.Matching_pattern in column_name: + continue + if ExtraFieldTypePrefix.List in column_name: + type_ = ExtraFieldTypePrefix.List + elif ExtraFieldTypePrefix.From in column_name: + type_ = ExtraFieldTypePrefix.From + elif ExtraFieldTypePrefix.To in column_name: + type_ = ExtraFieldTypePrefix.To + elif ExtraFieldTypePrefix.Str in column_name: + type_ = ExtraFieldTypePrefix.Str + else: + query.append((getattr(model, column_name) == value)) + # raise Exception('known error') + continue + sub_query = [] + table_column_name = column_name.replace(type_, "") + operator_column_name = column_name + process_type_map[type_] + operators = param.get(operator_column_name, None) + if not operators: + raise QueryOperatorNotFound(f'The query operator of {column_name} not found!') + if not isinstance(operators, list): + operators = [operators] + for operator in operators: + sub_query.append(process_map[operator](getattr(model, table_column_name), value)) + query.append((or_(*sub_query))) + return query + + +class OrmConfig(BaseConfig): + orm_mode = True + + +def sqlalchemy_to_pydantic( + db_model: Type, *, + crud_methods: List[CrudMethods], + sql_type: str = SqlType.postgresql, + exclude_columns: List[str] = None, + constraints=None, + # foreign_include: Optional[any] = None, + exclude_primary_key=False) -> CRUDModel: + db_model, _ = convert_table_to_model(db_model) + if exclude_columns is None: + exclude_columns = [] + # if foreign_include is None: + # foreign_include = {} + request_response_mode_set = {} + model_builder = ApiParameterSchemaBuilder(db_model, + constraints=constraints, + exclude_column=exclude_columns, + sql_type=sql_type, + # foreign_include=foreign_include, + exclude_primary_key=exclude_primary_key) + REQUIRE_PRIMARY_KEY_CRUD_METHOD = [CrudMethods.DELETE_ONE.value, + CrudMethods.FIND_ONE.value, + CrudMethods.PATCH_ONE.value, + CrudMethods.POST_REDIRECT_GET.value, + CrudMethods.UPDATE_ONE.value] + for crud_method in crud_methods: + if crud_method.value in REQUIRE_PRIMARY_KEY_CRUD_METHOD and not model_builder.primary_key_str: + raise PrimaryMissing(f"The generation of this API [{crud_method.value}] requires a primary key") + + if crud_method.value == CrudMethods.UPSERT_ONE.value: + model_builder.upsert_one() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.UPSERT_MANY.value: + model_builder.upsert_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + + elif crud_method.value == CrudMethods.CREATE_ONE.value: + model_builder.create_one() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + + elif crud_method.value == CrudMethods.CREATE_MANY.value: + model_builder.create_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.DELETE_ONE.value: + model_builder.delete_one() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.DELETE_MANY.value: + model_builder.delete_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.FIND_ONE.value: + model_builder.find_one() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.FIND_MANY.value: + model_builder.find_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.POST_REDIRECT_GET.value: + model_builder.post_redirect_get() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.PATCH_ONE.value: + model_builder.patch() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.UPDATE_ONE.value: + model_builder.update_one() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.UPDATE_MANY.value: + model_builder.update_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.PATCH_MANY.value: + model_builder.patch_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.FIND_ONE_WITH_FOREIGN_TREE.value: + model_builder.foreign_tree_get_one() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + elif crud_method.value == CrudMethods.FIND_MANY_WITH_FOREIGN_TREE.value: + model_builder.foreign_tree_get_many() + request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value + if request_method not in request_response_mode_set: + request_response_mode_set[request_method] = {} + request_response_mode_set[request_method][crud_method.value] = True + model_builder.code_gen.gen() + + return CRUDModel( + **{**request_response_mode_set, + **{"PRIMARY_KEY_NAME": model_builder.primary_key_str, + "UNIQUE_LIST": model_builder.unique_fields}}) + + +process_type_map = { + ExtraFieldTypePrefix.List: ExtraFieldType.Comparison_operator, + ExtraFieldTypePrefix.From: ExtraFieldType.Comparison_operator, + ExtraFieldTypePrefix.To: ExtraFieldType.Comparison_operator, + ExtraFieldTypePrefix.Str: ExtraFieldType.Matching_pattern, +} + +process_map = { + RangeFromComparisonOperators.Greater_than: + lambda field, value: field > value, + + RangeFromComparisonOperators.Greater_than_or_equal_to: + lambda field, value: field >= value, + + RangeToComparisonOperators.Less_than: + lambda field, value: field < value, + + RangeToComparisonOperators.Less_than_or_equal_to: + lambda field, value: field <= value, + + ItemComparisonOperators.Equal: + lambda field, values: or_(field == value for value in values), + + ItemComparisonOperators.Not_equal: + lambda field, values: or_(field != value for value in values), + + ItemComparisonOperators.In: + lambda field, values: or_(field.in_(values)), + + ItemComparisonOperators.Not_in: + lambda field, values: or_(field.notin_(values)), + + MatchingPatternInStringBase.case_insensitive: + lambda field, values: or_(field.ilike(value) for value in values), + + MatchingPatternInStringBase.case_sensitive: + lambda field, values: or_(field.like(value) for value in values), + + MatchingPatternInStringBase.not_case_insensitive: + lambda field, values: or_(field.not_ilike(value) for value in values), + + MatchingPatternInStringBase.not_case_sensitive: + lambda field, values: or_(field.not_like(value) for value in values), + + MatchingPatternInStringBase.contains: + lambda field, values: or_(field.contains(value) for value in values), + + PGSQLMatchingPatternInString.similar_to: + lambda field, values: or_(field.op("SIMILAR TO")(value) for value in values), + + PGSQLMatchingPatternInString.not_similar_to: + lambda field, values: or_(field.op("NOT SIMILAR TO")(value) for value in values), + + PGSQLMatchingPatternInString.match_regex_with_case_sensitive: + lambda field, values: or_(field.op("~")(value) for value in values), + + PGSQLMatchingPatternInString.match_regex_with_case_insensitive: + lambda field, values: or_(field.op("~*")(value) for value in values), + + PGSQLMatchingPatternInString.does_not_match_regex_with_case_sensitive: + lambda field, values: or_(field.op("!~")(value) for value in values), + + PGSQLMatchingPatternInString.does_not_match_regex_with_case_insensitive: + lambda field, values: or_(field.op("!~*")(value) for value in values) +} + + +def table_to_declarative_base(db_model): + db_name = str(db_model.fullname) + Base = declarative_base() + if not db_model.primary_key: + db_model.append_column(Column('__id', Integer, primary_key=True, autoincrement=True)) + table_dict = {'__tablename__': db_name} + for i in db_model.c: + _, = i.expression.base_columns + _.table = None + table_dict[str(i.key)] = _ + tmp = type(f'{db_name}', (Base,), table_dict) + tmp.__table__ = db_model + return tmp + + +def group_find_many_join(list_of_dict: List[dict]) -> List[dict]: + def group_by_foreign_key(item): + tmp = {} + for k, v in item.items(): + if '_foreign' not in k: + tmp[k] = v + return tmp + + response_list = [] + for key, group in groupby(list_of_dict, group_by_foreign_key): + response = {} + for i in group: + for k, v in i.items(): + if '_foreign' in k: + if k not in response: + response[k] = [v] + else: + response[k].append(v) + for response_ in response: + i.pop(response_, None) + result = {**i, **response} + response_list.append(result) + return response_list + + + + +def path_query_builder(params, model) -> List[Union[BinaryExpression]]: + query = [] + if not params: + return query + for param_name, param_value in params.items(): + table_with_column = param_name.split(FOREIGN_PATH_PARAM_KEYWORD) + assert len(table_with_column) == 2 + table_name, column_name = table_with_column + table_model = model[table_name] + query.append((getattr(table_model, column_name) == param_value)) + return query diff --git a/src/fastapi_quickcrud_codegen/model/__init__.py b/src/fastapi_quickcrud_codegen/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/model/common_builder.py b/src/fastapi_quickcrud_codegen/model/common_builder.py new file mode 100644 index 0000000..1d64867 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/common_builder.py @@ -0,0 +1,134 @@ +import inspect +import sys +from pathlib import Path +from textwrap import dedent +from typing import ClassVar + +import importmagic +import jinja2 +from importmagic import SymbolIndex, Scope +from sqlalchemy import Table + + +class CommonCodeGen(): + def __init__(self): + self.code = "" + self.model_code = "" + self.index = SymbolIndex() + lib_path: list[str] = [i for i in sys.path if "fastapi_quickcrud_codegen" not in i] + self.index.build_index(lib_path) + self.import_list = "" + + # todo add tpye for template_generator + def gen(self, template_generator_method): + template_generator_method( self.import_list + "\n\n" + self.code) + + def gen_model(self, model): + if isinstance(model, Table): + raise TypeError("not support table yet") + model_code = inspect.getsource(model) + self.model_code += "\n\n\n" + model_code + + def build_app(self, *, async_mode, model_name): + mode = "async" if async_mode else "sync" + TEMPLATE_FILE_PATH: ClassVar[str] = f'route/{mode}_find_one.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'{mode}_find_one.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render( + {"model_name": model_name}) + self.code += "\n\n\n" + code + + def build_api_route(self): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/api_route.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'api_route.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render() + self.code += "\n\n\n" + code + + def build_type(self): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/typing.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'typing.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render() + self.code += "\n\n\n" + code + + def build_utils(self): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/utils.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'utils.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render() + self.import_list = """ +from fastapi_quick_crud_template.common.http_exception import QueryOperatorNotFound +from fastapi_quick_crud_template.common.typing import ExtraFieldType, ExtraFieldTypePrefix, process_type_map, \ + process_map + +""" + self.code += "\n\n\n" + code + + def build_http_exception(self): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/http_exception.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'http_exception.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render() + self.code += "\n\n\n" + code + + def build_db(self): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/db.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'db.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render() + self.code += "\n\n\n" + code + + def build_db_session(self, model_list): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/memory_sql_session.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'memory_sql_session.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render({"model_list": model_list}) + self.code += "\n\n\n" + code + + def build_app(self, model_list): + TEMPLATE_FILE_PATH: ClassVar[str] = f'common/app.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'app.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render({"model_list": model_list}) + self.code += "\n\n\n" + code diff --git a/src/fastapi_quickcrud_codegen/model/crud_builder.py b/src/fastapi_quickcrud_codegen/model/crud_builder.py new file mode 100644 index 0000000..b20ecc1 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/crud_builder.py @@ -0,0 +1,65 @@ +import inspect +import sys +from pathlib import Path +from textwrap import dedent +from typing import ClassVar + +import importmagic +import jinja2 +from importmagic import SymbolIndex, Scope +from sqlalchemy import Table + +from fastapi_quickcrud_codegen.generator.crud_template_generator import CrudTemplateGenerator + + +class CrudCodeGen(): + def __init__(self, file_name, model_name, tags, prefix): + self.file_name = file_name + self.code = "\n\n\n" + "api = APIRouter(tags=" + str(tags) + ',' + "prefix=" + '"' + prefix + '")' + "\n\n" + # self.index = SymbolIndex() + # lib_path: list[str] = [i for i in sys.path if "FastAPIQuickCRUD" not in i] + # self.index.build_index(lib_path) + self.model_name = model_name + self.import_list = f""" + +import copy +from http import HTTPStatus +from typing import List +from os import path + +from sqlalchemy import and_, select +from fastapi import Depends, Response, APIRouter +from sqlalchemy.sql.elements import BinaryExpression + +from fastapi_quick_crud_template.common.utils import find_query_builder +from fastapi_quick_crud_template.common.sql_session import db_session +from fastapi_quick_crud_template.model.{file_name} import ({model_name}FindOneResponseModel, + {model_name}PrimaryKeyModel, + {model_name}FindOneRequestBody, + {model_name}) + """ + + def gen(self, template_generator: CrudTemplateGenerator): + # src = dedent(self.model_code + "\n\n" +self.code) + # scope = Scope.from_source(src) + # + # unresolved, unreferenced = scope.find_unresolved_and_unreferenced_symbols() + # a = importmagic.get_update(src, self.index, unresolved, unreferenced) + # python_source = importmagic.update_imports(src, self.index, unresolved, unreferenced) + # template_generator.add_route(self.file_name, python_source) + template_generator.add_route(self.file_name, self.import_list + "\n\n" + self.code) + + def build_find_one_route(self, *, async_mode, path): + mode = "async" if async_mode else "sync" + TEMPLATE_FILE_PATH: ClassVar[str] = f'route/{mode}_find_one.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = f'{mode}_find_one.jinja2' + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render( + {"model_name": self.model_name, "path": path}) + self.code += "\n\n\n" + code + diff --git a/src/fastapi_quickcrud_codegen/model/model_builder.py b/src/fastapi_quickcrud_codegen/model/model_builder.py new file mode 100644 index 0000000..104ab7a --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/model_builder.py @@ -0,0 +1,133 @@ +import inspect +import sys +from pathlib import Path +from textwrap import dedent +from typing import ClassVar + +import importmagic +import jinja2 +from importmagic import SymbolIndex, Scope +from sqlalchemy import Table + +from fastapi_quickcrud_codegen.generator.model_template_generator import model_template_gen + + +class ModelCodeGen(): + def __init__(self, file_name, db_type): + self.file_name = file_name + self.table_list = {} + self.code = "" + self.model_code = "" + self.index = SymbolIndex() + lib_path: list[str] = [i for i in sys.path if "FastAPIQuickCRUD" not in i] + self.index.build_index(lib_path) + self.import_list = f""" +import uuid +from dataclasses import dataclass +from datetime import datetime, timedelta, date, time +from decimal import Decimal +from typing import Optional, List, Union, NewType + +from fastapi import Query, Body +from sqlalchemy import * +from sqlalchemy.dialects.{db_type} import * + +from fastapi_quick_crud_template.common.utils import value_of_list_to_str, ExcludeUnsetBaseModel, filter_none +from fastapi_quick_crud_template.common.db import Base +from fastapi_quick_crud_template.common.typing import ItemComparisonOperators, PGSQLMatchingPatternInString, \ + ExtraFieldTypePrefix, RangeToComparisonOperators, MatchingPatternInStringBase, RangeFromComparisonOperators +""" + + def gen(self): + # src = dedent(self.model_code + "\n\n" +self.code) + # scope = Scope.from_source(src) + # + # unresolved, unreferenced = scope.find_unresolved_and_unreferenced_symbols() + # python_source = importmagic.update_imports(src, self.index, unresolved, unreferenced) + # model_template_gen.add_model(self.file_name, python_source) + return model_template_gen.add_model(self.file_name, self.import_list + "\n\n" + self.model_code + "\n\n" + self.code) + + def gen_model(self, model): + if isinstance(model, Table): + raise TypeError("not support table yet") + model_code = inspect.getsource(model) + self.model_code += "\n\n\n" + model_code + + def build_base_model(self, *, class_name, fields, description=None, orm_mode=True, + value_of_list_to_str_columns=None, filter_none=None): + TEMPLATE_FILE_PATH: ClassVar[str] = 'pydantic/BaseModel.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = "BaseModel.jinja2" + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render( + {"class_name": class_name, "fields": fields, "description": description, "orm_mode": orm_mode, + "value_of_list_to_str_columns": value_of_list_to_str_columns, "filter_none": filter_none}) + self.table_list[class_name] = code + self.code += "\n\n\n" + code + + def build_base_model_root(self, *, class_name, field, description=None, base_model="BaseModel", + value_of_list_to_str_columns=None, filter_none=None): + + if class_name in self.table_list: + return self.table_list[class_name] + TEMPLATE_FILE_PATH: ClassVar[str] = 'pydantic/BaseModel.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = "BaseModel_root.jinja2" + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render( + {"class_name": class_name, "field": field, "description": description, "base_model": base_model,"value_of_list_to_str_columns": value_of_list_to_str_columns, "filter_none": filter_none}) + self.table_list[class_name] = code + self.code += "\n\n\n" + code + + def build_dataclass(self, *, class_name, fields, description=None, value_of_list_to_str_columns=None, + filter_none=None): + if class_name in self.table_list: + return self.table_list[class_name] + TEMPLATE_FILE_PATH: ClassVar[str] = 'pydantic/BaseModel.jinja2' + template_file_path = Path(TEMPLATE_FILE_PATH) + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = "dataclass.jinja2" + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render({"class_name": class_name, "fields": fields, "description": description, + "value_of_list_to_str_columns": value_of_list_to_str_columns, + "filter_none": filter_none}) + self.code += "\n\n\n" + code + + def build_enum(self, *, class_name, fields, description=None): + if class_name in self.table_list: + return self.table_list[class_name] + TEMPLATE_FILE_PATH: ClassVar[str] = '' + template_file_path = Path(TEMPLATE_FILE_PATH) + BASE_CLASS: ClassVar[str] = 'pydantic.BaseModel' + + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = "Enum.jinja2" + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render({"class_name": class_name, "fields": fields, "description": description}) + self.table_list[class_name] = code + self.code += "\n\n\n" + code + + def build_constant(self, *, constants): + TEMPLATE_FILE_PATH: ClassVar[str] = '' + template_file_path = Path(TEMPLATE_FILE_PATH) + TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' + templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) + templateEnv = jinja2.Environment(loader=templateLoader) + TEMPLATE_FILE = "Constant.jinja2" + template = templateEnv.get_template(TEMPLATE_FILE) + code = template.render({"constants": constants}) + self.code += "\n\n\n" + code + diff --git a/src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 b/src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 new file mode 100644 index 0000000..e361c9a --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 @@ -0,0 +1,9 @@ + +{%- for constant in constants %} + {% if constant[1] is iterable and (constant[1] is not string and constant[1] is not mapping )%} +{{ constant[0] }} = "{{ constant[1] | join('", "') }}" + {% else %} +{{ constant[0] }} = "{{ constant[1] }}" + {% endif %} + +{%- endfor -%} diff --git a/src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 b/src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 new file mode 100644 index 0000000..04d53e3 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 @@ -0,0 +1,12 @@ +{% for decorator in decorators -%} +{{ decorator }} +{% endfor -%} +class {{ class_name }}(Enum): +{%- if description %} + """ + {{ description }} + """ +{%- endif %} +{%- for field in fields %} + {{ field[0] }} = {{ field[1] }} +{%- endfor -%} diff --git a/src/fastapi_quickcrud_codegen/model/template/__init__.py b/src/fastapi_quickcrud_codegen/model/template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/model/template/common/__init__.py b/src/fastapi_quickcrud_codegen/model/template/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 new file mode 100644 index 0000000..ce1effb --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +api_routes: List[APIRouter] = [] diff --git a/src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 new file mode 100644 index 0000000..2fd8bd5 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 @@ -0,0 +1,15 @@ +import uvicorn +from fastapi import FastAPI +{% for model in model_list -%} +from fastapi_quick_crud_template.route.{{ model["model_name"] }} import api as {{ model["model_name"] }}_router + +{%- endfor %} +app = FastAPI() + +[app.include_router(api_route) for api_route in [ +{% for model in model_list -%} +{{ model["model_name"] }}_router, +{%- endfor %} +]] + +uvicorn.run(app, host="0.0.0.0", port=8000, debug=False) diff --git a/src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 new file mode 100644 index 0000000..711eda2 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 @@ -0,0 +1,4 @@ +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() +metadata = Base.metadata \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 new file mode 100644 index 0000000..e428c7e --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 @@ -0,0 +1,71 @@ +from fastapi import HTTPException + + +class FindOneApiNotRegister(HTTPException): + pass + + +class CRUDBuilderException(BaseException): + pass + + +class RequestMissing(CRUDBuilderException): + pass + + +class PrimaryMissing(CRUDBuilderException): + pass + + +class UnknownOrderType(CRUDBuilderException): + pass + + +class UpdateColumnEmptyException(CRUDBuilderException): + pass + + +class UnknownColumn(CRUDBuilderException): + pass + + +class QueryOperatorNotFound(CRUDBuilderException): + pass + + +class UnknownError(CRUDBuilderException): + pass + + +class ConflictColumnsCannotHit(CRUDBuilderException): + pass + + +class MultipleSingleUniqueNotSupportedException(CRUDBuilderException): + pass + + +class SchemaException(CRUDBuilderException): + pass + + +class CompositePrimaryKeyConstraintNotSupportedException(CRUDBuilderException): + pass + + +class MultiplePrimaryKeyNotSupportedException(CRUDBuilderException): + pass + + +class ColumnTypeNotSupportedException(CRUDBuilderException): + pass + + +class InvalidRequestMethod(CRUDBuilderException): + pass + +class FDDRestHTTPException(HTTPException): + """Baseclass for all HTTP exceptions in FDD Rest API. This exception can be called as WSGI + application to render a default error page or you can catch the subclasses + of it independently and render nicer error messages. + """ diff --git a/src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 new file mode 100644 index 0000000..436c01f --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 @@ -0,0 +1,91 @@ +import asyncio +from typing import Generator + +from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.pool import StaticPool + + +{% for model in model_list -%} + +from fastapi_quick_crud_template.model.{{ model["model_name"] }} import {{ model["file_name"] }} +{%- endfor %} + +{%- if is_memory_sql %} + # please manually update if don't want to use in-memory db +{%- endif %} + +{%- if async_mode %} + + +SQLALCHEMY_DATABASE_URL = f"sqlite+aiosqlite://" + +engine = create_async_engine(SQLALCHEMY_DATABASE_URL, + future=True, + echo=True, + pool_pre_ping=True, + pool_recycle=7200, + connect_args={"check_same_thread": False}, + poolclass=StaticPool) +session = sessionmaker(autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession) +{%- else %} + + +SQLALCHEMY_DATABASE_URL = f"sqlite://" + +engine = create_engine(SQLALCHEMY_DATABASE_URL, + future=True, + echo=True, + pool_pre_ping=True, + pool_recycle=7200, + connect_args={"check_same_thread": False}, + poolclass=StaticPool) +session = sessionmaker(bind=engine, autocommit=False) +{%- endif %} + + + +{%- if async_mode %} + +{% for model in model_list -%} +async def create_table(engine, {{ model["file_name"] }}): + async with engine.begin() as conn: + wait conn.run_sync(model._sa_registry.metadata.create_all) + + loop = asyncio.get_event_loop() + loop.run_until_complete(create_table(engine, Mode)) +{%- endfor %} + + +{%- else %} + + +{% for model in model_list -%} +{{ model["file_name"] }}.__table__.create(engine, checkfirst=True) +{%- endfor %} +{%- endif %} + + +{%- if async_mode %} + + +async def db_session(): + async with session() as session: + yield session +{%- else %} + + +def db_session() -> Generator: + try: + db = session() + yield db + except Exception as e: + db.rollback() + raise e + finally: + db.close() +{%- endif %} diff --git a/src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 new file mode 100644 index 0000000..ce1effb --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +api_routes: List[APIRouter] = [] diff --git a/src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 new file mode 100644 index 0000000..b51e42a --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 @@ -0,0 +1,154 @@ +from enum import Enum, auto +from itertools import chain +from sqlalchemy import or_ +from strenum import StrEnum + +class CrudMethods(Enum): + FIND_ONE = "FIND_ONE" + FIND_MANY = "FIND_MANY" + UPDATE_ONE = "UPDATE_ONE" + UPDATE_MANY = "UPDATE_MANY" + PATCH_ONE = "PATCH_ONE" + PATCH_MANY = "PATCH_MANY" + UPSERT_ONE = "UPSERT_ONE" + UPSERT_MANY = "UPSERT_MANY" + CREATE_ONE = "CREATE_ONE" + CREATE_MANY = "CREATE_MANY" + DELETE_ONE = "DELETE_ONE" + DELETE_MANY = "DELETE_MANY" + POST_REDIRECT_GET = "POST_REDIRECT_GET" + FIND_ONE_WITH_FOREIGN_TREE = "FIND_ONE_WITH_FOREIGN_TREE" + FIND_MANY_WITH_FOREIGN_TREE = "FIND_MANY_WITH_FOREIGN_TREE" + + @staticmethod + def get_table_full_crud_method(): + return [CrudMethods.FIND_MANY, CrudMethods.CREATE_MANY, CrudMethods.UPDATE_MANY, CrudMethods.PATCH_MANY, + CrudMethods.DELETE_MANY] + + @staticmethod + def get_declarative_model_full_crud_method(): + return [CrudMethods.FIND_MANY, CrudMethods.FIND_ONE, + CrudMethods.UPDATE_MANY, CrudMethods.UPDATE_ONE, + CrudMethods.PATCH_MANY, CrudMethods.PATCH_ONE, CrudMethods.CREATE_MANY, + CrudMethods.DELETE_MANY, CrudMethods.DELETE_ONE, CrudMethods.FIND_ONE_WITH_FOREIGN_TREE, + CrudMethods.FIND_MANY_WITH_FOREIGN_TREE] + + + +class ExtraFieldTypePrefix(StrEnum): + List = '____list' + From = '____from' + To = '____to' + Str = '____str' + + + +class ExtraFieldType(StrEnum): + Comparison_operator = '_____comparison_operator' + Matching_pattern = '_____matching_pattern' + + + +class MatchingPatternInStringBase(StrEnum): + case_insensitive = auto() + case_sensitive = auto() + not_case_insensitive = auto() + not_case_sensitive = auto() + contains = auto() + + +class PGSQLMatchingPattern(StrEnum): + match_regex_with_case_sensitive = auto() + match_regex_with_case_insensitive = auto() + does_not_match_regex_with_case_sensitive = auto() + does_not_match_regex_with_case_insensitive = auto() + similar_to = auto() + not_similar_to = auto() + + +PGSQLMatchingPatternInString = StrEnum('PGSQLMatchingPatternInString', + {Pattern: auto() for Pattern in + chain(MatchingPatternInStringBase, PGSQLMatchingPattern)}) + +process_type_map = { + ExtraFieldTypePrefix.List: ExtraFieldType.Comparison_operator, + ExtraFieldTypePrefix.From: ExtraFieldType.Comparison_operator, + ExtraFieldTypePrefix.To: ExtraFieldType.Comparison_operator, + ExtraFieldTypePrefix.Str: ExtraFieldType.Matching_pattern, +} + +class RangeFromComparisonOperators(StrEnum): + Greater_than = auto() + Greater_than_or_equal_to = auto() + + +class RangeToComparisonOperators(StrEnum): + Less_than = auto() + Less_than_or_equal_to = auto() + + +class ItemComparisonOperators(StrEnum): + Equal = auto() + Not_equal = auto() + In = auto() + Not_in = auto() + + +process_map = { + RangeFromComparisonOperators.Greater_than: + lambda field, value: field > value, + + RangeFromComparisonOperators.Greater_than_or_equal_to: + lambda field, value: field >= value, + + RangeToComparisonOperators.Less_than: + lambda field, value: field < value, + + RangeToComparisonOperators.Less_than_or_equal_to: + lambda field, value: field <= value, + + ItemComparisonOperators.Equal: + lambda field, values: or_(field == value for value in values), + + ItemComparisonOperators.Not_equal: + lambda field, values: or_(field != value for value in values), + + ItemComparisonOperators.In: + lambda field, values: or_(field.in_(values)), + + ItemComparisonOperators.Not_in: + lambda field, values: or_(field.notin_(values)), + + MatchingPatternInStringBase.case_insensitive: + lambda field, values: or_(field.ilike(value) for value in values), + + MatchingPatternInStringBase.case_sensitive: + lambda field, values: or_(field.like(value) for value in values), + + MatchingPatternInStringBase.not_case_insensitive: + lambda field, values: or_(field.not_ilike(value) for value in values), + + MatchingPatternInStringBase.not_case_sensitive: + lambda field, values: or_(field.not_like(value) for value in values), + + MatchingPatternInStringBase.contains: + lambda field, values: or_(field.contains(value) for value in values), + + PGSQLMatchingPatternInString.similar_to: + lambda field, values: or_(field.op("SIMILAR TO")(value) for value in values), + + PGSQLMatchingPatternInString.not_similar_to: + lambda field, values: or_(field.op("NOT SIMILAR TO")(value) for value in values), + + PGSQLMatchingPatternInString.match_regex_with_case_sensitive: + lambda field, values: or_(field.op("~")(value) for value in values), + + PGSQLMatchingPatternInString.match_regex_with_case_insensitive: + lambda field, values: or_(field.op("~*")(value) for value in values), + + PGSQLMatchingPatternInString.does_not_match_regex_with_case_sensitive: + lambda field, values: or_(field.op("!~")(value) for value in values), + + PGSQLMatchingPatternInString.does_not_match_regex_with_case_insensitive: + lambda field, values: or_(field.op("!~*")(value) for value in values) +} diff --git a/src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 new file mode 100644 index 0000000..61a8201 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 @@ -0,0 +1,103 @@ +from typing import TypeVar, List, Union +from copy import deepcopy + +from sqlalchemy import or_ +from sqlalchemy.orm import declarative_base +from sqlalchemy.sql.elements import BinaryExpression +from pydantic import BaseModel + + +Base = TypeVar("Base", bound=declarative_base) + + +def find_query_builder(param: dict, model: Base) -> List[Union[BinaryExpression]]: + query = [] + for column_name, value in param.items(): + if ExtraFieldType.Comparison_operator in column_name or ExtraFieldType.Matching_pattern in column_name: + continue + if ExtraFieldTypePrefix.List in column_name: + type_ = ExtraFieldTypePrefix.List + elif ExtraFieldTypePrefix.From in column_name: + type_ = ExtraFieldTypePrefix.From + elif ExtraFieldTypePrefix.To in column_name: + type_ = ExtraFieldTypePrefix.To + elif ExtraFieldTypePrefix.Str in column_name: + type_ = ExtraFieldTypePrefix.Str + else: + query.append((getattr(model, column_name) == value)) + # raise Exception('known error') + continue + sub_query = [] + table_column_name = column_name.replace(type_, "") + operator_column_name = column_name + process_type_map[type_] + operators = param.get(operator_column_name, None) + if not operators: + raise QueryOperatorNotFound(f'The query operator of {column_name} not found!') + if not isinstance(operators, list): + operators = [operators] + for operator in operators: + sub_query.append(process_map[operator](getattr(model, table_column_name), value)) + query.append((or_(*sub_query))) + return query + + +def value_of_list_to_str(request_or_response_object, columns): + received_request = deepcopy(request_or_response_object.__dict__) + if isinstance(columns, str): + columns = [columns] + if 'insert' in request_or_response_object.__dict__: + insert_str_list = [] + for insert_item in request_or_response_object.__dict__['insert']: + for column in columns: + for insert_item_column, _ in insert_item.__dict__.items(): + if column in insert_item_column: + value_ = insert_item.__dict__[insert_item_column] + if value_ is not None: + if isinstance(value_, list): + str_value_ = [str(i) for i in value_] + else: + str_value_ = str(value_) + setattr(insert_item, insert_item_column, str_value_) + insert_str_list.append(insert_item) + setattr(request_or_response_object, 'insert', insert_str_list) + else: + for column in columns: + for received_column_name, _ in received_request.items(): + if column in received_column_name: + value_ = received_request[received_column_name] + if value_ is not None: + if isinstance(value_, list): + str_value_ = [str(i) for i in value_] + else: + str_value_ = str(value_) + setattr(request_or_response_object, received_column_name, str_value_) + + +def filter_none(request_or_response_object): + received_request = deepcopy(request_or_response_object.__dict__) + if 'insert' in received_request: + insert_item_without_null = [] + for received_insert in received_request['insert']: + received_insert_ = deepcopy(received_insert) + for received_insert_item, received_insert_value in received_insert_.__dict__.items(): + if hasattr(received_insert_value, '__module__'): + if received_insert_value.__module__ == 'fastapi.params' or received_insert_value is None: + delattr(received_insert, received_insert_item) + elif received_insert_value is None: + delattr(received_insert, received_insert_item) + + insert_item_without_null.append(received_insert) + setattr(request_or_response_object, 'insert', insert_item_without_null) + else: + for name, value in received_request.items(): + if hasattr(value, '__module__'): + if value.__module__ == 'fastapi.params' or value is None: + delattr(request_or_response_object, name) + elif value is None: + delattr(request_or_response_object, name) + +class ExcludeUnsetBaseModel(BaseModel): + def dict(self, *args, **kwargs): + if kwargs and kwargs.get("exclude_none") is not None: + kwargs["exclude_unset"] = True + return BaseModel.dict(self, *args, **kwargs) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 new file mode 100644 index 0000000..d46ef23 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 @@ -0,0 +1,38 @@ +{% for decorator in decorators -%} +{{ decorator }} +{% endfor -%} +class {{ class_name }}(BaseModel): + """ + auto gen by FastApi quick CRUD + """ +{%- if not fields %} + pass +{%- endif %} +{%- if config %} +{%- filter indent(4) %} +{% include 'Config.jinja2' %} +{%- endfilter %} +{%- endif %} +{%- for field in fields -%} + {%- if field|length > 2 %} + {{ field[0] }}: {{ field[1] }} = {{field[2]}} + + {%- else %} + {{ field[0] }}: {{ field[1] }} + {%- endif %} +{%- endfor -%} + +{%- if value_of_list_to_str_columns or filter_none %} + def __init__(self): + {%- if value_of_list_to_str_columns %} + value_of_list_to_str(self, {{ value_of_list_to_str_columns }}) + {%- endif %} + {%- if filter_none %} + filter_none(self) + {%- endif %} +{%- endif %} + +{%- if orm_mode %} + class Config: + orm_mode = True +{%- endif %} \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 new file mode 100644 index 0000000..03dc8ca --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 @@ -0,0 +1,31 @@ +{% for decorator in decorators -%} +{{ decorator }} +{% endfor -%} +class {{ class_name }}({{ base_model }}): +{%- if description %} + """ + auto gen by FastApi quick CRUD + """ +{%- endif %} +{%- if config %} +{%- filter indent(4) %} +{% include 'Config.jinja2' %} +{%- endfilter %} +{%- endif %} +{%- if not field %} + pass +{%- else %} + __root__: List[{{ field[0] }}] +{%- endif %} + +{%- if value_of_list_to_str_columns or filter_none %} + def __init__(self): + {%- if value_of_list_to_str_columns %} + value_of_list_to_str(self, {{ value_of_list_to_str_columns }}) + {%- endif %} + {%- if filter_none %} + filter_none(self) + {%- endif %} +{%- endif %} + class Config: + orm_mode = True diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 new file mode 100644 index 0000000..3612790 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 @@ -0,0 +1,4 @@ +class Config: +{%- for field_name, value in config.dict(exclude_unset=True).items() %} + {{ field_name }} = {{ value }} +{%- endfor %} \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/__init__.py b/src/fastapi_quickcrud_codegen/model/template/pydantic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 new file mode 100644 index 0000000..1b147ff --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 @@ -0,0 +1,38 @@ +{% for decorator in decorators -%} +{{ decorator }} +{% endfor -%} +@dataclass +{%- if base_class %} +class {{ class_name }}({{ base_class }}): +{%- else %} +class {{ class_name }}: +{%- endif %} +{%- if description %} + """ + {{ description }} + """ +{%- endif %} +{%- if not fields %} + pass +{%- endif %} +{%- for field in fields -%} + {%- if field|length > 2 %} + {{ field[0] }}: {{ field[1] }} = {{field[2]}} + {%- else %} + {{ field[0] }}: {{ field[1] }} + {%- endif %} +{%- endfor -%} + + +{%- if value_of_list_to_str_columns or filter_none %} + def __post_init__(self): + """ + auto gen by FastApi quick CRUD + """ + {%- if value_of_list_to_str_columns %} + value_of_list_to_str(self, {{ value_of_list_to_str_columns }}) + {%- endif %} + {%- if filter_none %} + filter_none(self) + {%- endif %} +{%- endif %} diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 new file mode 100644 index 0000000..ffdf367 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 @@ -0,0 +1,4 @@ +def __post_init__(self): +{% for method in methods -%} + {{methods}}(self) +{% endfor -%} \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 new file mode 100644 index 0000000..ebaa0e0 --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 @@ -0,0 +1,6 @@ + +class ExcludeUnsetBaseModel(BaseModel): + def dict(self, *args, **kwargs): + if kwargs and kwargs.get("exclude_none") is not None: + kwargs["exclude_unset"] = True + return BaseModel.dict(self, *args, **kwargs) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/route/__init__.py b/src/fastapi_quickcrud_codegen/model/template/route/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 b/src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 new file mode 100644 index 0000000..640bd4a --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 @@ -0,0 +1,30 @@ +@api.get("{{ path }}", status_code=200, response_model={{ model_name }}FindOneResponseModel) +def get_one_by_primary_key(response: Response, + url_param=Depends({{ model_name }}PrimaryKeyModel), + query=Depends({{ model_name }}FindOneRequestBody), + session=Depends(db_session)): + filter_list: List[BinaryExpression] = find_query_builder(param=query.__dict__, + model=UntitledTable256) + + extra_query_expression: List[BinaryExpression] = find_query_builder(param=url_param.__dict__, + model=UntitledTable256) + model = {{ model_name }} + stmt = select(*[model]).where(and_(*filter_list + extra_query_expression)) + sql_executed_result = session.execute(stmt) + + one_row_data = sql_executed_result.fetchall() + if not one_row_data: + return Response('specific data not found', status_code=HTTPStatus.NOT_FOUND) + response = [] + for i in one_row_data: + i = dict(i) + result__ = copy.deepcopy(i) + tmp = {} + for key_, value_ in result__.items(): + tmp[key_] = value_ + response.append(tmp) + if isinstance(response, list): + response = response[0] + response.headers["x-total-count"] = str(1) + session.commit() + return response diff --git a/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/__init__.py b/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 b/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 new file mode 100644 index 0000000..ef3510f --- /dev/null +++ b/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 @@ -0,0 +1,24 @@ +{% for decorator in decorators -%} +{{ decorator }} +{% endfor -%} +@dataclass +{%- if base_class %} +class {{ class_name }}({{ base_class }}): +{%- else %} +class {{ class_name }}: +{%- endif %} +{%- if description %} + """ + {{ description }} + """ +{%- endif %} +{%- if not fields %} + pass +{%- endif %} +{%- for field in fields -%} + {%- if field|length > 2 %} + {{ field[0] }}: {{ field[1] }} = {{field[2]}} + {%- else %} + {{ field[0] }}: {{ field[1] }} + {%- endif %} +{%- endfor -%} diff --git a/src/fastapi_quickcrud_codegen/parse/__init__.py b/src/fastapi_quickcrud_codegen/parse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastapi_quickcrud_codegen/parse/parse_import.py b/src/fastapi_quickcrud_codegen/parse/parse_import.py new file mode 100644 index 0000000..3d7a17a --- /dev/null +++ b/src/fastapi_quickcrud_codegen/parse/parse_import.py @@ -0,0 +1,24 @@ +import ast +from collections import namedtuple + +Import = namedtuple("Import", ["module", "name", "alias"]) + +def get_imports(path): + with open(path) as fh: + root = ast.parse(fh.read(), path) + + for node in ast.iter_child_nodes(root): + if isinstance(node, ast.Import): + module = [] + elif isinstance(node, ast.ImportFrom): + module = node.module.split('.') + else: + continue + + for n in node.names: + if not module: + yield f'import {n.name} ' + elif n.asname: + yield f'from {".".join(module)} import {n.name} as {n.asname}' + else: + yield f'from {".".join(module)} import {n.name} ' diff --git a/src/sample_test.py b/src/sample_test.py new file mode 100644 index 0000000..f61f4dd --- /dev/null +++ b/src/sample_test.py @@ -0,0 +1,86 @@ +import os + +from fastapi import FastAPI +from sqlalchemy import ARRAY, BigInteger, Boolean, CHAR, Column, Date, DateTime, Float, Integer, \ + JSON, Numeric, SmallInteger, String, Text, Time, UniqueConstraint, text +from sqlalchemy.dialects.postgresql import INTERVAL, JSONB, UUID +from sqlalchemy.orm import declarative_base, sessionmaker + +from fastapi_quickcrud_codegen import crud_router_builder, CrudMethods + +TEST_DATABASE_URL = os.environ.get('TEST_DATABASE_URL', 'postgresql://postgres:1234@127.0.0.1:5432/postgres') + +app = FastAPI() + +Base = declarative_base() +metadata = Base.metadata + +from sqlalchemy import create_engine + +engine = create_engine(TEST_DATABASE_URL, future=True, echo=True, + pool_use_lifo=True, pool_pre_ping=True, pool_recycle=7200) +async_session = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_transaction_session(): + try: + db = async_session() + yield db + finally: + db.close() + + +class UntitledTable256(Base): + primary_key_of_table = "primary_key" + unique_fields = ['primary_key', 'int4_value', 'float4_value'] + __tablename__ = 'test_build_myself' + __table_args__ = ( + UniqueConstraint('primary_key', 'int4_value', 'float4_value'), + ) + primary_key = Column(Integer, primary_key=True, info={'alias_name': 'primary_key'}, autoincrement=True, + server_default="nextval('test_build_myself_id_seq'::regclass)") + bool_value = Column(Boolean, nullable=False, server_default=text("false")) + # bytea_value = Column(LargeBinary) + char_value = Column(CHAR(10)) + date_value = Column(Date, server_default=text("now()")) + float4_value = Column(Float, nullable=False) + float8_value = Column(Float(53), nullable=False, server_default=text("10.10")) + int2_value = Column(SmallInteger, nullable=False) + int4_value = Column(Integer, nullable=True) + int8_value = Column(BigInteger, server_default=text("99")) + interval_value = Column(INTERVAL) + json_value = Column(JSON) + jsonb_value = Column(JSONB(astext_type=Text())) + numeric_value = Column(Numeric) + text_value = Column(Text) + time_value = Column(Time) + timestamp_value = Column(DateTime) + timestamptz_value = Column(DateTime(True)) + timetz_value = Column(Time(True)) + uuid_value = Column(UUID(as_uuid=True)) + varchar_value = Column(String) + # xml_value = Column(NullType) + array_value = Column(ARRAY(Integer())) + array_str__value = Column(ARRAY(String())) + # box_valaue = Column(NullType) + + +crud_route_child2 = crud_router_builder( + db_model=UntitledTable256, + prefix="/blog_comment", + tags=["blog_comment"], + db_session=get_transaction_session +) + +app = FastAPI() +[app.include_router(i) for i in [crud_route_child2]] + + +@app.get("/", tags=["child"]) +async def root(): + return {"message": "Hello World"} + + +import uvicorn + +uvicorn.run(app, host="0.0.0.0", port=8002, debug=False) diff --git a/tutorial/foreign_tree/async_m2m.py b/tutorial/foreign_tree/async_m2m.py index 2a2b9ff..c4d6a28 100644 --- a/tutorial/foreign_tree/async_m2m.py +++ b/tutorial/foreign_tree/async_m2m.py @@ -1,4 +1,5 @@ import asyncio +import inspect from fastapi import FastAPI from sqlalchemy import Column, Integer, \ From da7bcd85645c30ae824ff56c281d5406d993d7d2 Mon Sep 17 00:00:00 2001 From: LuisLuii Date: Mon, 17 Oct 2022 10:08:56 +0800 Subject: [PATCH 2/2] move the code gen feature to other repo --- .gitignore | 3 + src/fastapi_quickcrud_codegen/__init__.py | 5 - .../crud_generator.py | 162 --- .../generator/__init__.py | 0 .../common_module_template_generator.py | 110 -- .../generator/crud_template_generator.py | 53 - .../generator/hardcode_template_generator.py | 88 -- .../generator/model_template_generator.py | 50 - .../misc/__init__.py | 0 .../misc/abstract_execute.py | 34 - .../misc/abstract_parser.py | 375 ----- .../misc/abstract_query.py | 412 ------ .../misc/abstract_route.py | 1281 ----------------- .../misc/constant.py | 4 - .../misc/covert_model.py | 24 - .../misc/crud_model.py | 46 - .../misc/exceptions.py | 85 -- .../misc/get_table_name.py | 16 - .../misc/memory_sql.py | 70 - .../misc/schema_builder.py | 1124 --------------- src/fastapi_quickcrud_codegen/misc/type.py | 162 --- src/fastapi_quickcrud_codegen/misc/utils.py | 349 ----- .../model/__init__.py | 0 .../model/common_builder.py | 134 -- .../model/crud_builder.py | 65 - .../model/model_builder.py | 133 -- .../model/template/Constant.jinja2 | 9 - .../model/template/Enum.jinja2 | 12 - .../model/template/__init__.py | 0 .../model/template/common/__init__.py | 0 .../model/template/common/api_route.jinja2 | 3 - .../model/template/common/app.jinja2 | 15 - .../model/template/common/db.jinja2 | 4 - .../template/common/http_exception.jinja2 | 71 - .../template/common/memory_sql_session.jinja2 | 91 -- .../template/common/route_container.jinja2 | 3 - .../model/template/common/typing.jinja2 | 154 -- .../model/template/common/utils.jinja2 | 103 -- .../model/template/pydantic/BaseModel.jinja2 | 38 - .../template/pydantic/BaseModel_root.jinja2 | 31 - .../model/template/pydantic/Config.jinja2 | 4 - .../model/template/pydantic/__init__.py | 0 .../model/template/pydantic/dataclass.jinja2 | 38 - .../template/pydantic/dataclass_method.jinja2 | 4 - .../pydantic/exclude_unset_baseModel.jinja2 | 6 - .../model/template/route/__init__.py | 0 .../model/template/route/sync_find_one.jinja2 | 30 - .../model/template/sqlalchemy/__init__.py | 0 .../template/sqlalchemy/dataclass.jinja2 | 24 - .../parse/__init__.py | 0 .../parse/parse_import.py | 24 - 51 files changed, 3 insertions(+), 5446 deletions(-) delete mode 100644 src/fastapi_quickcrud_codegen/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/crud_generator.py delete mode 100644 src/fastapi_quickcrud_codegen/generator/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py delete mode 100644 src/fastapi_quickcrud_codegen/generator/crud_template_generator.py delete mode 100644 src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py delete mode 100644 src/fastapi_quickcrud_codegen/generator/model_template_generator.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_execute.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_parser.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_query.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/abstract_route.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/constant.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/covert_model.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/crud_model.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/exceptions.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/get_table_name.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/memory_sql.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/schema_builder.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/type.py delete mode 100644 src/fastapi_quickcrud_codegen/misc/utils.py delete mode 100644 src/fastapi_quickcrud_codegen/model/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/model/common_builder.py delete mode 100644 src/fastapi_quickcrud_codegen/model/crud_builder.py delete mode 100644 src/fastapi_quickcrud_codegen/model/model_builder.py delete mode 100644 src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/route/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/model/template/sqlalchemy/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 delete mode 100644 src/fastapi_quickcrud_codegen/parse/__init__.py delete mode 100644 src/fastapi_quickcrud_codegen/parse/parse_import.py diff --git a/.gitignore b/.gitignore index fcbaa99..196f5d0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ workspace.xml *.iml *.xml *.sh +fastapi_quickcrud_code_generator_beta.egg-info +src/fastapi_quick_crud_template +src/fastapi_quickcrud_codegen_backup diff --git a/src/fastapi_quickcrud_codegen/__init__.py b/src/fastapi_quickcrud_codegen/__init__.py deleted file mode 100644 index 8d796ff..0000000 --- a/src/fastapi_quickcrud_codegen/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .misc.utils import sqlalchemy_to_pydantic -from .crud_generator import crud_router_builder -from .misc.type import CrudMethods - - diff --git a/src/fastapi_quickcrud_codegen/crud_generator.py b/src/fastapi_quickcrud_codegen/crud_generator.py deleted file mode 100644 index 4bbe1f2..0000000 --- a/src/fastapi_quickcrud_codegen/crud_generator.py +++ /dev/null @@ -1,162 +0,0 @@ -from typing import \ - List, \ - TypeVar, Union, Optional - -from fastapi import \ - APIRouter -from pydantic import \ - BaseModel -from sqlalchemy.sql.schema import Table - -from . import sqlalchemy_to_pydantic -from .generator.common_module_template_generator import CommonModuleTemplateGenerator -from .generator.crud_template_generator import CrudTemplateGenerator -from .misc.crud_model import CRUDModel -from .misc.get_table_name import get_table_name -from .misc.type import CrudMethods, SqlType -from .misc.utils import convert_table_to_model -from .model.common_builder import CommonCodeGen -from .model.crud_builder import CrudCodeGen - -CRUDModelType = TypeVar("CRUDModelType", bound=BaseModel) -CompulsoryQueryModelType = TypeVar("CompulsoryQueryModelType", bound=BaseModel) -OnConflictModelType = TypeVar("OnConflictModelType", bound=BaseModel) - - -def crud_router_builder( - *, - db_model_list: Union[Table, 'DeclarativeBaseModel'], - async_mode: Optional[bool], - sql_type: Optional[SqlType], - crud_methods: Optional[List[CrudMethods]] = None, - exclude_columns: Optional[List[str]] = None, - # foreign_include: Optional[Base] = None -) -> APIRouter: - """ - @param db_model: - The Sqlalchemy Base model/Table you want to use it to build api. - - @param async_mode: - As your database connection - - @param sql_type: - You sql database type - - @param db_session: - The callable variable and return a session generator that will be used to get database connection session for fastapi. - - @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_codegen_codegen import CrudMethods - example: - [CrudMethods.GET_MANY,CrudMethods.ONE] - note: - if there is no primary key in your SQLAlchemy model, it dose not support request with - 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: - 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 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 foreign_include: BaseModel - Used to build foreign tree api - - @return: - APIRouter for fastapi - """ - model_list = [] - for db_model_info in db_model_list: - - db_model = db_model_info["db_model"] - prefix = db_model_info["prefix"] - tags = db_model_info["tags"] - - table_name = db_model.__name__ - model_name = get_table_name(db_model) - - model_list.append({"model_name": model_name, "file_name": table_name}) - - - db_model, NO_PRIMARY_KEY = convert_table_to_model(db_model) - - # code gen - crud_code_generator = CrudCodeGen(model_name, model_name=table_name, tags=tags, prefix=prefix) - # create a file - crud_template_generator = CrudTemplateGenerator() - - constraints = db_model.__table__.constraints - - common_module_template_generator = CommonModuleTemplateGenerator() - - # type - common_code_builder = CommonCodeGen() - common_code_builder.build_type() - common_code_builder.gen(common_module_template_generator.add_type) - - # module - common_utils_code_builder = CommonCodeGen() - common_utils_code_builder.build_utils() - common_utils_code_builder.gen(common_module_template_generator.add_utils) - - # http_exception - common_http_exception_code_builder = CommonCodeGen() - common_http_exception_code_builder.build_http_exception() - common_http_exception_code_builder.gen(common_module_template_generator.add_http_exception) - - # db - common_db_code_builder = CommonCodeGen() - common_db_code_builder.build_db() - common_db_code_builder.gen(common_module_template_generator.add_db) - - if not crud_methods and NO_PRIMARY_KEY == False: - crud_methods = CrudMethods.get_declarative_model_full_crud_method() - if not crud_methods and NO_PRIMARY_KEY == True: - crud_methods = CrudMethods.get_table_full_crud_method() - - crud_models_builder: CRUDModel = sqlalchemy_to_pydantic - crud_models: CRUDModel = crud_models_builder(db_model=db_model, - constraints=constraints, - crud_methods=crud_methods, - exclude_columns=exclude_columns, - sql_type=sql_type, - exclude_primary_key=NO_PRIMARY_KEY) - - methods_dependencies = crud_models.get_available_request_method() - primary_name = crud_models.PRIMARY_KEY_NAME - if primary_name: - path = '/{' + primary_name + '}' - else: - path = "" - - def find_one_api(): - crud_code_generator.build_find_one_route(async_mode=async_mode, path=path) - - api_register = { - CrudMethods.FIND_ONE.value: find_one_api, - } - for request_method in methods_dependencies: - value_of_dict_crud_model = crud_models.get_model_by_request_method(request_method) - crud_model_of_this_request_methods = value_of_dict_crud_model.keys() - for crud_model_of_this_request_method in crud_model_of_this_request_methods: - api_register[crud_model_of_this_request_method.value]() - crud_code_generator.gen(crud_template_generator) - - # sql session - common_db_session_code_builder = CommonCodeGen() - common_db_session_code_builder.build_db_session(model_list=model_list) - common_db_session_code_builder.gen(common_module_template_generator.add_memory_sql_session) - - # app py - common_app_code_builder = CommonCodeGen() - common_app_code_builder.build_app(model_list=model_list) - common_app_code_builder.gen(common_module_template_generator.add_app) diff --git a/src/fastapi_quickcrud_codegen/generator/__init__.py b/src/fastapi_quickcrud_codegen/generator/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py b/src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py deleted file mode 100644 index 3be7edd..0000000 --- a/src/fastapi_quickcrud_codegen/generator/common_module_template_generator.py +++ /dev/null @@ -1,110 +0,0 @@ -import inspect -import os -import shutil -import sys - -from sqlalchemy import Table - -from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, ROUTE, COMMON - - -class CommonModuleTemplateGenerator: - def __init__(self): - dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) - self.current_directory = dirname - self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) - self.module_path_map = {} - - - def __create_root_template_folder(self): - if not os.path.exists(self.template_root_directory): - os.makedirs(self.template_root_directory) - - def __create_folder(self, path): - if not os.path.exists(path): - os.makedirs(path) - - def __create_module_folder(self): - if not os.path.exists(self.template_model_directory): - os.makedirs(self.template_model_directory) - - def add_resolver(self, model_name, code): - template_module_directory = os.path.join(self.template_root_directory, model_name) - template_model_directory = os.path.join(template_module_directory) - - path = f'{template_model_directory}/__init__.py' - self.add_to_model_file(path, "") - - self.__create_model_folder(template_model_directory) - path = f'{template_model_directory}/{model_name}.py' - self.add_to_model_file(path, code) - self.module_path_map[model_name] = {'model': path} - - def add_type(self, code): - template_module_directory = os.path.join(self.template_root_directory, COMMON) - self.__create_folder(template_module_directory) - - path = f'{template_module_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - path = f'{template_module_directory}/typing.py' - self.create_file_and_add_code_into_there(path, code) - - def add_utils(self, code): - template_module_directory = os.path.join(self.template_root_directory, COMMON) - self.__create_folder(template_module_directory) - - path = f'{template_module_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - path = f'{template_module_directory}/utils.py' - self.create_file_and_add_code_into_there(path, code) - - def add_http_exception(self, code): - template_module_directory = os.path.join(self.template_root_directory, COMMON) - self.__create_folder(template_module_directory) - - path = f'{template_module_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - path = f'{template_module_directory}/http_exception.py' - self.create_file_and_add_code_into_there(path, code) - - def add_db(self, code): - template_module_directory = os.path.join(self.template_root_directory, COMMON) - self.__create_folder(template_module_directory) - - path = f'{template_module_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - path = f'{template_module_directory}/db.py' - self.create_file_and_add_code_into_there(path, code) - - def add_memory_sql_session(self, code): - - template_module_directory = os.path.join(self.template_root_directory, COMMON) - self.__create_folder(template_module_directory) - - path = f'{template_module_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - path = f'{template_module_directory}/sql_session.py' - self.create_file_and_add_code_into_there(path, code) - - - def add_app(self, code): - - template_module_directory = os.path.join(self.template_root_directory) - self.__create_folder(template_module_directory) - - path = f'{template_module_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - path = f'{template_module_directory}/app.py' - self.create_file_and_add_code_into_there(path, code) - - @staticmethod - def create_file_and_add_code_into_there(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - diff --git a/src/fastapi_quickcrud_codegen/generator/crud_template_generator.py b/src/fastapi_quickcrud_codegen/generator/crud_template_generator.py deleted file mode 100644 index 5fd1aea..0000000 --- a/src/fastapi_quickcrud_codegen/generator/crud_template_generator.py +++ /dev/null @@ -1,53 +0,0 @@ -import os -import sys - -from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, ROUTE - - -class CrudTemplateGenerator: - def __init__(self): - dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) - self.current_directory = dirname - self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) - self.module_path_map = {} - - - def __create_root_template_folder(self): - if not os.path.exists(self.template_root_directory): - os.makedirs(self.template_root_directory) - - def __create_model_folder(self, path): - if not os.path.exists(path): - os.makedirs(path) - - def __create_module_folder(self): - if not os.path.exists(self.template_model_directory): - os.makedirs(self.template_model_directory) - - def add_route(self, model_name, code): - template_model_directory = os.path.join(self.template_root_directory, ROUTE) - - self.__create_model_folder(template_model_directory) - - path = f'{template_model_directory}/__init__.py' - self.add_code_to_file(path, "") - - path = f'{template_model_directory}/{model_name}.py' - self.add_code_to_file(path, code) - # self.module_path_map[model_name] = {'model': path} - - - - @staticmethod - def add_code_to_file(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - - @staticmethod - def add_to_controller_file(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - - - - diff --git a/src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py b/src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py deleted file mode 100644 index 29d8246..0000000 --- a/src/fastapi_quickcrud_codegen/generator/hardcode_template_generator.py +++ /dev/null @@ -1,88 +0,0 @@ -import inspect -import os -import shutil -import sys - -from sqlalchemy import Table - -from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, ROUTE - - -class HardCodeTemplateGenerator: - def __init__(self): - dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) - self.current_directory = dirname - self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) - self.module_path_map = {} - - - def __create_root_template_folder(self): - if not os.path.exists(self.template_root_directory): - os.makedirs(self.template_root_directory) - - def __create_model_folder(self, path): - if not os.path.exists(path): - os.makedirs(path) - - def __create_module_folder(self): - if not os.path.exists(self.template_model_directory): - os.makedirs(self.template_model_directory) - - def add_resolver(self, model_name, code): - template_module_directory = os.path.join(self.template_root_directory, model_name) - template_model_directory = os.path.join(template_module_directory, ROUTE) - - path = f'{template_model_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - self.__create_model_folder(template_model_directory) - path = f'{template_model_directory}/{model_name}.py' - self.create_file_and_add_code_into_there(path, code) - self.module_path_map[model_name] = {'model': path} - - def add_type(self, code): - template_module_directory = os.path.join(self.template_root_directory, "typing") - template_model_directory = os.path.join(template_module_directory, ROUTE) - - path = f'{template_model_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - self.__create_model_folder(template_model_directory) - path = f'{template_model_directory}/typing.py' - self.create_file_and_add_code_into_there(path, code) - - def add_utils(self, code): - template_module_directory = os.path.join(self.template_root_directory, "find_query_builder") - template_model_directory = os.path.join(template_module_directory, ROUTE) - - path = f'{template_model_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - self.__create_model_folder(template_model_directory) - path = f'{template_model_directory}/find_query_builder.py' - self.create_file_and_add_code_into_there(path, code) - - def add_http_exception(self, code): - template_module_directory = os.path.join(self.template_root_directory, "http_exception") - template_model_directory = os.path.join(template_module_directory, ROUTE) - - path = f'{template_model_directory}/__init__.py' - self.create_file_and_add_code_into_there(path, "") - - self.__create_model_folder(template_model_directory) - path = f'{template_model_directory}/http_exception.py' - self.create_file_and_add_code_into_there(path, code) - - @staticmethod - def create_file_and_add_code_into_there(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - - @staticmethod - def add_to_controller_file(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - - - - diff --git a/src/fastapi_quickcrud_codegen/generator/model_template_generator.py b/src/fastapi_quickcrud_codegen/generator/model_template_generator.py deleted file mode 100644 index 174bef8..0000000 --- a/src/fastapi_quickcrud_codegen/generator/model_template_generator.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import sys - -from fastapi_quickcrud_codegen.misc.constant import GENERATION_FOLDER, MODEL - - -class ModelTemplateGenerator: - def __init__(self): - dirname, filename = os.path.split(os.path.abspath(sys.argv[0])) - self.current_directory = dirname - self.template_root_directory = os.path.join(self.current_directory, GENERATION_FOLDER) - self.module_path_map = {} - - def __create_root_template_folder(self): - if not os.path.exists(self.template_root_directory): - os.makedirs(self.template_root_directory) - - def __create_model_folder(self, path): - if not os.path.exists(path): - os.makedirs(path) - - def __create_module_folder(self): - if not os.path.exists(self.template_model_directory): - os.makedirs(self.template_model_directory) - - def add_model(self, model_name, code): - template_model_directory = os.path.join(self.template_root_directory, MODEL) - self.__create_model_folder(template_model_directory) - - path = f'{template_model_directory}/__init__.py' - self.add_code_to_file(path, "") - - path = f'{template_model_directory}/{model_name}.py' - self.add_code_to_file(path, code) - self.module_path_map[model_name] = {'model': path} - - @staticmethod - def add_code_to_file(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - - @staticmethod - def add_to_controller_file(path, code): - with open(path, 'a') as model_file: - model_file.write(code) - - -model_template_gen = ModelTemplateGenerator() - - diff --git a/src/fastapi_quickcrud_codegen/misc/__init__.py b/src/fastapi_quickcrud_codegen/misc/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_execute.py b/src/fastapi_quickcrud_codegen/misc/abstract_execute.py deleted file mode 100644 index 864fcc3..0000000 --- a/src/fastapi_quickcrud_codegen/misc/abstract_execute.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any - -from sqlalchemy.sql.elements import BinaryExpression - - -class SQLALchemyExecuteService(object): - - def __init__(self): - pass - - @staticmethod - def add(session, model) -> Any: - session.add(model) - - @staticmethod - def add_all(session, model) -> Any: - session.add_all(model) - - @staticmethod - async def async_flush(session) -> Any: - await session.flush() - - @staticmethod - def flush(session) -> Any: - session.flush() - - @staticmethod - async def async_execute(session, stmt: BinaryExpression) -> Any: - return await session.execute(stmt) - - @staticmethod - def execute(session, stmt: BinaryExpression) -> Any: - return session.execute(stmt) - diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_parser.py b/src/fastapi_quickcrud_codegen/misc/abstract_parser.py deleted file mode 100644 index b58de4d..0000000 --- a/src/fastapi_quickcrud_codegen/misc/abstract_parser.py +++ /dev/null @@ -1,375 +0,0 @@ -import copy -from http import HTTPStatus -from urllib.parse import urlencode -from pydantic import parse_obj_as -from starlette.responses import Response, RedirectResponse - -from .utils import group_find_many_join -from .exceptions import FindOneApiNotRegister - - -class SQLAlchemyGeneralSQLeResultParse(object): - - def __init__(self, async_model, crud_models): - - """ - :param async_model: bool - :param crud_models: pre ready - :param autocommit: bool - """ - - self.async_mode = async_model - self.crud_models = crud_models - self.primary_name = crud_models.PRIMARY_KEY_NAME - - async def async_commit(self, session): - await session.commit() - - def commit(self, session): - session.commit() - - async def async_delete(self, session, data): - await session.delete(data) - - def delete(self, session, data): - session.delete(data) - - def update_data_model(self, data, update_args): - for update_arg_name, update_arg_value in update_args.items(): - setattr(data, update_arg_name, update_arg_value) - return data - - @staticmethod - async def async_rollback(session): - await session.rollback() - - @staticmethod - def rollback(session): - session.rollback() - - @staticmethod - def _response_builder(sql_execute_result, fastapi_response, response_model): - result = parse_obj_as(response_model, sql_execute_result) - fastapi_response.headers["x-total-count"] = str(len(sql_execute_result) if isinstance(sql_execute_result, list) - else '1') - return result - - # async def async_update_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - # result = self._response_builder(sql_execute_result, fastapi_response, response_model) - # await self.async_commit(kwargs.get('session')) - # return result - # - # async def async_patch_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - # result = self._response_builder(sql_execute_result, fastapi_response, response_model) - # await self.async_commit(kwargs.get('session')) - # return result - # - # def patch_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - # result = self._response_builder(sql_execute_result, fastapi_response, response_model) - # self.commit(kwargs.get('session')) - # return result - - def update_func(self, response_model, sql_execute_result, fastapi_response, update_args, update_one): - if not isinstance(sql_execute_result, list): - sql_execute_result = [sql_execute_result] - tmp = [] - for i in sql_execute_result: - tmp.append(self.update_data_model(i, update_args=update_args)) - - if not update_one: - sql_execute_result = tmp - else: - sql_execute_result, = tmp - return self._response_builder(response_model=response_model, - sql_execute_result=sql_execute_result, - fastapi_response=fastapi_response) - - def update(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): - session = kwargs.get('session') - update_one = kwargs.get('update_one') - result = self.update_func(response_model, sql_execute_result, fastapi_response, update_args, update_one) - self.commit(session) - return result - - async def async_update(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): - session = kwargs.get('session') - update_one = kwargs.get('update_one') - result = self.update_func(response_model, sql_execute_result, fastapi_response, update_args, update_one) - await self.async_commit(session) - return result - - @staticmethod - def find_one_sub_func(sql_execute_result, response_model, fastapi_response, **kwargs): - join = kwargs.get('join_mode', None) - - one_row_data = sql_execute_result.fetchall() - if not one_row_data: - return Response('specific data not found', status_code=HTTPStatus.NOT_FOUND) - response = [] - for i in one_row_data: - i = dict(i) - result__ = copy.deepcopy(i) - tmp = {} - for key_, value_ in result__.items(): - if '_____' in key_: - key, foreign_column = key_.split('_____') - if key not in tmp: - tmp[key] = {foreign_column: value_} - else: - tmp[key][foreign_column] = value_ - else: - tmp[key_] = value_ - response.append(tmp) - if join: - response = group_find_many_join(response) - if isinstance(response, list): - response = response[0] - fastapi_response.headers["x-total-count"] = str(1) - return response - - async def async_find_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.find_one_sub_func(sql_execute_result, response_model, fastapi_response, **kwargs) - await self.async_commit(kwargs.get('session')) - return result - - def find_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.find_one_sub_func(sql_execute_result, response_model, fastapi_response, **kwargs) - self.commit(kwargs.get('session')) - return result - - @staticmethod - def find_many_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs): - join = kwargs.get('join_mode', None) - result = sql_execute_result.fetchall() - if not result: - return Response(status_code=HTTPStatus.NO_CONTENT) - response = [] - for i in result: - i = dict(i) - result__ = copy.deepcopy(i) - tmp = {} - for key_, value_ in result__.items(): - if '_____' in key_: - key, foreign_column = key_.split('_____') - if key not in tmp: - tmp[key] = {foreign_column: value_} - else: - tmp[key][foreign_column] = value_ - else: - tmp[key_] = value_ - response.append(tmp) - - fastapi_response.headers["x-total-count"] = str(len(response)) - if join: - response = group_find_many_join(response) - response = parse_obj_as(response_model, response) - return response - - async def async_find_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.find_many_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) - await self.async_commit(kwargs.get('session')) - return result - - def find_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.find_many_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) - self.commit(kwargs.get('session')) - return result - - # @staticmethod - # def update_one_sub_func(response_model, sql_execute_result, fastapi_response): - # result = parse_obj_as(response_model, sql_execute_result) - # fastapi_response.headers["x-total-count"] = str(1) - # return result - # - # async def async_update_one(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): - # session = kwargs.get('session') - # if not sql_execute_result: - # return Response(status_code=HTTPStatus.NOT_FOUND) - # data = self.update_data_model(sql_execute_result, update_args=update_args) - # result = self.update_one_sub_func(response_model, data, fastapi_response) - # await self.commit(session) - # return result - # - # def update_one(self, *, response_model, sql_execute_result, fastapi_response, update_args, **kwargs): - # session = kwargs.get('session') - # if not sql_execute_result: - # return Response(status_code=HTTPStatus.NOT_FOUND) - # data = self.update_data_model(sql_execute_result, update_args=update_args) - # result = self.update_one_sub_func(response_model, data, fastapi_response) - # self.commit(session) - # return result - - # async def async_patch_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - # result = self.update_one_sub_func(response_model, sql_execute_result, fastapi_response) - # await self.async_commit(kwargs.get('session')) - # return result - # - # def patch_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - # result = self.update_one_sub_func(response_model, sql_execute_result, fastapi_response) - # self.commit(kwargs.get('session')) - # return result - - @staticmethod - def create_one_sub_func(response_model, sql_execute_result, fastapi_response): - inserted_data, = sql_execute_result - result = parse_obj_as(response_model, inserted_data) - fastapi_response.headers["x-total-count"] = str(1) - return result - - async def async_create_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.create_one_sub_func(response_model, sql_execute_result, fastapi_response) - await self.async_commit(kwargs.get('session')) - return result - - def create_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.create_one_sub_func(response_model, sql_execute_result, fastapi_response) - self.commit(kwargs.get('session')) - return result - - @staticmethod - def create_many_sub_func(response_model, sql_execute_result, fastapi_response): - result = parse_obj_as(response_model, sql_execute_result) - fastapi_response.headers["x-total-count"] = str(len(sql_execute_result)) - return result - - async def async_create_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.create_many_sub_func(response_model, sql_execute_result, fastapi_response) - await self.async_commit(kwargs.get('session')) - return result - - def create_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.create_many_sub_func(response_model, sql_execute_result, fastapi_response) - self.commit(kwargs.get('session')) - return result - - @staticmethod - def upsert_one_sub_func(response_model, sql_execute_result, fastapi_response): - sql_execute_result = sql_execute_result.fetchone() - result = parse_obj_as(response_model, dict(sql_execute_result)) - fastapi_response.headers["x-total-count"] = str(1) - return result - - async def async_upsert_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.upsert_one_sub_func(response_model, sql_execute_result, fastapi_response) - await self.async_commit(kwargs.get('session')) - return result - - def upsert_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.upsert_one_sub_func(response_model, sql_execute_result, fastapi_response) - self.commit(kwargs.get('session')) - return result - - @staticmethod - def upsert_many_sub_func(response_model, sql_execute_result, fastapi_response): - insert_result_list = sql_execute_result.fetchall() - result = parse_obj_as(response_model, insert_result_list) - fastapi_response.headers["x-total-count"] = str(len(insert_result_list)) - return result - - async def async_upsert_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.upsert_many_sub_func(response_model, sql_execute_result, fastapi_response) - await self.async_commit(kwargs.get('session')) - return result - - def upsert_many(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - result = self.upsert_many_sub_func(response_model, sql_execute_result, fastapi_response) - self.commit(kwargs.get('session')) - return result - - def delete_one_sub_func(self, response_model, sql_execute_result, fastapi_response, **kwargs): - if not sql_execute_result: - return Response(status_code=HTTPStatus.NOT_FOUND) - result = parse_obj_as(response_model, sql_execute_result) - fastapi_response.headers["x-total-count"] = str(1) - return result - - def delete_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - session = kwargs.get('session') - if sql_execute_result: - self.delete(session, sql_execute_result) - result = self.delete_one_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) - self.commit(session) - return result - - async def async_delete_one(self, *, response_model, sql_execute_result, fastapi_response, **kwargs): - session = kwargs.get('session') - if sql_execute_result: - self.delete(session, sql_execute_result) - result = self.delete_one_sub_func(response_model, sql_execute_result, fastapi_response, **kwargs) - await self.async_commit(session) - return result - - def delete_many_sub_func(self, response_model, sql_execute_result, fastapi_response): - if not sql_execute_result: - return Response(status_code=HTTPStatus.NO_CONTENT) - deleted_rows = sql_execute_result - result = parse_obj_as(response_model, deleted_rows) - fastapi_response.headers["x-total-count"] = str(len(deleted_rows)) - return result - - def delete_many(self, *, response_model, sql_execute_results, fastapi_response, **kwargs): - session = kwargs.get('session') - if sql_execute_results: - for sql_execute_result in sql_execute_results: - self.delete(session, sql_execute_result) - result = self.delete_many_sub_func(response_model, sql_execute_results, fastapi_response) - self.commit(session) - return result - - async def async_delete_many(self, *, response_model, sql_execute_results, fastapi_response, **kwargs): - session = kwargs.get('session') - if sql_execute_results: - for sql_execute_result in sql_execute_results: - await self.async_delete(session, sql_execute_result) - result = self.delete_many_sub_func(response_model, sql_execute_results, fastapi_response) - await self.async_commit(session) - return result - - def has_end_point(self, fastapi_request) -> bool: - redirect_end_point = fastapi_request.url.path + "/{" + self.primary_name + "}" - redirect_url_exist = False - for route in fastapi_request.app.routes: - if route.path == redirect_end_point: - route_request_method, = route.methods - if route_request_method.upper() == 'GET': - redirect_url_exist = True - return redirect_url_exist - - def post_redirect_get_sub_func(self, response_model, sql_execute_result, fastapi_request): - result = parse_obj_as(response_model, sql_execute_result) - primary_key_field = result.__dict__.pop(self.primary_name, None) - assert primary_key_field is not None - redirect_url = fastapi_request.url.path + "/" + str(primary_key_field) - return redirect_url - - def get_post_redirect_get_url(self, response_model, sql_execute_result, fastapi_request): - redirect_url = self.post_redirect_get_sub_func(response_model, sql_execute_result, fastapi_request) - header_dict = {i[0].decode("utf-8"): i[1].decode("utf-8") for i in fastapi_request.headers.__dict__['_list']} - redirect_url += f'?{urlencode(header_dict)}' - return redirect_url - - async def async_post_redirect_get(self, *, response_model, sql_execute_result, fastapi_request, **kwargs): - session = kwargs['session'] - if not self.has_end_point(fastapi_request): - await self.async_rollback(session) - raise FindOneApiNotRegister(404, - f'End Point {fastapi_request.url.path}/{ {self.primary_name} }' - f' with GET method not found') - redirect_url = self.get_post_redirect_get_url(response_model, sql_execute_result, fastapi_request) - await self.async_commit(session) - return RedirectResponse(redirect_url, - status_code=HTTPStatus.SEE_OTHER - ) - - def post_redirect_get(self, *, response_model, sql_execute_result, fastapi_request, **kwargs): - session = kwargs['session'] - if not self.has_end_point(fastapi_request): - self.rollback(session) - raise FindOneApiNotRegister(404, - f'End Point {fastapi_request.url.path}/{ {self.primary_name} }' - f' with GET method not found') - redirect_url = self.get_post_redirect_get_url(response_model, sql_execute_result, fastapi_request) - self.commit(session) - return RedirectResponse(redirect_url, - status_code=HTTPStatus.SEE_OTHER - ) diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_query.py b/src/fastapi_quickcrud_codegen/misc/abstract_query.py deleted file mode 100644 index 89252f4..0000000 --- a/src/fastapi_quickcrud_codegen/misc/abstract_query.py +++ /dev/null @@ -1,412 +0,0 @@ -from abc import ABC -from typing import List, Union - -from sqlalchemy import and_, select, text -from sqlalchemy.dialects.postgresql import insert -from sqlalchemy.sql.elements import BinaryExpression -from sqlalchemy.sql.schema import Table - -from .exceptions import UnknownOrderType, UnknownColumn, UpdateColumnEmptyException -from .type import Ordering -from .utils import clean_input_fields, path_query_builder -from .utils import find_query_builder - - -class SQLAlchemyGeneralSQLQueryService(ABC): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - - """ - :param model: declarative_base model - :param async_mode: bool - """ - - self.model = model - self.model_columns = model - self.async_mode = async_mode - self.foreign_table_mapping = foreign_table_mapping - - def get_many(self, *, - join_mode, - query, - target_model=None, - abstract_param=None - ) -> BinaryExpression: - - filter_args = query - limit = filter_args.pop('limit', None) - offset = filter_args.pop('offset', None) - order_by_columns = filter_args.pop('order_by_columns', None) - model = self.model - if target_model: - model = self.foreign_table_mapping[target_model] - filter_list: List[BinaryExpression] = find_query_builder(param=filter_args, - model=model) - path_filter_list: List[BinaryExpression] = path_query_builder(params=abstract_param, - model=self.foreign_table_mapping) - join_table_instance_list: list = self.get_join_select_fields(join_mode) - - - if not isinstance(self.model, Table): - model = model.__table__ - - stmt = select(*[model] + join_table_instance_list).filter(and_(*filter_list+path_filter_list)) - if order_by_columns: - order_by_query_list = [] - - for order_by_column in order_by_columns: - if not order_by_column: - continue - sort_column, order_by = (order_by_column.replace(' ', '').split(':') + [None])[:2] - if not hasattr(self.model_columns, sort_column): - raise UnknownColumn(f'column {sort_column} is not exited') - if not order_by: - order_by_query_list.append(getattr(self.model_columns, sort_column).asc()) - elif order_by.upper() == Ordering.DESC.upper(): - order_by_query_list.append(getattr(self.model_columns, sort_column).desc()) - elif order_by.upper() == Ordering.ASC.upper(): - order_by_query_list.append(getattr(self.model_columns, sort_column).asc()) - else: - raise UnknownOrderType(f"Unknown order type {order_by}, only accept DESC or ASC") - if order_by_query_list: - stmt = stmt.order_by(*order_by_query_list) - stmt = stmt.limit(limit).offset(offset) - stmt = self.get_join_by_excpression(stmt, join_mode=join_mode) - return stmt - - def get_one(self, *, - extra_args: dict, - filter_args: dict, - ) -> BinaryExpression: - filter_list: List[BinaryExpression] = find_query_builder(param=filter_args, - model=self.model_columns) - - extra_query_expression: List[BinaryExpression] = find_query_builder(param=extra_args, - model=self.model) - model = self.model - if not isinstance(self.model, Table): - model = model.__table__ - stmt = select(*[model]).where(and_(*filter_list + extra_query_expression)) - return stmt - - def create(self, *, - insert_arg, - create_one=True, - ) -> List[BinaryExpression]: - insert_arg_dict: Union[list, dict] = insert_arg - if not create_one: - insert_arg_list: list = insert_arg_dict.pop('insert', None) - insert_arg_dict = [] - for i in insert_arg_list: - insert_arg_dict.append(i.__dict__) - if not isinstance(insert_arg_dict, list): - insert_arg_dict = [insert_arg_dict] - - insert_arg_dict: list[dict] = [clean_input_fields(model=self.model_columns, param=insert_arg) - for insert_arg in insert_arg_dict] - if isinstance(insert_arg_dict, list): - new_data = [] - for i in insert_arg_dict: - new_data.append(self.model(**i)) - return new_data - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError - - def insert_one(self, *, - insert_args) -> BinaryExpression: - insert_args = insert_args - update_columns = clean_input_fields(insert_args, - self.model_columns) - inserted_instance = self.model(**update_columns) - return inserted_instance - - def get_join_select_fields(self, join_mode=None): - join_table_instance_list = [] - if not join_mode: - return join_table_instance_list - for _, table_instance in join_mode.items(): - for local_reference in table_instance['local_reference_pairs_set']: - if 'exclude' in local_reference and local_reference['exclude']: - continue - for column in local_reference['reference_table_columns']: - foreign_table_name = local_reference['reference']['reference_table'] - join_table_instance_list.append( - column.label(foreign_table_name + '_foreign_____' + str(column).split('.')[1])) - return join_table_instance_list - - def get_join_by_excpression(self, stmt: BinaryExpression, join_mode=None) -> BinaryExpression: - if not join_mode: - return stmt - for join_table, data in join_mode.items(): - for local_reference in data['local_reference_pairs_set']: - local = local_reference['local']['local_column'] - reference = local_reference['reference']['reference_column'] - local_column = getattr(local_reference['local_table_columns'], local) - reference_column = getattr(local_reference['reference_table_columns'], reference) - table = local_reference['reference_table'] - stmt = stmt.join(table, local_column == reference_column) - return stmt - - # def delete(self, - # *, - # delete_args: dict, - # session, - # primary_key: dict = None, - # ) -> BinaryExpression: - # filter_list: List[BinaryExpression] = find_query_builder(param=delete_args, - # model=self.model_columns) - # if primary_key: - # filter_list += find_query_builder(param=primary_key, - # model=self.model_columns) - # - # delete_instance = session.query(self.model).where(and_(*filter_list)) - # return delete_instance - - def model_query(self, - *, - session, - extra_args: dict = None, - filter_args: dict = None, - ) -> BinaryExpression: - - ''' - used for delette and update - ''' - - filter_list: List[BinaryExpression] = find_query_builder(param=filter_args, - model=self.model_columns) - if extra_args: - filter_list += find_query_builder(param=extra_args, - model=self.model_columns) - stmt = select(self.model).where(and_(*filter_list)) - return stmt - - def get_one_with_foreign_pk(self, *, - join_mode, - query, - target_model, - abstract_param=None - ) -> BinaryExpression: - model = self.foreign_table_mapping[target_model] - filter_list: List[BinaryExpression] = find_query_builder(param=query, - model=model) - path_filter_list: List[BinaryExpression] = path_query_builder(params=abstract_param, - model=self.foreign_table_mapping) - join_table_instance_list: list = self.get_join_select_fields(join_mode) - - if not isinstance(self.model, Table): - model = model.__table__ - - stmt = select(*[model] + join_table_instance_list).filter(and_(*filter_list + path_filter_list)) - - stmt = self.get_join_by_excpression(stmt, join_mode=join_mode) - return stmt - - - # def update(self, *, - # update_args, - # extra_query, - # session, - # primary_key=None, - # ) -> BinaryExpression: - # - # - # filter_list: List[BinaryExpression] = find_query_builder(param=extra_query, - # model=self.model_columns) - # if primary_key: - # primary_key = primary_key - # filter_list += find_query_builder(param=primary_key, model=self.model_columns) - # update_stmt = update(self.model).where(and_(*filter_list)).values(update_args) - # update_stmt = update_stmt.execution_options(synchronize_session=False) - # return update_stmt - - -class SQLAlchemyPGSQLQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - - """ - :param model: declarative_base model - :param async_mode: bool - """ - super(SQLAlchemyPGSQLQueryService, - self).__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - insert_arg_dict: Union[list, dict] = insert_arg - - insert_with_conflict_handle = insert_arg_dict.pop('on_conflict', None) - if not upsert_one: - insert_arg_list: list = insert_arg_dict.pop('insert', None) - insert_arg_dict = [] - for i in insert_arg_list: - insert_arg_dict.append(i.__dict__) - - if not isinstance(insert_arg_dict, list): - insert_arg_dict: list[dict] = [insert_arg_dict] - insert_arg_dict: list[dict] = [clean_input_fields(model=self.model_columns, param=insert_arg) - for insert_arg in insert_arg_dict] - insert_stmt = insert(self.model).values(insert_arg_dict) - - if unique_fields and insert_with_conflict_handle: - update_columns = clean_input_fields(insert_with_conflict_handle.__dict__.get('update_columns', None), - self.model_columns) - if not update_columns: - raise UpdateColumnEmptyException('update_columns parameter must be a non-empty list ') - conflict_update_dict = {} - for columns in update_columns: - conflict_update_dict[columns] = getattr(insert_stmt.excluded, columns) - - conflict_list = clean_input_fields(model=self.model_columns, param=unique_fields) - conflict_update_dict = clean_input_fields(model=self.model_columns, param=conflict_update_dict) - insert_stmt = insert_stmt.on_conflict_do_update(index_elements=conflict_list, - set_=conflict_update_dict - ) - insert_stmt = insert_stmt.returning(text('*')) - return insert_stmt - - -class SQLAlchemySQLITEQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - """ - :param model: declarative_base model - :param async_mode: bool - """ - super().__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError - - -class SQLAlchemyMySQLQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - """ - :param model: declarative_base model - :param async_mode: bool - """ - super().__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError - - -class SQLAlchemyMariaDBQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - """ - :param model: declarative_base model - :param async_mode: bool - """ - super().__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError - - -class SQLAlchemyOracleQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - """ - :param model: declarative_base model - :param async_mode: bool - """ - super().__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError - - -class SQLAlchemyMSSqlQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - """ - :param model: declarative_base model - :param async_mode: bool - """ - super().__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError - - -class SQLAlchemyNotSupportQueryService(SQLAlchemyGeneralSQLQueryService): - - def __init__(self, *, model, async_mode, foreign_table_mapping): - """ - :param model: declarative_base model - :param async_mode: bool - """ - super().__init__(model=model, - async_mode=async_mode, - foreign_table_mapping=foreign_table_mapping) - self.model = model - self.model_columns = model - self.async_mode = async_mode - - def upsert(self, *, - insert_arg, - unique_fields: List[str], - upsert_one=True, - ) -> BinaryExpression: - raise NotImplementedError diff --git a/src/fastapi_quickcrud_codegen/misc/abstract_route.py b/src/fastapi_quickcrud_codegen/misc/abstract_route.py deleted file mode 100644 index 0f78d3a..0000000 --- a/src/fastapi_quickcrud_codegen/misc/abstract_route.py +++ /dev/null @@ -1,1281 +0,0 @@ -from abc import abstractmethod, ABC -from http import HTTPStatus -from typing import Union - -from fastapi import \ - Depends, \ - Response -from sqlalchemy.exc import IntegrityError -from starlette.requests import Request - - -class SQLAlchemyGeneralSQLBaseRouteSource(ABC): - """ This route will support the SQL SQLAlchemy dialects. """ - - @classmethod - def find_one(cls, api, - *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - dependencies, - request_url_param_model, - request_query_model, - db_session): - - if not async_mode: - @api.get(path, status_code=200, response_model=response_model, dependencies=dependencies) - def get_one_by_primary_key(response: Response, - request: Request, - url_param=Depends(request_url_param_model), - query=Depends(request_query_model), - session=Depends(db_session)): - - join = query.__dict__.pop('join_foreign_table', None) - stmt = query_service.get_one(filter_args=query.__dict__, - extra_args=url_param.__dict__, - join_mode=join) - query_result = execute_service.execute(session, stmt) - response_result = parsing_service.find_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session, - join_mode=join) - return response_result - else: - @api.get(path, status_code=200, response_model=response_model, dependencies=dependencies) - async def async_get_one_by_primary_key(response: Response, - request: Request, - url_param=Depends(request_url_param_model), - query=Depends(request_query_model), - session=Depends(db_session)): - - join = query.__dict__.pop('join_foreign_table', None) - stmt = query_service.get_one(filter_args=query.__dict__, - extra_args=url_param.__dict__, - join_mode=join) - query_result = await execute_service.async_execute(session, stmt) - - response_result = await parsing_service.async_find_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session, - join_mode=join) - return response_result - - @classmethod - def find_many(cls, api, *, - query_service, - parsing_service, - execute_service, - async_mode, - path, - response_model, - dependencies, - request_query_model, - db_session): - - if async_mode: - @api.get(path, dependencies=dependencies, response_model=response_model) - async def async_get_many(response: Response, - request: Request, - query=Depends(request_query_model), - session=Depends( - db_session) - ): - join = query.__dict__.pop('join_foreign_table', None) - stmt = query_service.get_many(query=query.__dict__, join_mode=join) - - query_result = await execute_service.async_execute(session, stmt) - - parsed_response = await parsing_service.async_find_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - join_mode=join, - session=session) - return parsed_response - else: - @api.get(path, dependencies=dependencies, response_model=response_model) - def get_many(response: Response, - request: Request, - query=Depends(request_query_model), - session=Depends( - db_session) - ): - join = query.__dict__.pop('join_foreign_table', None) - - stmt = query_service.get_many(query=query.__dict__, join_mode=join) - query_result = execute_service.execute(session, stmt) - parsed_response = parsing_service.find_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - join_mode=join, - session=session) - return parsed_response - - @abstractmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - raise NotImplementedError - - @abstractmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - - raise NotImplementedError - - @classmethod - def create_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - if async_mode: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - async def async_insert_one( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - # stmt = query_service.create(insert_arg=query) - - new_inserted_data = query_service.create(insert_arg=query.__dict__) - - execute_service.add_all(session, new_inserted_data) - try: - await execute_service.async_flush(session) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return await parsing_service.async_create_one(response_model=response_model, - sql_execute_result=new_inserted_data, - fastapi_response=response, - session=session) - else: - - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - def insert_one( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - - new_inserted_data = query_service.create(insert_arg=query.__dict__) - - execute_service.add_all(session, new_inserted_data) - - try: - execute_service.flush(session) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return parsing_service.create_one(response_model=response_model, - sql_execute_result=new_inserted_data, - fastapi_response=response, - session=session) - - @classmethod - def create_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - - if async_mode: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - async def async_insert_many( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - inserted_data = query_service.create(insert_arg=query.__dict__, - create_one=False) - - execute_service.add_all(session, inserted_data) - - try: - await execute_service.async_flush(session) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return await parsing_service.async_create_many(response_model=response_model, - sql_execute_result=inserted_data, - fastapi_response=response, - session=session) - else: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - def insert_many( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - - # inserted_data = query.__dict__['insert'] - update_list = query.__dict__ - inserted_data = query_service.create(insert_arg=update_list, - create_one=False) - - execute_service.add_all(session, inserted_data) - - try: - execute_service.flush(session) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return parsing_service.create_many(response_model=response_model, - sql_execute_result=inserted_data, - fastapi_response=response, - session=session) - - @classmethod - def delete_one(cls, api, *, - query_service, - parsing_service, - execute_service, - async_mode, - path, - response_model, - dependencies, - request_query_model, - request_url_model, - db_session, ): - - if async_mode: - @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) - async def async_delete_one_by_primary_key(response: Response, - request: Request, - query=Depends(request_query_model), - request_url_param_model=Depends(request_url_model), - session=Depends(db_session)): - # delete_instance = query_service.model_query( - # filter_args=request_url_param_model.__dict__, - # extra_args=query.__dict__, - # session=session) - filter_stmt = query_service.model_query(filter_args=request_url_param_model.__dict__, - extra_args=query.__dict__, - session=session) - - tmp = await session.execute(filter_stmt) - delete_instance = tmp.scalar() - - return await parsing_service.async_delete_one(response_model=response_model, - sql_execute_result=delete_instance, - fastapi_response=response, - session=session) - - else: - @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) - def delete_one_by_primary_key(response: Response, - request: Request, - query=Depends(request_query_model), - request_url_param_model=Depends(request_url_model), - session=Depends(db_session)): - filter_stmt = query_service.model_query(filter_args=request_url_param_model.__dict__, - extra_args=query.__dict__, - session=session) - delete_instance = session.execute(filter_stmt).scalar() - - return parsing_service.delete_one(response_model=response_model, - sql_execute_result=delete_instance, - fastapi_response=response, - session=session) - - @classmethod - def delete_many(cls, api, *, - query_service, - parsing_service, - execute_service, - async_mode, - path, - response_model, - dependencies, - request_query_model, - db_session): - if async_mode: - @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) - async def async_delete_many_by_query(response: Response, - request: Request, - query=Depends(request_query_model), - session=Depends(db_session)): - filter_stmt = query_service.model_query(filter_args=query.__dict__, - session=session) - - tmp = await session.execute(filter_stmt) - data_instance = [i for i in tmp.scalars()] - return await parsing_service.async_delete_many(response_model=response_model, - sql_execute_results=data_instance, - fastapi_response=response, - session=session) - else: - - @api.delete(path, status_code=200, response_model=response_model, dependencies=dependencies) - def delete_many_by_query(response: Response, - request: Request, - query=Depends(request_query_model), - session=Depends(db_session)): - filter_stmt = query_service.model_query(filter_args=query.__dict__, - session=session) - - delete_instance = [i for i in session.execute(filter_stmt).scalars()] - - return parsing_service.delete_many(response_model=response_model, - sql_execute_results=delete_instance, - fastapi_response=response, - session=session) - - @classmethod - def post_redirect_get(cls, api, *, - dependencies, - request_body_model, - db_session, - crud_service, - result_parser, - execute_service, - async_mode, - response_model): - if async_mode: - @api.post("", status_code=303, response_class=Response, dependencies=dependencies) - async def async_create_one_and_redirect_to_get_one_api_with_primary_key( - request: Request, - insert_args: request_body_model = Depends(), - session=Depends(db_session), - ): - new_inserted_data = crud_service.insert_one(insert_args=insert_args.__dict__) - - execute_service.add(session, new_inserted_data) - try: - await execute_service.async_flush(session) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return await result_parser.async_post_redirect_get(response_model=response_model, - sql_execute_result=new_inserted_data, - fastapi_request=request, - session=session) - else: - @api.post("", status_code=303, response_class=Response, dependencies=dependencies) - def create_one_and_redirect_to_get_one_api_with_primary_key( - request: Request, - insert_args: request_body_model = Depends(), - session=Depends(db_session), - ): - - new_inserted_data = crud_service.insert_one(insert_args=insert_args.__dict__) - - execute_service.add(session, new_inserted_data) - try: - execute_service.flush(session) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - - return result_parser.post_redirect_get(response_model=response_model, - sql_execute_result=new_inserted_data, - fastapi_request=request, - session=session) - - @classmethod - def patch_one(cls, api, *, - path, - response_model, - dependencies, - request_url_param_model, - request_body_model, - request_query_model, - execute_service, - db_session, - crud_service, - result_parser, - async_mode): - if async_mode: - - @api.patch(path, - status_code=200, - response_model=Union[response_model], - dependencies=dependencies) - async def async_partial_update_one_by_primary_key( - response: Response, - primary_key: request_url_param_model = Depends(), - patch_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session), - ): - filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, - extra_args=extra_query.__dict__, - session=session) - - data_instance = await session.execute(filter_stmt) - data_instance = data_instance.scalar() - - try: - return await result_parser.async_update(response_model=response_model, - sql_execute_result=data_instance, - update_args=patch_data.__dict__, - fastapi_response=response, - session=session, - update_one=True) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - else: - @api.patch(path, - status_code=200, - response_model=Union[response_model], - dependencies=dependencies) - def partial_update_one_by_primary_key( - response: Response, - primary_key: request_url_param_model = Depends(), - patch_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session), - ): - filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, - extra_args=extra_query.__dict__, - session=session) - - update_instance = session.execute(filter_stmt).scalar() - - try: - return result_parser.update(response_model=response_model, - sql_execute_result=update_instance, - update_args=patch_data.__dict__, - fastapi_response=response, - session=session, - update_one=True) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - - @classmethod - def patch_many(cls, api, *, - path, - response_model, - dependencies, - request_body_model, - request_query_model, - db_session, - crud_service, - result_parser, - execute_service, - async_mode): - if async_mode: - @api.patch(path, - status_code=200, - response_model=response_model, - dependencies=dependencies) - async def async_partial_update_many_by_query( - response: Response, - patch_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session) - ): - - filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, - session=session) - - tmp = await session.execute(filter_stmt) - data_instance = [i for i in tmp.scalars()] - - if not data_instance: - return Response(status_code=HTTPStatus.NO_CONTENT) - try: - return await result_parser.async_update(response_model=response_model, - sql_execute_result=data_instance, - fastapi_response=response, - update_args=patch_data.__dict__, - session=session, - update_one=False) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - else: - @api.patch(path, - status_code=200, - response_model=response_model, - dependencies=dependencies) - def partial_update_many_by_query( - response: Response, - patch_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session) - ): - filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, - session=session) - - data_instance = [i for i in session.execute(filter_stmt).scalars()] - - if not data_instance: - return Response(status_code=HTTPStatus.NO_CONTENT) - try: - return result_parser.update(response_model=response_model, - sql_execute_result=data_instance, - fastapi_response=response, - update_args=patch_data.__dict__, - session=session, - update_one=False) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - - @classmethod - def put_one(cls, api, *, - path, - request_url_param_model, - request_body_model, - response_model, - dependencies, - request_query_model, - db_session, - crud_service, - result_parser, - execute_service, - async_mode): - if async_mode: - @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) - async def async_entire_update_by_primary_key( - response: Response, - primary_key: request_url_param_model = Depends(), - update_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session), - ): - filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, - extra_args=extra_query.__dict__, - session=session) - - data_instance = await session.execute(filter_stmt) - data_instance = data_instance.scalar() - - if not data_instance: - return Response(status_code=HTTPStatus.NOT_FOUND) - try: - return await result_parser.async_update(response_model=response_model, - sql_execute_result=data_instance, - fastapi_response=response, - update_args=update_data.__dict__, - session=session, - update_one=True) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - else: - @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) - def entire_update_by_primary_key( - response: Response, - primary_key: request_url_param_model = Depends(), - update_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session), - ): - filter_stmt = crud_service.model_query(filter_args=primary_key.__dict__, - extra_args=extra_query.__dict__, - session=session) - - data_instance = session.execute(filter_stmt).scalar() - - if not data_instance: - return Response(status_code=HTTPStatus.NOT_FOUND) - try: - return result_parser.update(response_model=response_model, - sql_execute_result=data_instance, - fastapi_response=response, - update_args=update_data.__dict__, - session=session, - update_one=True) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - - @classmethod - def put_many(cls, api, *, - path, - response_model, - dependencies, - request_query_model, - request_body_model, - db_session, - crud_service, - result_parser, - execute_service, - async_mode): - if async_mode: - @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) - async def async_entire_update_many_by_query( - response: Response, - update_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session), - ): - filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, - session=session) - tmp = await session.execute(filter_stmt) - data_instance = [i for i in tmp.scalars()] - - if not data_instance: - return Response(status_code=HTTPStatus.NO_CONTENT) - try: - return await result_parser.async_update(response_model=response_model, - sql_execute_result=data_instance, - fastapi_response=response, - update_args=update_data.__dict__, - session=session, - update_one=False) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - - else: - @api.put(path, status_code=200, response_model=response_model, dependencies=dependencies) - def entire_update_many_by_query( - response: Response, - update_data: request_body_model = Depends(), - extra_query: request_query_model = Depends(), - session=Depends(db_session), - ): - - filter_stmt = crud_service.model_query(filter_args=extra_query.__dict__, - session=session) - - data_instance = [i for i in session.execute(filter_stmt).scalars()] - - if not data_instance: - return Response(status_code=HTTPStatus.NO_CONTENT) - try: - return result_parser.update(response_model=response_model, - sql_execute_result=data_instance, - fastapi_response=response, - update_args=update_data.__dict__, - session=session, - update_one=False) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - - # return result_parser.update_many(response_model=response_model, - # sql_execute_result=query_result, - # fastapi_response=response, - # session=session) - - @classmethod - def find_one_foreign_tree(cls, api, *, - query_service, - parsing_service, - execute_service, - async_mode, - path, - response_model, - dependencies, - request_query_model, - request_url_param_model, - function_name, - db_session): - - if async_mode: - @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) - async def async_get_one_with_foreign_tree(response: Response, - request: Request, - url_param=Depends(request_url_param_model), - query=Depends(request_query_model), - session=Depends( - db_session) - ): - target_model = request.url.path.split("/")[-2] - join = query.__dict__.pop('join_foreign_table', None) - stmt = query_service.get_one_with_foreign_pk(query=query.__dict__, - join_mode=join, - abstract_param=url_param.__dict__, - target_model=target_model) - - query_result = await execute_service.async_execute(session, stmt) - - parsed_response = await parsing_service.async_find_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - join_mode=join, - session=session) - return parsed_response - else: - @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) - def get_one_with_foreign_tree(response: Response, - request: Request, - url_param=Depends(request_url_param_model), - query=Depends(request_query_model), - session=Depends( - db_session) - ): - target_model = request.url.path.split("/")[-2] - join = query.__dict__.pop('join_foreign_table', None) - - stmt = query_service.get_one_with_foreign_pk(query=query.__dict__, - join_mode=join, - abstract_param=url_param.__dict__, - target_model=target_model) - query_result = execute_service.execute(session, stmt) - parsed_response = parsing_service.find_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - join_mode=join, - session=session) - return parsed_response - - @classmethod - def find_many_foreign_tree(cls, api, *, - query_service, - parsing_service, - execute_service, - async_mode, - path, - response_model, - dependencies, - request_query_model, - request_url_param_model, - function_name, - db_session): - - if async_mode: - @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) - async def async_get_many_with_foreign_tree(response: Response, - request: Request, - url_param=Depends(request_url_param_model), - query=Depends(request_query_model), - session=Depends( - db_session) - ): - target_model = request.url.path.split("/")[-1] - join = query.__dict__.pop('join_foreign_table', None) - stmt = query_service.get_many(query=query.__dict__, join_mode=join, abstract_param=url_param.__dict__, - target_model=target_model) - - query_result = await execute_service.async_execute(session, stmt) - - parsed_response = await parsing_service.async_find_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - join_mode=join, - session=session) - return parsed_response - else: - @api.get(path, dependencies=dependencies, response_model=response_model, name=function_name) - def get_many_with_foreign_tree(response: Response, - request: Request, - url_param=Depends(request_url_param_model), - query=Depends(request_query_model), - session=Depends( - db_session) - ): - target_model = request.url.path.split("/")[-1] - join = query.__dict__.pop('join_foreign_table', None) - stmt = query_service.get_many(query=query.__dict__, join_mode=join, abstract_param=url_param.__dict__, - target_model=target_model) - query_result = execute_service.execute(session, stmt) - parsed_response = parsing_service.find_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - join_mode=join, - session=session) - - return parsed_response - - -class SQLAlchemyPGSQLRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - if async_mode: - - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - async def async_insert_one_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list) - - try: - query_result = await execute_service.async_execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) - return result - return await parsing_service.async_upsert_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - else: - - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - def insert_one_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list) - try: - query_result = execute_service.execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) - return result - return parsing_service.upsert_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - - if async_mode: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - async def async_insert_many_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list, - upsert_one=False) - try: - query_result = await execute_service.async_execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) - return result - return await parsing_service.async_upsert_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - else: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - def insert_many_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list, - upsert_one=False) - try: - query_result = execute_service.execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT, content=err_msg) - return result - return parsing_service.upsert_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - - -class SQLAlchemySQLLiteRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - if async_mode: - - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - async def async_insert_one_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list) - - try: - query_result = await execute_service.async_execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return await parsing_service.async_upsert_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - else: - - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - def insert_one_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list) - try: - query_result = execute_service.execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return parsing_service.upsert_one(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - - if async_mode: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - async def async_insert_many_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list, - upsert_one=False) - try: - query_result = await execute_service.async_execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return await parsing_service.async_upsert_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - else: - @api.post(path, status_code=201, response_model=response_model, dependencies=dependencies) - def insert_many_and_support_upsert( - response: Response, - request: Request, - query: request_body_model = Depends(request_body_model), - session=Depends(db_session) - ): - - stmt = query_service.upsert(insert_arg=query.__dict__, - unique_fields=unique_list, - upsert_one=False) - try: - query_result = execute_service.execute(session, stmt) - except IntegrityError as e: - err_msg, = e.orig.args - if 'unique constraint' not in err_msg.lower(): - raise e - result = Response(status_code=HTTPStatus.CONFLICT) - return result - return parsing_service.upsert_many(response_model=response_model, - sql_execute_result=query_result, - fastapi_response=response, - session=session) - - -class SQLAlchemyMySQLRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - raise NotImplementedError - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - raise NotImplementedError - - -class SQLAlchemyMariadbRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - raise NotImplementedError - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - raise NotImplementedError - - -class SQLAlchemyOracleRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - raise NotImplementedError - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - raise NotImplementedError - - -class SQLAlchemyMSSQLRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - raise NotImplementedError - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - raise NotImplementedError - - -class SQLAlchemyNotSupportRouteSource(SQLAlchemyGeneralSQLBaseRouteSource): - ''' - This route will support the SQL SQLAlchemy dialects - ''' - - @classmethod - def upsert_one(cls, api, *, - path, - query_service, - parsing_service, - execute_service, - async_mode, - response_model, - request_body_model, - dependencies, - db_session, - unique_list): - raise NotImplementedError - - @classmethod - def upsert_many(cls, api, *, - query_service, - parsing_service, - async_mode, - path, - response_model, - dependencies, - request_body_model, - db_session, - unique_list, - execute_service): - raise NotImplementedError diff --git a/src/fastapi_quickcrud_codegen/misc/constant.py b/src/fastapi_quickcrud_codegen/misc/constant.py deleted file mode 100644 index d243385..0000000 --- a/src/fastapi_quickcrud_codegen/misc/constant.py +++ /dev/null @@ -1,4 +0,0 @@ -GENERATION_FOLDER = "fastapi_quick_crud_template" -MODEL = "model" -ROUTE = "route" -COMMON = "common" diff --git a/src/fastapi_quickcrud_codegen/misc/covert_model.py b/src/fastapi_quickcrud_codegen/misc/covert_model.py deleted file mode 100644 index f9a15b3..0000000 --- a/src/fastapi_quickcrud_codegen/misc/covert_model.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.sql.schema import Table - - -def convert_table_to_model(db_model): - NO_PRIMARY_KEY = False - if not isinstance(db_model, Table): - return db_model, NO_PRIMARY_KEY - db_name = str(db_model.fullname) - table_dict = {'__table__': db_model, - '__tablename__': db_name} - - if not db_model.primary_key: - table_dict['__mapper_args__'] = { - "primary_key": [i for i in db_model._columns] - } - NO_PRIMARY_KEY = True - - for i in db_model.c: - col, = i.expression.base_columns - table_dict[str(i.key)] = col - - return type(f'{db_name}DeclarativeBaseClass', (declarative_base(),), table_dict), NO_PRIMARY_KEY diff --git a/src/fastapi_quickcrud_codegen/misc/crud_model.py b/src/fastapi_quickcrud_codegen/misc/crud_model.py deleted file mode 100644 index 8019287..0000000 --- a/src/fastapi_quickcrud_codegen/misc/crud_model.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import (Optional, - Dict, - List) - -from pydantic import BaseModel -from pydantic.main import ModelMetaclass - -from .exceptions import (RequestMissing, - InvalidRequestMethod) -from .type import CrudMethods - - -class RequestResponseModel(BaseModel): - requestUrlParamModel: Optional[str] - requestRelationshipUrlParamField: Optional[List[str]] - requestQueryModel: Optional[str] - requestBodyModel: Optional[str] - responseModel: Optional[str] - jsonRequestFieldModel: Optional[str] - jsonbRequestFieldModel: Optional[str] - arrayRequestFieldModel: Optional[str] - foreignListModel: Optional[List[dict]] - - -class CRUDModel(BaseModel): - GET: Optional[Dict[CrudMethods, bool]] - POST: Optional[Dict[CrudMethods, bool]] - PUT: Optional[Dict[CrudMethods, bool]] - PATCH: Optional[Dict[CrudMethods, bool]] - DELETE: Optional[Dict[CrudMethods, bool]] - PRIMARY_KEY_NAME: Optional[str] - UNIQUE_LIST: Optional[List[str]] - - def get_available_request_method(self): - return [i for i in self.dict(exclude_unset=True, ).keys() if i in ["GET", "POST", "PUT", "PATCH", "DELETE"]] - - def get_model_by_request_method(self, request_method): - available_methods = self.dict() - if request_method not in available_methods.keys(): - raise InvalidRequestMethod(f'{request_method} is not an available request method') - if not available_methods[request_method]: - raise RequestMissing( - f'{request_method} is not available, ' - f'make sure the CRUDModel contains this request method') - _ = available_methods[request_method] - return _ diff --git a/src/fastapi_quickcrud_codegen/misc/exceptions.py b/src/fastapi_quickcrud_codegen/misc/exceptions.py deleted file mode 100644 index a6b87a9..0000000 --- a/src/fastapi_quickcrud_codegen/misc/exceptions.py +++ /dev/null @@ -1,85 +0,0 @@ -from fastapi import HTTPException - - -class FindOneApiNotRegister(HTTPException): - pass - - -class CRUDBuilderException(BaseException): - pass - - -class RequestMissing(CRUDBuilderException): - pass - - -class PrimaryMissing(CRUDBuilderException): - pass - - -class UnknownOrderType(CRUDBuilderException): - pass - - -class UpdateColumnEmptyException(CRUDBuilderException): - pass - - -class UnknownColumn(CRUDBuilderException): - pass - - -class QueryOperatorNotFound(CRUDBuilderException): - pass - - -class UnknownError(CRUDBuilderException): - pass - - -class ConflictColumnsCannotHit(CRUDBuilderException): - pass - - -class MultipleSingleUniqueNotSupportedException(CRUDBuilderException): - pass - - -class SchemaException(CRUDBuilderException): - pass - - -class CompositePrimaryKeyConstraintNotSupportedException(CRUDBuilderException): - pass - - -class MultiplePrimaryKeyNotSupportedException(CRUDBuilderException): - pass - - -class ColumnTypeNotSupportedException(CRUDBuilderException): - pass - - -class InvalidRequestMethod(CRUDBuilderException): - pass - - -# -# class NotFoundError(MongoQueryError): -# def __init__(self, Collection: Type[ModelType], model: BaseModel): -# detail = "does not exist" -# super().__init__(Collection, model, detail) -# -# -# -# class DuplicatedError(MongoQueryError): -# def __init__(self, Collection: Type[ModelType], model: BaseModel): -# detail = "was already existed" -# super().__init__(Collection, model, detail) - -class FDDRestHTTPException(HTTPException): - """Baseclass for all HTTP exceptions in FDD Rest API. This exception can be called as WSGI - application to render a default error page or you can catch the subclasses - of it independently and render nicer error messages. - """ diff --git a/src/fastapi_quickcrud_codegen/misc/get_table_name.py b/src/fastapi_quickcrud_codegen/misc/get_table_name.py deleted file mode 100644 index f404ead..0000000 --- a/src/fastapi_quickcrud_codegen/misc/get_table_name.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import Table - - -def get_table_name_from_table(table): - return table.name - - -def get_table_name_from_model(table): - return table.__tablename__ - - -def get_table_name(table): - if isinstance(table, Table): - return get_table_name_from_table(table) - else: - return get_table_name_from_model(table) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/misc/memory_sql.py b/src/fastapi_quickcrud_codegen/misc/memory_sql.py deleted file mode 100644 index 6438b24..0000000 --- a/src/fastapi_quickcrud_codegen/misc/memory_sql.py +++ /dev/null @@ -1,70 +0,0 @@ -import asyncio -import string -import random -from typing import Generator - -from sqlalchemy import create_engine -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import declarative_base, sessionmaker -from sqlalchemy.pool import StaticPool - - -class MemorySql(): - def __init__(self, async_mode: bool = False): - """ - - @type async_mode: bool - used to build sync or async memory sql connection - """ - self.async_mode = async_mode - SQLALCHEMY_DATABASE_URL = f"sqlite{'+aiosqlite' if async_mode else ''}://" - if not async_mode: - self.engine = create_engine(SQLALCHEMY_DATABASE_URL, - future=True, - echo=True, - pool_pre_ping=True, - pool_recycle=7200, - connect_args={"check_same_thread": False}, - poolclass=StaticPool) - self.sync_session = sessionmaker(bind=self.engine, - autocommit=False, ) - else: - self.engine = create_async_engine(SQLALCHEMY_DATABASE_URL, - future=True, - echo=True, - pool_pre_ping=True, - pool_recycle=7200, - connect_args={"check_same_thread": False}, - poolclass=StaticPool) - self.sync_session = sessionmaker(autocommit=False, - autoflush=False, - bind=self.engine, - class_=AsyncSession) - - def create_memory_table(self, Mode: 'declarative_base()'): - if not self.async_mode: - Mode.__table__.create(self.engine, checkfirst=True) - else: - async def create_table(engine, model): - async with engine.begin() as conn: - await conn.run_sync(model._sa_registry.metadata.create_all) - - loop = asyncio.get_event_loop() - loop.run_until_complete(create_table(self.engine, Mode)) - - def get_memory_db_session(self) -> Generator: - try: - db = self.sync_session() - yield db - except Exception as e: - db.rollback() - raise e - finally: - db.close() - - async def async_get_memory_db_session(self): - async with self.sync_session() as session: - yield session - -async_memory_db = MemorySql(True) -sync_memory_db = MemorySql() \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/misc/schema_builder.py b/src/fastapi_quickcrud_codegen/misc/schema_builder.py deleted file mode 100644 index eda959c..0000000 --- a/src/fastapi_quickcrud_codegen/misc/schema_builder.py +++ /dev/null @@ -1,1124 +0,0 @@ -import uuid -import warnings -from copy import deepcopy -from dataclasses import (make_dataclass) -from enum import auto -from typing import (Optional, - Any) -from typing import (Type, - Dict, - List, - Tuple, - TypeVar, - NewType, - Union) - -import pydantic -from fastapi import (Body, - Query) -from pydantic import (BaseModel, - create_model, - BaseConfig) -from pydantic.dataclasses import dataclass as pydantic_dataclass -from sqlalchemy import UniqueConstraint, Table, Column -from sqlalchemy import inspect -from sqlalchemy.orm import DeclarativeMeta -from sqlalchemy.orm import declarative_base -from strenum import StrEnum - -from fastapi_quickcrud_codegen.generator.model_template_generator import model_template_gen -from fastapi_quickcrud_codegen.misc.covert_model import convert_table_to_model -from fastapi_quickcrud_codegen.misc.exceptions import (SchemaException, - ColumnTypeNotSupportedException) -from fastapi_quickcrud_codegen.misc.get_table_name import get_table_name -from fastapi_quickcrud_codegen.misc.type import (Ordering, - ExtraFieldTypePrefix, - ExtraFieldType, - SqlType, ) -from fastapi_quickcrud_codegen.model.model_builder import ModelCodeGen - -FOREIGN_PATH_PARAM_KEYWORD = "__pk__" -BaseModelT = TypeVar('BaseModelT', bound=BaseModel) -DataClassT = TypeVar('DataClassT', bound=Any) -DeclarativeClassT = NewType('DeclarativeClassT', declarative_base) -TableNameT = NewType('TableNameT', str) -ResponseModelT = NewType('ResponseModelT', BaseModel) -ForeignKeyName = NewType('ForeignKeyName', str) -TableInstance = NewType('TableInstance', Table) - - -class ExcludeUnsetBaseModel(BaseModel): - def dict(self, *args, **kwargs): - if kwargs and kwargs.get("exclude_none") is not None: - kwargs["exclude_unset"] = True - return BaseModel.dict(self, *args, **kwargs) - - -class OrmConfig(BaseConfig): - orm_mode = True - - -def _add_orm_model_config_into_pydantic_model(pydantic_model, **kwargs) -> BaseModelT: - validators = kwargs.get('validators', None) - config = kwargs.get('config', None) - field_definitions = { - name_: (field_.outer_type_, field_.field_info.default) - for name_, field_ in pydantic_model.__fields__.items() - } - return create_model(f'{pydantic_model.__name__}WithValidators', - **field_definitions, - __config__=config, - __validators__=validators) - -def _model_from_dataclass(kls: DataClassT) -> Type[BaseModel]: - """ Converts a stdlib dataclass to a pydantic BaseModel. """ - - return pydantic_dataclass(kls).__pydantic_model__ - - -def _to_require_but_default(model: Type[BaseModelT]) -> Type[BaseModelT]: - """ - Create a new BaseModel with the exact same fields as `model` - but making them all require but there are default value - """ - config = model.Config - field_definitions = {} - for name_, field_ in model.__fields__.items(): - field_definitions[name_] = (field_.outer_type_, field_.field_info.default) - return create_model(f'{model.__name__}RequireButDefault', **field_definitions, - __config__=config) # type: ignore[arg-type] - - -def _filter_none(request_or_response_object): - received_request = deepcopy(request_or_response_object.__dict__) - if 'insert' in received_request: - insert_item_without_null = [] - for received_insert in received_request['insert']: - received_insert_ = deepcopy(received_insert) - for received_insert_item, received_insert_value in received_insert_.__dict__.items(): - if hasattr(received_insert_value, '__module__'): - if received_insert_value.__module__ == 'fastapi.params' or received_insert_value is None: - delattr(received_insert, received_insert_item) - elif received_insert_value is None: - delattr(received_insert, received_insert_item) - - insert_item_without_null.append(received_insert) - setattr(request_or_response_object, 'insert', insert_item_without_null) - else: - for name, value in received_request.items(): - if hasattr(value, '__module__'): - if value.__module__ == 'fastapi.params' or value is None: - delattr(request_or_response_object, name) - elif value is None: - delattr(request_or_response_object, name) - - -class ApiParameterSchemaBuilder: - unsupported_data_types = ["BLOB"] - partial_supported_data_types = ["INTERVAL", "JSON", "JSONB"] - - def __init__(self, db_model: Type, sql_type, exclude_column=None, constraints=None, - exclude_primary_key=False - # ,foreign_include=False - ): - self.class_name = db_model.__name__ - self.root_table_name = get_table_name(db_model) - self.constraints = constraints - self.exclude_primary_key = exclude_primary_key - if exclude_column is None: - self._exclude_column = [] - else: - self._exclude_column = exclude_column - self.alias_mapper: Dict[str, str] = {} # Table not support alias - if self.exclude_primary_key: - self.__db_model: Table = db_model - self.__db_model_table: Table = db_model.__table__ - self.__columns = db_model.__table__.c - self.db_name: str = db_model.__tablename__ - else: - self.__db_model: DeclarativeClassT = db_model - self.__db_model_table: Table = db_model.__table__ - self.db_name: str = db_model.__tablename__ - self.__columns = db_model.__table__.c - model = self.__db_model - - self.code_gen = ModelCodeGen(self.root_table_name, sql_type) - self.code_gen.gen_model(db_model) - - self.primary_key_str, self._primary_key_dataclass_model, self._primary_key_field_definition \ - = self._extract_primary() - self.unique_fields: List[str] = self._extract_unique() - - self.code_gen.build_constant(constants= [("PRIMARY_KEY_NAME", self.primary_key_str), - ("UNIQUE_LIST", self.unique_fields)]) - self.uuid_type_columns = [] - self.str_type_columns = [] - self.number_type_columns = [] - self.datetime_type_columns = [] - self.timedelta_type_columns = [] - self.bool_type_columns = [] - self.json_type_columns = [] - self.array_type_columns = [] - self.foreign_table_response_model_sets: Dict[TableNameT, ResponseModelT] = {} - self.all_field: List[dict] = self._extract_all_field() - self.sql_type = sql_type - - def extra_foreign_table(self, db_model=None) -> Dict[ForeignKeyName, dict]: - if db_model is None: - db_model = self.__db_model - if self.exclude_primary_key: - return self._extra_foreign_table_from_table() - else: - return self._extra_foreign_table_from_declarative_base(db_model) - - def _extract_primary(self, db_model_table=None) -> Union[tuple, Tuple[Union[str, Any], - DataClassT, - Tuple[Union[str, Any], - Union[Type[uuid.UUID], Any], - Optional[Any]]]]: - if db_model_table == None: - db_model_table = self.__db_model_table - primary_list = db_model_table.primary_key.columns.values() - if not primary_list or self.exclude_primary_key: - return (None, None, None) - if len(primary_list) > 1: - raise SchemaException( - f'multiple primary key / or composite not supported; {self.db_name} ') - primary_key_column, = primary_list - column_type = str(primary_key_column.type) - try: - python_type = primary_key_column.type.python_type - if column_type in self.unsupported_data_types: - raise ColumnTypeNotSupportedException( - f'The type of column {primary_key_column.key} ({column_type}) not supported yet') - if column_type in self.partial_supported_data_types: - warnings.warn( - f'The type of column {primary_key_column.key} ({column_type}) ' - f'is not support data query (as a query parameters )') - - except NotImplementedError: - if column_type == "UUID": - python_type = uuid.UUID - else: - raise ColumnTypeNotSupportedException( - f'The type of column {primary_key_column.key} ({column_type}) not supported yet') - # handle if python type is UUID - if python_type.__name__ in ['str', - 'int', - 'float', - 'Decimal', - 'UUID', - 'bool', - 'date', - 'time', - 'datetime']: - column_type = python_type.__name__ - else: - raise ColumnTypeNotSupportedException( - f'The type of column {primary_key_column.key} ({column_type}) not supported yet') - - default = self._extra_default_value(primary_key_column) - description = self._get_field_description(primary_key_column) - if default is ...: - warnings.warn( - f'The column of {primary_key_column.key} has not default value ' - f'and it is not nullable and in exclude_list' - f'it may throw error when you insert data ') - primary_column_name = str(primary_key_column.key) - primary_field_definitions = (primary_column_name, column_type, default) - class_name = f'{self.class_name}PrimaryKeyModel' - self.code_gen.build_dataclass(class_name=class_name, fields=[(primary_field_definitions[0], - primary_field_definitions[1], - f'Query({primary_field_definitions[2]})')]) - primary_columns_model: DataClassT = make_dataclass(f'{self.class_name + str(uuid.uuid4())}_PrimaryKeyModel', - [(primary_field_definitions[0], - primary_field_definitions[1], - Query(primary_field_definitions[2], - description=description))], - namespace={ - '__post_init__': lambda - self_object: self._value_of_list_to_str( - self_object, self.uuid_type_columns) - }) - - assert primary_column_name and primary_columns_model and primary_field_definitions - return primary_column_name, primary_columns_model, primary_field_definitions - - def _extract_unique(self) -> List[str]: - unique_constraint = None - if not self.constraints: - return [] - for constraint in self.constraints: - if isinstance(constraint, UniqueConstraint): - if unique_constraint: - raise SchemaException( - "Only support one unique constraint/ Use unique constraint and composite unique constraint " - "at same time is not supported / Use composite unique constraint if there are more than one unique constraint") - unique_constraint = constraint - if unique_constraint: - unique_column_name_list = [] - for constraint_column in unique_constraint.columns: - column_name = str(constraint_column.key) - unique_column_name = column_name - unique_column_name_list.append(unique_column_name) - return unique_column_name_list - else: - return [] - - @staticmethod - def _get_field_description(column: Column) -> str: - if not hasattr(column, 'comment'): - return "" - return column.comment - - def _extract_all_field(self, columns=None) -> List[dict]: - fields: List[dict] = [] - if not columns: - columns = self.__columns - elif isinstance(columns, DeclarativeMeta): - columns = columns.__table__.c - elif isinstance(columns, Table): - columns = columns.c - for column in columns: - column_name = str(column.key) - column_foreign = [i.target_fullname for i in column.foreign_keys] - default = self._extra_default_value(column) - if column_name in self._exclude_column: - continue - column_type = str(column.type) - description = self._get_field_description(column) - try: - python_type = column.type.python_type - if column_type in self.unsupported_data_types: - raise ColumnTypeNotSupportedException( - f'The type of column {column_name} ({column_type}) not supported yet') - if column_type in self.partial_supported_data_types: - warnings.warn( - f'The type of column {column_name} ({column_type}) ' - f'is not support data query (as a query parameters )') - except NotImplementedError: - if column_type == "UUID": - python_type = uuid.UUID - else: - raise ColumnTypeNotSupportedException( - f'The type of column {column_name} ({column_type}) not supported yet') - # string filter - if python_type.__name__ in ['str']: - self.str_type_columns.append(column_name) - # uuid filter - elif python_type.__name__ in ['UUID']: - self.uuid_type_columns.append(column.name) - python_type.__name__ = "uuid.UUID" - # number filter - elif python_type.__name__ in ['int', 'float', 'Decimal']: - self.number_type_columns.append(column_name) - # date filter - elif python_type.__name__ in ['date', 'time', 'datetime']: - self.datetime_type_columns.append(column_name) - # timedelta filter - elif python_type.__name__ in ['timedelta']: - self.timedelta_type_columns.append(column_name) - # bool filter - elif python_type.__name__ in ['bool']: - self.bool_type_columns.append(column_name) - # json filter - elif python_type.__name__ in ['dict']: - self.json_type_columns.append(column_name) - # array filter - elif python_type.__name__ in ['list']: - self.array_type_columns.append(column_name) - base_column_detail, = column.base_columns - if hasattr(base_column_detail.type, 'item_type'): - item_type = base_column_detail.type.item_type.python_type - fields.append({'column_name': column_name, - 'column_type': f"List[{item_type.__name__}]", - 'column_default': default, - 'column_description': description}) - continue - else: - raise ColumnTypeNotSupportedException( - f'The type of column {column_name} ({column_type}) not supported yet') - - if column_type == "JSONB": - fields.append({'column_name': column_name, - 'column_type': f'Union[{python_type.__name__}, list]', - 'column_default': default, - 'column_description': description, - 'column_foreign': column_foreign}) - else: - fields.append({'column_name': column_name, - 'column_type': python_type.__name__, - 'column_default': default, - 'column_description': description, - 'column_foreign': column_foreign}) - - return fields - - @staticmethod - def _value_of_list_to_str(request_or_response_object, columns): - received_request = deepcopy(request_or_response_object.__dict__) - if isinstance(columns, str): - columns = [columns] - if 'insert' in request_or_response_object.__dict__: - insert_str_list = [] - for insert_item in request_or_response_object.__dict__['insert']: - for column in columns: - for insert_item_column, _ in insert_item.__dict__.items(): - if column in insert_item_column: - value_ = insert_item.__dict__[insert_item_column] - if value_ is not None: - if isinstance(value_, list): - str_value_ = [str(i) for i in value_] - else: - str_value_ = str(value_) - setattr(insert_item, insert_item_column, str_value_) - insert_str_list.append(insert_item) - setattr(request_or_response_object, 'insert', insert_str_list) - else: - for column in columns: - for received_column_name, _ in received_request.items(): - if column in received_column_name: - value_ = received_request[received_column_name] - if value_ is not None: - if isinstance(value_, list): - str_value_ = [str(i) for i in value_] - else: - str_value_ = str(value_) - setattr(request_or_response_object, received_column_name, str_value_) - - @staticmethod - def _assign_join_table_instance(request_or_response_object, join_table_mapping): - received_request = deepcopy(request_or_response_object.__dict__) - join_table_replace = {} - if 'join_foreign_table' in received_request: - for join_table in received_request['join_foreign_table']: - if join_table in join_table_mapping: - join_table_replace[str(join_table)] = join_table_mapping[join_table] - setattr(request_or_response_object, 'join_foreign_table', join_table_replace) - - @staticmethod - def _get_many_string_matching_patterns_description_builder(): - return '''
Composite string field matching pattern
-
Allow to select more than one pattern for string query -
https://www.postgresql.org/docs/9.3/functions-matching.html ''' - - @staticmethod - def _get_many_order_by_columns_description_builder(all_columns, regex_validation, primary_name): - return f'''
support column: -
{all_columns}

support ordering: -
{list(map(str, Ordering))} -
-
example: -
  {primary_name}:ASC -
  {primary_name}: DESC -
  {primary_name} : DESC -
  {primary_name} (default sort by ASC)''' - - @staticmethod - def _extra_default_value(column): - if not column.nullable: - if column.default is not None: - default = column.default.arg - elif column.server_default is not None: - default = "None" - elif column.primary_key and column.autoincrement == True: - default = "None" - else: - default = "..." - else: - if column.default is not None: - default = column.default.arg - else: - default = "None" - return default - - def _assign_str_matching_pattern(self, field_of_param: dict, result_: List[dict]) -> List[dict]: - if self.sql_type == SqlType.postgresql: - operator = "List[PGSQLMatchingPatternInString]" - else: - operator = "List[MatchingPatternInStringBase]" - - for i in [ - {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.Str + ExtraFieldType.Matching_pattern, - 'column_type': f'Optional[{operator}]', - 'column_default': f'[MatchingPatternInStringBase.case_sensitive]', - 'column_description': "None"}, - {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.Str, - 'column_type': f'Optional[List[{field_of_param["column_type"]}]]', - 'column_default': "None", - 'column_description': field_of_param['column_description']} - ]: - result_.append(i) - return result_ - - @staticmethod - def _assign_list_comparison(field_of_param, result_: List[dict]) -> List[dict]: - for i in [ - { - 'column_name': field_of_param[ - 'column_name'] + f'{ExtraFieldTypePrefix.List}{ExtraFieldType.Comparison_operator}', - 'column_type': 'Optional[ItemComparisonOperators]', - 'column_default': 'ItemComparisonOperators.In', - 'column_description': "None"}, - {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.List, - 'column_type': f'Optional[List[{field_of_param["column_type"]}]]', - 'column_default': 'None', - 'column_description': field_of_param['column_description']} - - ]: - result_.append(i) - return result_ - - @staticmethod - def _assign_range_comparison(field_of_param, result_: List[dict]) -> List[dict]: - for i in [ - {'column_name': field_of_param[ - 'column_name'] + f'{ExtraFieldTypePrefix.From}{ExtraFieldType.Comparison_operator}', - 'column_type': 'Optional[RangeFromComparisonOperators]', - 'column_default': 'RangeFromComparisonOperators.Greater_than_or_equal_to', - 'column_description': "None"}, - - {'column_name': field_of_param[ - 'column_name'] + f'{ExtraFieldTypePrefix.To}{ExtraFieldType.Comparison_operator}', - 'column_type': 'Optional[RangeToComparisonOperators]', - 'column_default': 'RangeToComparisonOperators.Less_than.Less_than_or_equal_to', - 'column_description': "None"}, - ]: - result_.append(i) - - for i in [ - {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.From, - 'column_type': f'Optional[NewType(ExtraFieldTypePrefix.From, {field_of_param["column_type"]})]', - 'column_default': "None", - 'column_description': field_of_param['column_description']}, - - {'column_name': field_of_param['column_name'] + ExtraFieldTypePrefix.To, - 'column_type': f'Optional[NewType(ExtraFieldTypePrefix.To, {field_of_param["column_type"]})]', - 'column_default': "None", - 'column_description': field_of_param['column_description']} - ]: - result_.append(i) - return result_ - - def _get_fizzy_query_param(self, exclude_column: List[str] = None, fields=None) -> List[dict]: - if not fields: - fields = self.all_field - if not exclude_column: - exclude_column = [] - fields_: List[dict] = deepcopy(fields) - result = [] - for field_ in fields_: - if field_['column_name'] in exclude_column: - continue - if "column_foreign" in field_ and field_['column_foreign']: - jump = False - for foreign in field_['column_foreign']: - if foreign in exclude_column: - jump = True - if jump: - continue - field_['column_default'] = None - if field_['column_name'] in self.str_type_columns: - result = self._assign_str_matching_pattern(field_, result) - result = self._assign_list_comparison(field_, result) - - elif field_['column_name'] in self.uuid_type_columns or \ - field_['column_name'] in self.bool_type_columns: - result = self._assign_list_comparison(field_, result) - - elif field_['column_name'] in self.number_type_columns or \ - field_['column_name'] in self.datetime_type_columns: - result = self._assign_range_comparison(field_, result) - result = self._assign_list_comparison(field_, result) - - return result - - def _assign_pagination_param(self, result_: List[tuple]) -> List[Union[Tuple, Dict]]: - all_column_ = [i['column_name'] for i in self.all_field] - - regex_validation = "(?=(" + '|'.join(all_column_) + r")?\s?:?\s*?(?=(" + '|'.join( - list(map(str, Ordering))) + r"))?)" - columns_with_ordering = pydantic.constr(regex=regex_validation) - - for i in [ - ('limit', 'Optional[int]', "Query(None)"), - ('offset', 'Optional[int]', "Query(None)"), - ('order_by_columns', f'Optional[List[pydantic.constr(regex="{regex_validation}")]]', - f'''Query( - None, - description="""{self._get_many_order_by_columns_description_builder( - all_columns=all_column_, - regex_validation=regex_validation, - primary_name='any name of column')}""")''') - ]: - result_.append(i) - return result_ - - def upsert_one(self) -> Tuple: - request_validation = [lambda self_object: _filter_none(self_object)] - request_fields = [] - response_fields = [] - - # Create on_conflict Model - all_column_ = [i['column_name'] for i in self.all_field] - conflict_columns = ('update_columns', - "Optional[List[str]]", - f"Body({set(all_column_) - set(self.unique_fields)},description='update_columns should contain which columns you want to update when the unique columns got conflict')") - - self.code_gen.build_dataclass(class_name=self.class_name + "UpsertOneConflictModel", - fields=[conflict_columns]) - on_conflict_handle = [('on_conflict', f"Optional[{self.class_name + 'UpsertOneConflictModel'}]", - "Body(None)")] - - # Create Request and Response Model - all_field = deepcopy(self.all_field) - for i in all_field: - request_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']}, description={i['column_description']})")) - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']}, description={i['column_description']})")) - - self.code_gen.build_dataclass(class_name=self.class_name + "UpsertOneRequestBodyModel", - fields=request_fields + on_conflict_handle, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_dataclass(class_name=self.class_name + "UpsertOneResponseModel", - fields=response_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - return None, self.class_name + "UpsertOneRequestBodyModel", self.class_name + "UpsertOneResponseModel" - - def upsert_many(self) -> Tuple: - insert_fields = [] - response_fields = [] - - # Create on_conflict Model - all_column_ = [i['column_name'] for i in self.all_field] - conflict_columns = ('update_columns', - "Optional[List[str]]", - f"Body({set(all_column_) - set(self.unique_fields)},description='update_columns should contain which columns you want to update when the unique columns got conflict')") - - self.code_gen.build_dataclass(class_name=self.class_name + "UpsertManyConflictModel", - fields=[conflict_columns]) - on_conflict_handle = [('on_conflict', f"Optional[{self.class_name + 'UpsertManyConflictModel'}]", - "Body(None)")] - - # Ready the Request and Response Model - all_field = deepcopy(self.all_field) - - for i in all_field: - insert_fields.append((i['column_name'], - i['column_type'], - f'field(default=Body({i["column_default"]}, description={i["column_description"]}))')) - - if i["column_default"] == "None": - i["column_default"] = "..." - response_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]}, description={i["column_description"]})')) - - self.code_gen.build_dataclass(class_name=self.class_name + "UpsertManyItemRequestBodyModel", - fields=insert_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - - insert_list_field = [('insert', f"List[{self.class_name + 'UpsertManyItemRequestBodyModel'}]", "Body(...)")] - - self.code_gen.build_dataclass(class_name=self.class_name + "UpsertManyItemListRequestBodyModel", - fields=insert_list_field + on_conflict_handle, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True - ) - - self.code_gen.build_base_model(class_name=self.class_name + "UpsertManyItemResponseModel", - fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - - self.code_gen.build_base_model_root(class_name=self.class_name + "UpsertManyItemListResponseModel", - field=( - f'{f"{self.class_name}UpsertManyItemResponseModel"}', - None)) - - return None, self.class_name + "UpsertManyItemListRequestBodyModel", self.class_name + "UpsertManyItemListResponseModel" - - def create_one(self) -> Tuple: - request_validation = [lambda self_object: _filter_none(self_object)] - request_fields = [] - response_fields = [] - - # Create Request and Response Model - all_field = deepcopy(self.all_field) - for i in all_field: - request_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]}, description={i["column_description"]})')) - response_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]}, description={i["column_description"]})')) - - # Ready the uuid to str validator - if self.uuid_type_columns: - request_validation.append(lambda self_object: self._value_of_list_to_str(self_object, - self.uuid_type_columns)) - - self.code_gen.build_dataclass(class_name=self.class_name + "CreateOneRequestBodyModel", - fields=request_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - self.code_gen.build_base_model(class_name=self.class_name + "CreateOneResponseModel", - fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - - return None, self.class_name + "CreateOneRequestBodyModel", self.class_name + "CreateOneResponseModel" - - def create_many(self) -> Tuple: - insert_fields = [] - response_fields = [] - - all_field = deepcopy(self.all_field) - for i in all_field: - insert_fields.append((i['column_name'], - i['column_type'], - f'field(default=Body({i["column_default"]}, description={i["column_description"]}))')) - - if i["column_default"] == "None": - i["column_default"] = "..." - response_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]}, description={i["column_description"]})')) - - self.code_gen.build_dataclass(class_name=self.class_name + "CreateManyItemRequestModel", - fields=insert_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - - insert_list_field = [('insert', f"List[{self.class_name + 'CreateManyItemRequestModel'}]", "Body(...)")] - - self.code_gen.build_dataclass(class_name=self.class_name + "CreateManyItemListRequestModel", - fields=insert_list_field) - - self.code_gen.build_base_model(class_name=self.class_name + "CreateManyItemResponseModel", - fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - - self.code_gen.build_base_model_root(class_name=self.class_name + "CreateManyItemListResponseModel", - field=( - f'{f"{self.class_name}CreateManyItemResponseModel"}', - None)) - - return None, self.class_name + "CreateManyItemListRequestModel", self.class_name + "CreateManyItemListResponseModel" - - def find_many(self) -> Tuple: - - query_param: List[dict] = self._get_fizzy_query_param() - query_param: List[Tuple] = self._assign_pagination_param(query_param) - - response_fields = [] - all_field = deepcopy(self.all_field) - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - None)) - request_fields = [] - for i in query_param: - assert isinstance(i, Tuple) or isinstance(i, dict) - if isinstance(i, Tuple): - request_fields.append(i) - if isinstance(i, dict): - request_fields.append((i['column_name'], - i['column_type'], - f'Query({i["column_default"]}, description={i["column_description"]})')) - - self.code_gen.build_dataclass(class_name=self.class_name + "FindManyRequestBody", fields=request_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_dataclass(class_name=self.class_name + "FindManyResponseModel", fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model_root(class_name=self.class_name + "FindManyResponseRootModel", - field=( - f'{self.class_name + "FindManyResponseModel"}', - None), - base_model="ExcludeUnsetBaseModel") - - return self.class_name + "FindManyRequestBody", None, f'{self.class_name}FindManyResponseItemListModel' - - def _extra_relation_primary_key(self, relation_dbs): - primary_key_columns = [] - foreign_table_name = "" - primary_column_names = [] - for db_model_table in relation_dbs: - table_name = db_model_table.key - foreign_table_name += table_name + "_" - primary_list = db_model_table.primary_key.columns.values() - primary_key_column, = primary_list - column_type = str(primary_key_column.type) - try: - python_type = primary_key_column.type.python_type - if column_type in self.unsupported_data_types: - raise ColumnTypeNotSupportedException( - f'The type of column {primary_key_column.key} ({column_type}) not supported yet') - if column_type in self.partial_supported_data_types: - warnings.warn( - f'The type of column {primary_key_column.key} ({column_type}) ' - f'is not support data query (as a query parameters )') - - except NotImplementedError: - if column_type == "UUID": - python_type = uuid.UUID - else: - raise ColumnTypeNotSupportedException( - f'The type of column {primary_key_column.key} ({column_type}) not supported yet') - # handle if python type is UUID - if python_type.__name__ in ['str', - 'int', - 'float', - 'Decimal', - 'UUID', - 'bool', - 'date', - 'time', - 'datetime']: - column_type = python_type - else: - raise ColumnTypeNotSupportedException( - f'The type of column {primary_key_column.key} ({column_type}) not supported yet') - default = self._extra_default_value(primary_key_column) - if default is ...: - warnings.warn( - f'The column of {primary_key_column.key} has not default value ' - f'and it is not nullable and in exclude_list' - f'it may throw error when you insert data ') - description = self._get_field_description(primary_key_column) - primary_column_name = str(primary_key_column.key) - alias_primary_column_name = table_name + FOREIGN_PATH_PARAM_KEYWORD + str(primary_key_column.key) - primary_column_names.append(alias_primary_column_name) - primary_key_columns.append((alias_primary_column_name, column_type, Query(default, - description=description))) - - # TODO test foreign uuid key - primary_columns_model: DataClassT = make_dataclass(f'{foreign_table_name + str(uuid.uuid4())}_PrimaryKeyModel', - primary_key_columns, - namespace={ - '__post_init__': lambda - self_object: self._value_of_list_to_str( - self_object, self.uuid_type_columns) - }) - assert primary_column_names and primary_columns_model and primary_key_columns - return primary_column_names, primary_columns_model, primary_key_columns - - def find_one(self) -> Tuple: - query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) - response_fields = [] - all_field = deepcopy(self.all_field) - - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]})')) - - request_fields = [] - for i in query_param: - assert isinstance(i, dict) or isinstance(i, tuple) - if isinstance(i, Tuple): - request_fields.append(i) - else: - request_fields.append((i['column_name'], - i['column_type'], - f'Query({i["column_default"]})')) - self.code_gen.build_dataclass(class_name=self.class_name + "FindOneRequestBody", fields=request_fields, - value_of_list_to_str_columns=self.uuid_type_columns, filter_none=True) - - self.code_gen.build_dataclass(class_name=self.class_name + "FindOneResponseModel", fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - self.code_gen.build_base_model_root(class_name=self.class_name + "FindOneResponseRootModel", - field=( - f'{self.class_name + "FindOneResponseModel"}', - None), - base_model="ExcludeUnsetBaseModel") - - return self.class_name + "PrimaryKeyModel", self.class_name + "FindOneRequestBody", None, self.class_name + "FindOneResponseRootModel", None - - def delete_one(self) -> Tuple: - query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) - response_fields = [] - all_field = deepcopy(self.all_field) - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']})")) - - request_fields = [] - for i in query_param: - assert isinstance(i, dict) - request_fields.append((i['column_name'], - i['column_type'], - f"Query({i['column_default']}, description={i['column_description']})")) - - self.code_gen.build_dataclass(class_name=self.class_name + "DeleteOneRequestBodyModel", - fields=request_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model(class_name=self.class_name + "DeleteOneResponseModel", - fields=response_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - return self._primary_key_dataclass_model, self.class_name + "DeleteOneRequestBodyModel", None, self.class_name + "DeleteOneResponseModel" - - def delete_many(self) -> Tuple: - query_param: List[dict] = self._get_fizzy_query_param() - response_fields = [] - all_field = deepcopy(self.all_field) - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']})")) - - request_fields = [] - for i in query_param: - assert isinstance(i, dict) - request_fields.append((i['column_name'], - i['column_type'], - f"Query({i['column_default']}, description={i['column_description']})")) - - self.code_gen.build_dataclass(class_name=self.class_name + "DeleteManyRequestBodyModel", - fields=request_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model(class_name=self.class_name + "DeleteManyItemResponseModel", - fields=response_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model_root(class_name=self.class_name + "DeleteManyItemListResponseModel", - field=( - f'{self.class_name + "DeleteManyItemResponseModel"}', - None)) - - return None, self.class_name + "DeleteManyRequestBodyModel", None, self.class_name + "DeleteManyItemListResponseModel" - - def patch(self) -> Tuple: - query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) - - response_fields = [] - all_field = deepcopy(self.all_field) - request_body_fields = [] - - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']})")) - if i['column_name'] != self.primary_key_str: - request_body_fields.append((i['column_name'], - i['column_type'], - f"Body(None, description={i['column_description']})")) - - request_query_fields = [] - for i in query_param: - assert isinstance(i, dict) - request_query_fields.append((i['column_name'], - i['column_type'], - f"Query({i['column_default']}, description={i['column_description']})")) - - self.code_gen.build_dataclass(class_name=self.class_name + "PatchOneRequestQueryModel", - fields=request_query_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_dataclass(class_name=self.class_name + "PatchOneRequestBodyModel", - fields=request_body_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model(class_name=self.class_name + "PatchOneResponseModel", - fields=response_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - return self._primary_key_dataclass_model, self.class_name + "PatchOneRequestQueryModel", self.class_name + "PatchOneRequestBodyModel", self.class_name + "PatchOneResponseModel" - - def update_one(self) -> Tuple: - query_param: List[dict] = self._get_fizzy_query_param(self.primary_key_str) - - response_fields = [] - all_field = deepcopy(self.all_field) - request_body_fields = [] - - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']})")) - if i['column_name'] not in [self.primary_key_str]: - request_body_fields.append((i['column_name'], - i['column_type'], - f"Body(..., description={i['column_description']})")) - - request_query_fields = [] - for i in query_param: - assert isinstance(i, dict) - request_query_fields.append((i['column_name'], - i['column_type'], - f"Query({i['column_default']}, description={i['column_description']})")) - - request_validation = [lambda self_object: _filter_none(self_object)] - if self.uuid_type_columns: - request_validation.append(lambda self_object: self._value_of_list_to_str(self_object, - self.uuid_type_columns)) - - self.code_gen.build_dataclass(class_name=self.class_name + "UpdateOneRequestQueryBody", - fields=request_query_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - - self.code_gen.build_dataclass(class_name=self.class_name + "UpdateOneRequestBodyBody", - fields=request_body_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - - self.code_gen.build_base_model(class_name=self.class_name + "UpdateOneResponseModel", - fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - return self.class_name + "PrimaryKeyModel", self.class_name + "UpdateOneRequestQueryBody", self.class_name + "UpdateOneRequestBodyBody", self.class_name + "UpdateOneResponseModel" - - def update_many(self) -> Tuple: - """ - In update many, it allow you update some columns into the same value in limit of a scope, - you can get the limit of scope by using request query. - And fill out the columns (except the primary key column and unique columns) you want to update - and the update value in the request body - - The response will show you the update result - :return: url param dataclass model - """ - query_param: List[dict] = self._get_fizzy_query_param() - - response_fields = [] - all_field = deepcopy(self.all_field) - request_body_fields = [] - - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']})")) - if i['column_name'] not in [self.primary_key_str]: - request_body_fields.append((i['column_name'], - i['column_type'], - f"Body(..., description={i['column_description']})")) - - request_query_fields = [] - for i in query_param: - assert isinstance(i, dict) - request_query_fields.append((i['column_name'], - i['column_type'], - f"Query({i['column_default']}, description={i['column_description']})")) - - self.code_gen.build_dataclass(class_name=self.class_name + "UpdateManyRequestQueryBody", - fields=request_query_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - self.code_gen.build_dataclass(class_name=self.class_name + "UpdateManyRequestBodyBody", - fields=request_body_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - self.code_gen.build_base_model(class_name=self.class_name + "UpdateManyResponseItemModel", - fields=response_fields, - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - self.code_gen.build_base_model_root(class_name=f'{self.class_name}UpdateManyResponseItemListModel', - field=( - f'Union[List[{f"{self.class_name}UpdateManyResponseItemModel"}]]', - None), - value_of_list_to_str_columns=self.uuid_type_columns, - filter_none=True) - # response_model = _add_orm_model_config_into_pydantic_model(response_model, config=OrmConfig) - - return None, self.class_name + "UpdateManyRequestQueryBody", self.class_name + "UpdateManyRequestBodyBody", f'{self.class_name}UpdateManyResponseItemListModel' - - def patch_many(self) -> Tuple: - """ - In update many, it allow you update some columns into the same value in limit of a scope, - you can get the limit of scope by using request query. - And fill out the columns (except the primary key column and unique columns) you want to update - and the update value in the request body - - The response will show you the update result - :return: url param dataclass model - """ - query_param: List[dict] = self._get_fizzy_query_param() - - response_fields = [] - all_field = deepcopy(self.all_field) - request_body_fields = [] - - for i in all_field: - response_fields.append((i['column_name'], - i['column_type'], - f"Body({i['column_default']})")) - if i['column_name'] not in [self.primary_key_str]: - request_body_fields.append((i['column_name'], - i['column_type'], - f"Body(None, description={i['column_description']})")) - - request_query_fields = [] - for i in query_param: - assert isinstance(i, dict) - request_query_fields.append((i['column_name'], - i['column_type'], - f"Query({i['column_default']}, description={i['column_description']})")) - - self.code_gen.build_dataclass(class_name=self.class_name + "PatchManyRequestQueryBody", - fields=request_query_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_dataclass(class_name=self.class_name + "PatchManyRequestBody", - fields=request_body_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model(class_name=self.class_name + "PatchManyItemResponseModel", - fields=response_fields, - filter_none=True, - value_of_list_to_str_columns=self.uuid_type_columns) - - self.code_gen.build_base_model_root(class_name=f'{self.class_name}PatchManyItemListResponseModel', - field=( - f'{f"{self.class_name}PatchManyItemResponseModel"}', - None)) - return None, self.class_name + "UpdateManyRequestQueryBody", self.class_name + "UpdateManyRequestBodyBody", f'{self.class_name}UpdateManyResponseItemListModel' - - def post_redirect_get(self) -> Tuple: - request_validation = [lambda self_object: _filter_none(self_object)] - request_body_fields = [] - response_body_fields = [] - - # Create Request and Response Model - all_field = deepcopy(self.all_field) - for i in all_field: - request_body_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]}, description={i["column_description"]})')) - response_body_fields.append((i['column_name'], - i['column_type'], - f'Body({i["column_default"]}, description={i["column_description"]})')) - - # Ready the uuid to str validator - if self.uuid_type_columns: - request_validation.append(lambda self_object: self._value_of_list_to_str(self_object, - self.uuid_type_columns)) - self.code_gen.build_dataclass(class_name=self.class_name + "PostAndRedirectRequestModel", - fields=request_body_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - self.code_gen.build_base_model(class_name=self.class_name + "PostAndRedirectResponseModel", - fields=response_body_fields, - value_of_list_to_str_columns=self.uuid_type_columns) - return None, self.class_name + "PostAndRedirectRequestModel", self.class_name + "PostAndRedirectResponseModel" - diff --git a/src/fastapi_quickcrud_codegen/misc/type.py b/src/fastapi_quickcrud_codegen/misc/type.py deleted file mode 100644 index 17416fc..0000000 --- a/src/fastapi_quickcrud_codegen/misc/type.py +++ /dev/null @@ -1,162 +0,0 @@ -from enum import Enum, auto -from itertools import chain - -from strenum import StrEnum - -from .exceptions import InvalidRequestMethod - - -class SqlType(StrEnum): - postgresql = auto() - mysql = auto() - mariadb = auto() - sqlite = auto() - oracle = auto() - mssql = auto() - -class Ordering(StrEnum): - DESC = auto() - ASC = auto() - - -class CrudMethods(Enum): - FIND_ONE = "FIND_ONE" - FIND_MANY = "FIND_MANY" - UPDATE_ONE = "UPDATE_ONE" - UPDATE_MANY = "UPDATE_MANY" - PATCH_ONE = "PATCH_ONE" - PATCH_MANY = "PATCH_MANY" - UPSERT_ONE = "UPSERT_ONE" - UPSERT_MANY = "UPSERT_MANY" - CREATE_ONE = "CREATE_ONE" - CREATE_MANY = "CREATE_MANY" - DELETE_ONE = "DELETE_ONE" - DELETE_MANY = "DELETE_MANY" - POST_REDIRECT_GET = "POST_REDIRECT_GET" - FIND_ONE_WITH_FOREIGN_TREE = "FIND_ONE_WITH_FOREIGN_TREE" - FIND_MANY_WITH_FOREIGN_TREE = "FIND_MANY_WITH_FOREIGN_TREE" - - @staticmethod - def get_table_full_crud_method(): - return [CrudMethods.FIND_MANY, CrudMethods.CREATE_MANY, CrudMethods.UPDATE_MANY, CrudMethods.PATCH_MANY, - CrudMethods.DELETE_MANY] - - @staticmethod - def get_declarative_model_full_crud_method(): - return [CrudMethods.FIND_MANY, CrudMethods.FIND_ONE, - CrudMethods.UPDATE_MANY, CrudMethods.UPDATE_ONE, - CrudMethods.PATCH_MANY, CrudMethods.PATCH_ONE, CrudMethods.CREATE_MANY, - CrudMethods.DELETE_MANY, CrudMethods.DELETE_ONE, CrudMethods.FIND_ONE_WITH_FOREIGN_TREE, - CrudMethods.FIND_MANY_WITH_FOREIGN_TREE] - - -class RequestMethods(Enum): - GET = "GET" - POST = "POST" - PUT = "PUT" - PATCH = "PATCH" - DELETE = "DELETE" - - -class CRUDRequestMapping(Enum): - FIND_ONE = RequestMethods.GET - FIND_ONE_WITH_FOREIGN_TREE = RequestMethods.GET - - FIND_MANY = RequestMethods.GET - FIND_MANY_WITH_FOREIGN_TREE = RequestMethods.GET - - UPDATE_ONE = RequestMethods.PUT - UPDATE_MANY = RequestMethods.PUT - - PATCH_ONE = RequestMethods.PATCH - PATCH_MANY = RequestMethods.PATCH - - CREATE_ONE = RequestMethods.POST - CREATE_MANY = RequestMethods.POST - - UPSERT_ONE = RequestMethods.POST - UPSERT_MANY = RequestMethods.POST - - DELETE_ONE = RequestMethods.DELETE - DELETE_MANY = RequestMethods.DELETE - - GET_VIEW = RequestMethods.GET - POST_REDIRECT_GET = RequestMethods.POST - - @classmethod - def get_request_method_by_crud_method(cls, value): - crud_methods = cls.__dict__ - if value not in crud_methods: - raise InvalidRequestMethod( - f'{value} is not an available request method, Please use CrudMethods to select available crud method') - return crud_methods[value].value - - -class ExtraFieldType(StrEnum): - Comparison_operator = '_____comparison_operator' - Matching_pattern = '_____matching_pattern' - - -class ExtraFieldTypePrefix(StrEnum): - List = '____list' - From = '____from' - To = '____to' - Str = '____str' - - -class RangeFromComparisonOperators(StrEnum): - Greater_than = auto() - Greater_than_or_equal_to = auto() - - -class RangeToComparisonOperators(StrEnum): - Less_than = auto() - Less_than_or_equal_to = auto() - - -class ItemComparisonOperators(StrEnum): - Equal = auto() - Not_equal = auto() - In = auto() - Not_in = auto() - - -class MatchingPatternInStringBase(StrEnum): - case_insensitive = auto() - case_sensitive = auto() - not_case_insensitive = auto() - not_case_sensitive = auto() - contains = auto() - - -class PGSQLMatchingPattern(StrEnum): - match_regex_with_case_sensitive = auto() - match_regex_with_case_insensitive = auto() - does_not_match_regex_with_case_sensitive = auto() - does_not_match_regex_with_case_insensitive = auto() - similar_to = auto() - not_similar_to = auto() - - -PGSQLMatchingPatternInString = StrEnum('PGSQLMatchingPatternInString', - {Pattern: auto() for Pattern in - chain(MatchingPatternInStringBase, PGSQLMatchingPattern)}) - - -class JSONMatchingMode(str, Enum): - match_the_key_value = 'match_the_key_value' - match_the_value_if_not_null_by_key = 'match_the_value_if_not_null_by_key' - custom_query = 'custom_query' - - -class JSONBMatchingMode(str, Enum): - match_the_key_value = 'match_the_key_value' - match_the_value_if_not_null_by_key = 'match_the_value_if_not_null_by_key' - custom_query = 'custom_query' - - -class SessionObject(StrEnum): - sqlalchemy = auto() - databases = auto() - -FOREIGN_PATH_PARAM_KEYWORD = "__pk__" \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/misc/utils.py b/src/fastapi_quickcrud_codegen/misc/utils.py deleted file mode 100644 index 08780f4..0000000 --- a/src/fastapi_quickcrud_codegen/misc/utils.py +++ /dev/null @@ -1,349 +0,0 @@ -from itertools import groupby -from typing import Type, List, Union, TypeVar, Optional - -from pydantic import BaseModel, BaseConfig -from sqlalchemy import Column, Integer -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.sql.elements import \ - or_, \ - BinaryExpression -from sqlalchemy.sql.schema import Table - -from .covert_model import convert_table_to_model -from .crud_model import CRUDModel -from .exceptions import QueryOperatorNotFound, PrimaryMissing, UnknownColumn -from .schema_builder import ApiParameterSchemaBuilder -from .type import \ - CrudMethods, \ - CRUDRequestMapping, \ - MatchingPatternInStringBase, \ - ExtraFieldType, \ - RangeFromComparisonOperators, \ - ExtraFieldTypePrefix, \ - RangeToComparisonOperators, \ - ItemComparisonOperators, PGSQLMatchingPatternInString, SqlType, FOREIGN_PATH_PARAM_KEYWORD - -Base = TypeVar("Base", bound=declarative_base) - -BaseModelT = TypeVar('BaseModelT', bound=BaseModel) - -__all__ = [ - 'sqlalchemy_to_pydantic', - # 'sqlalchemy_table_to_pydantic', - 'find_query_builder', - 'Base', - 'clean_input_fields', - 'group_find_many_join', - 'convert_table_to_model'] - -unsupported_data_types = ["BLOB"] -partial_supported_data_types = ["INTERVAL", "JSON", "JSONB"] - - -def clean_input_fields(param: Union[dict, list], model: Base): - assert isinstance(param, dict) or isinstance(param, list) or isinstance(param, set) - - if isinstance(param, dict): - stmt = {} - for column_name, value in param.items(): - if column_name == '__initialised__': - continue - column = getattr(model, column_name) - actual_column_name = column.expression.key - stmt[actual_column_name] = value - return stmt - if isinstance(param, list) or isinstance(param, set): - stmt = [] - for column_name in param: - if not hasattr(model, column_name): - raise UnknownColumn(f'column {column_name} is not exited') - column = getattr(model, column_name) - actual_column_name = column.expression.key - stmt.append(actual_column_name) - return stmt - - -def find_query_builder(param: dict, model: Base) -> List[Union[BinaryExpression]]: - query = [] - for column_name, value in param.items(): - if ExtraFieldType.Comparison_operator in column_name or ExtraFieldType.Matching_pattern in column_name: - continue - if ExtraFieldTypePrefix.List in column_name: - type_ = ExtraFieldTypePrefix.List - elif ExtraFieldTypePrefix.From in column_name: - type_ = ExtraFieldTypePrefix.From - elif ExtraFieldTypePrefix.To in column_name: - type_ = ExtraFieldTypePrefix.To - elif ExtraFieldTypePrefix.Str in column_name: - type_ = ExtraFieldTypePrefix.Str - else: - query.append((getattr(model, column_name) == value)) - # raise Exception('known error') - continue - sub_query = [] - table_column_name = column_name.replace(type_, "") - operator_column_name = column_name + process_type_map[type_] - operators = param.get(operator_column_name, None) - if not operators: - raise QueryOperatorNotFound(f'The query operator of {column_name} not found!') - if not isinstance(operators, list): - operators = [operators] - for operator in operators: - sub_query.append(process_map[operator](getattr(model, table_column_name), value)) - query.append((or_(*sub_query))) - return query - - -class OrmConfig(BaseConfig): - orm_mode = True - - -def sqlalchemy_to_pydantic( - db_model: Type, *, - crud_methods: List[CrudMethods], - sql_type: str = SqlType.postgresql, - exclude_columns: List[str] = None, - constraints=None, - # foreign_include: Optional[any] = None, - exclude_primary_key=False) -> CRUDModel: - db_model, _ = convert_table_to_model(db_model) - if exclude_columns is None: - exclude_columns = [] - # if foreign_include is None: - # foreign_include = {} - request_response_mode_set = {} - model_builder = ApiParameterSchemaBuilder(db_model, - constraints=constraints, - exclude_column=exclude_columns, - sql_type=sql_type, - # foreign_include=foreign_include, - exclude_primary_key=exclude_primary_key) - REQUIRE_PRIMARY_KEY_CRUD_METHOD = [CrudMethods.DELETE_ONE.value, - CrudMethods.FIND_ONE.value, - CrudMethods.PATCH_ONE.value, - CrudMethods.POST_REDIRECT_GET.value, - CrudMethods.UPDATE_ONE.value] - for crud_method in crud_methods: - if crud_method.value in REQUIRE_PRIMARY_KEY_CRUD_METHOD and not model_builder.primary_key_str: - raise PrimaryMissing(f"The generation of this API [{crud_method.value}] requires a primary key") - - if crud_method.value == CrudMethods.UPSERT_ONE.value: - model_builder.upsert_one() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.UPSERT_MANY.value: - model_builder.upsert_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - - elif crud_method.value == CrudMethods.CREATE_ONE.value: - model_builder.create_one() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - - elif crud_method.value == CrudMethods.CREATE_MANY.value: - model_builder.create_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.DELETE_ONE.value: - model_builder.delete_one() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.DELETE_MANY.value: - model_builder.delete_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.FIND_ONE.value: - model_builder.find_one() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.FIND_MANY.value: - model_builder.find_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.POST_REDIRECT_GET.value: - model_builder.post_redirect_get() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.PATCH_ONE.value: - model_builder.patch() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.UPDATE_ONE.value: - model_builder.update_one() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.UPDATE_MANY.value: - model_builder.update_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.PATCH_MANY.value: - model_builder.patch_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.FIND_ONE_WITH_FOREIGN_TREE.value: - model_builder.foreign_tree_get_one() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - elif crud_method.value == CrudMethods.FIND_MANY_WITH_FOREIGN_TREE.value: - model_builder.foreign_tree_get_many() - request_method = CRUDRequestMapping.get_request_method_by_crud_method(crud_method.value).value - if request_method not in request_response_mode_set: - request_response_mode_set[request_method] = {} - request_response_mode_set[request_method][crud_method.value] = True - model_builder.code_gen.gen() - - return CRUDModel( - **{**request_response_mode_set, - **{"PRIMARY_KEY_NAME": model_builder.primary_key_str, - "UNIQUE_LIST": model_builder.unique_fields}}) - - -process_type_map = { - ExtraFieldTypePrefix.List: ExtraFieldType.Comparison_operator, - ExtraFieldTypePrefix.From: ExtraFieldType.Comparison_operator, - ExtraFieldTypePrefix.To: ExtraFieldType.Comparison_operator, - ExtraFieldTypePrefix.Str: ExtraFieldType.Matching_pattern, -} - -process_map = { - RangeFromComparisonOperators.Greater_than: - lambda field, value: field > value, - - RangeFromComparisonOperators.Greater_than_or_equal_to: - lambda field, value: field >= value, - - RangeToComparisonOperators.Less_than: - lambda field, value: field < value, - - RangeToComparisonOperators.Less_than_or_equal_to: - lambda field, value: field <= value, - - ItemComparisonOperators.Equal: - lambda field, values: or_(field == value for value in values), - - ItemComparisonOperators.Not_equal: - lambda field, values: or_(field != value for value in values), - - ItemComparisonOperators.In: - lambda field, values: or_(field.in_(values)), - - ItemComparisonOperators.Not_in: - lambda field, values: or_(field.notin_(values)), - - MatchingPatternInStringBase.case_insensitive: - lambda field, values: or_(field.ilike(value) for value in values), - - MatchingPatternInStringBase.case_sensitive: - lambda field, values: or_(field.like(value) for value in values), - - MatchingPatternInStringBase.not_case_insensitive: - lambda field, values: or_(field.not_ilike(value) for value in values), - - MatchingPatternInStringBase.not_case_sensitive: - lambda field, values: or_(field.not_like(value) for value in values), - - MatchingPatternInStringBase.contains: - lambda field, values: or_(field.contains(value) for value in values), - - PGSQLMatchingPatternInString.similar_to: - lambda field, values: or_(field.op("SIMILAR TO")(value) for value in values), - - PGSQLMatchingPatternInString.not_similar_to: - lambda field, values: or_(field.op("NOT SIMILAR TO")(value) for value in values), - - PGSQLMatchingPatternInString.match_regex_with_case_sensitive: - lambda field, values: or_(field.op("~")(value) for value in values), - - PGSQLMatchingPatternInString.match_regex_with_case_insensitive: - lambda field, values: or_(field.op("~*")(value) for value in values), - - PGSQLMatchingPatternInString.does_not_match_regex_with_case_sensitive: - lambda field, values: or_(field.op("!~")(value) for value in values), - - PGSQLMatchingPatternInString.does_not_match_regex_with_case_insensitive: - lambda field, values: or_(field.op("!~*")(value) for value in values) -} - - -def table_to_declarative_base(db_model): - db_name = str(db_model.fullname) - Base = declarative_base() - if not db_model.primary_key: - db_model.append_column(Column('__id', Integer, primary_key=True, autoincrement=True)) - table_dict = {'__tablename__': db_name} - for i in db_model.c: - _, = i.expression.base_columns - _.table = None - table_dict[str(i.key)] = _ - tmp = type(f'{db_name}', (Base,), table_dict) - tmp.__table__ = db_model - return tmp - - -def group_find_many_join(list_of_dict: List[dict]) -> List[dict]: - def group_by_foreign_key(item): - tmp = {} - for k, v in item.items(): - if '_foreign' not in k: - tmp[k] = v - return tmp - - response_list = [] - for key, group in groupby(list_of_dict, group_by_foreign_key): - response = {} - for i in group: - for k, v in i.items(): - if '_foreign' in k: - if k not in response: - response[k] = [v] - else: - response[k].append(v) - for response_ in response: - i.pop(response_, None) - result = {**i, **response} - response_list.append(result) - return response_list - - - - -def path_query_builder(params, model) -> List[Union[BinaryExpression]]: - query = [] - if not params: - return query - for param_name, param_value in params.items(): - table_with_column = param_name.split(FOREIGN_PATH_PARAM_KEYWORD) - assert len(table_with_column) == 2 - table_name, column_name = table_with_column - table_model = model[table_name] - query.append((getattr(table_model, column_name) == param_value)) - return query diff --git a/src/fastapi_quickcrud_codegen/model/__init__.py b/src/fastapi_quickcrud_codegen/model/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/model/common_builder.py b/src/fastapi_quickcrud_codegen/model/common_builder.py deleted file mode 100644 index 1d64867..0000000 --- a/src/fastapi_quickcrud_codegen/model/common_builder.py +++ /dev/null @@ -1,134 +0,0 @@ -import inspect -import sys -from pathlib import Path -from textwrap import dedent -from typing import ClassVar - -import importmagic -import jinja2 -from importmagic import SymbolIndex, Scope -from sqlalchemy import Table - - -class CommonCodeGen(): - def __init__(self): - self.code = "" - self.model_code = "" - self.index = SymbolIndex() - lib_path: list[str] = [i for i in sys.path if "fastapi_quickcrud_codegen" not in i] - self.index.build_index(lib_path) - self.import_list = "" - - # todo add tpye for template_generator - def gen(self, template_generator_method): - template_generator_method( self.import_list + "\n\n" + self.code) - - def gen_model(self, model): - if isinstance(model, Table): - raise TypeError("not support table yet") - model_code = inspect.getsource(model) - self.model_code += "\n\n\n" + model_code - - def build_app(self, *, async_mode, model_name): - mode = "async" if async_mode else "sync" - TEMPLATE_FILE_PATH: ClassVar[str] = f'route/{mode}_find_one.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'{mode}_find_one.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render( - {"model_name": model_name}) - self.code += "\n\n\n" + code - - def build_api_route(self): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/api_route.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'api_route.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render() - self.code += "\n\n\n" + code - - def build_type(self): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/typing.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'typing.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render() - self.code += "\n\n\n" + code - - def build_utils(self): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/utils.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'utils.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render() - self.import_list = """ -from fastapi_quick_crud_template.common.http_exception import QueryOperatorNotFound -from fastapi_quick_crud_template.common.typing import ExtraFieldType, ExtraFieldTypePrefix, process_type_map, \ - process_map - -""" - self.code += "\n\n\n" + code - - def build_http_exception(self): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/http_exception.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'http_exception.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render() - self.code += "\n\n\n" + code - - def build_db(self): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/db.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'db.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render() - self.code += "\n\n\n" + code - - def build_db_session(self, model_list): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/memory_sql_session.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'memory_sql_session.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render({"model_list": model_list}) - self.code += "\n\n\n" + code - - def build_app(self, model_list): - TEMPLATE_FILE_PATH: ClassVar[str] = f'common/app.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'app.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render({"model_list": model_list}) - self.code += "\n\n\n" + code diff --git a/src/fastapi_quickcrud_codegen/model/crud_builder.py b/src/fastapi_quickcrud_codegen/model/crud_builder.py deleted file mode 100644 index b20ecc1..0000000 --- a/src/fastapi_quickcrud_codegen/model/crud_builder.py +++ /dev/null @@ -1,65 +0,0 @@ -import inspect -import sys -from pathlib import Path -from textwrap import dedent -from typing import ClassVar - -import importmagic -import jinja2 -from importmagic import SymbolIndex, Scope -from sqlalchemy import Table - -from fastapi_quickcrud_codegen.generator.crud_template_generator import CrudTemplateGenerator - - -class CrudCodeGen(): - def __init__(self, file_name, model_name, tags, prefix): - self.file_name = file_name - self.code = "\n\n\n" + "api = APIRouter(tags=" + str(tags) + ',' + "prefix=" + '"' + prefix + '")' + "\n\n" - # self.index = SymbolIndex() - # lib_path: list[str] = [i for i in sys.path if "FastAPIQuickCRUD" not in i] - # self.index.build_index(lib_path) - self.model_name = model_name - self.import_list = f""" - -import copy -from http import HTTPStatus -from typing import List -from os import path - -from sqlalchemy import and_, select -from fastapi import Depends, Response, APIRouter -from sqlalchemy.sql.elements import BinaryExpression - -from fastapi_quick_crud_template.common.utils import find_query_builder -from fastapi_quick_crud_template.common.sql_session import db_session -from fastapi_quick_crud_template.model.{file_name} import ({model_name}FindOneResponseModel, - {model_name}PrimaryKeyModel, - {model_name}FindOneRequestBody, - {model_name}) - """ - - def gen(self, template_generator: CrudTemplateGenerator): - # src = dedent(self.model_code + "\n\n" +self.code) - # scope = Scope.from_source(src) - # - # unresolved, unreferenced = scope.find_unresolved_and_unreferenced_symbols() - # a = importmagic.get_update(src, self.index, unresolved, unreferenced) - # python_source = importmagic.update_imports(src, self.index, unresolved, unreferenced) - # template_generator.add_route(self.file_name, python_source) - template_generator.add_route(self.file_name, self.import_list + "\n\n" + self.code) - - def build_find_one_route(self, *, async_mode, path): - mode = "async" if async_mode else "sync" - TEMPLATE_FILE_PATH: ClassVar[str] = f'route/{mode}_find_one.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = f'{mode}_find_one.jinja2' - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render( - {"model_name": self.model_name, "path": path}) - self.code += "\n\n\n" + code - diff --git a/src/fastapi_quickcrud_codegen/model/model_builder.py b/src/fastapi_quickcrud_codegen/model/model_builder.py deleted file mode 100644 index 104ab7a..0000000 --- a/src/fastapi_quickcrud_codegen/model/model_builder.py +++ /dev/null @@ -1,133 +0,0 @@ -import inspect -import sys -from pathlib import Path -from textwrap import dedent -from typing import ClassVar - -import importmagic -import jinja2 -from importmagic import SymbolIndex, Scope -from sqlalchemy import Table - -from fastapi_quickcrud_codegen.generator.model_template_generator import model_template_gen - - -class ModelCodeGen(): - def __init__(self, file_name, db_type): - self.file_name = file_name - self.table_list = {} - self.code = "" - self.model_code = "" - self.index = SymbolIndex() - lib_path: list[str] = [i for i in sys.path if "FastAPIQuickCRUD" not in i] - self.index.build_index(lib_path) - self.import_list = f""" -import uuid -from dataclasses import dataclass -from datetime import datetime, timedelta, date, time -from decimal import Decimal -from typing import Optional, List, Union, NewType - -from fastapi import Query, Body -from sqlalchemy import * -from sqlalchemy.dialects.{db_type} import * - -from fastapi_quick_crud_template.common.utils import value_of_list_to_str, ExcludeUnsetBaseModel, filter_none -from fastapi_quick_crud_template.common.db import Base -from fastapi_quick_crud_template.common.typing import ItemComparisonOperators, PGSQLMatchingPatternInString, \ - ExtraFieldTypePrefix, RangeToComparisonOperators, MatchingPatternInStringBase, RangeFromComparisonOperators -""" - - def gen(self): - # src = dedent(self.model_code + "\n\n" +self.code) - # scope = Scope.from_source(src) - # - # unresolved, unreferenced = scope.find_unresolved_and_unreferenced_symbols() - # python_source = importmagic.update_imports(src, self.index, unresolved, unreferenced) - # model_template_gen.add_model(self.file_name, python_source) - return model_template_gen.add_model(self.file_name, self.import_list + "\n\n" + self.model_code + "\n\n" + self.code) - - def gen_model(self, model): - if isinstance(model, Table): - raise TypeError("not support table yet") - model_code = inspect.getsource(model) - self.model_code += "\n\n\n" + model_code - - def build_base_model(self, *, class_name, fields, description=None, orm_mode=True, - value_of_list_to_str_columns=None, filter_none=None): - TEMPLATE_FILE_PATH: ClassVar[str] = 'pydantic/BaseModel.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = "BaseModel.jinja2" - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render( - {"class_name": class_name, "fields": fields, "description": description, "orm_mode": orm_mode, - "value_of_list_to_str_columns": value_of_list_to_str_columns, "filter_none": filter_none}) - self.table_list[class_name] = code - self.code += "\n\n\n" + code - - def build_base_model_root(self, *, class_name, field, description=None, base_model="BaseModel", - value_of_list_to_str_columns=None, filter_none=None): - - if class_name in self.table_list: - return self.table_list[class_name] - TEMPLATE_FILE_PATH: ClassVar[str] = 'pydantic/BaseModel.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = "BaseModel_root.jinja2" - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render( - {"class_name": class_name, "field": field, "description": description, "base_model": base_model,"value_of_list_to_str_columns": value_of_list_to_str_columns, "filter_none": filter_none}) - self.table_list[class_name] = code - self.code += "\n\n\n" + code - - def build_dataclass(self, *, class_name, fields, description=None, value_of_list_to_str_columns=None, - filter_none=None): - if class_name in self.table_list: - return self.table_list[class_name] - TEMPLATE_FILE_PATH: ClassVar[str] = 'pydantic/BaseModel.jinja2' - template_file_path = Path(TEMPLATE_FILE_PATH) - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = "dataclass.jinja2" - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render({"class_name": class_name, "fields": fields, "description": description, - "value_of_list_to_str_columns": value_of_list_to_str_columns, - "filter_none": filter_none}) - self.code += "\n\n\n" + code - - def build_enum(self, *, class_name, fields, description=None): - if class_name in self.table_list: - return self.table_list[class_name] - TEMPLATE_FILE_PATH: ClassVar[str] = '' - template_file_path = Path(TEMPLATE_FILE_PATH) - BASE_CLASS: ClassVar[str] = 'pydantic.BaseModel' - - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = "Enum.jinja2" - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render({"class_name": class_name, "fields": fields, "description": description}) - self.table_list[class_name] = code - self.code += "\n\n\n" + code - - def build_constant(self, *, constants): - TEMPLATE_FILE_PATH: ClassVar[str] = '' - template_file_path = Path(TEMPLATE_FILE_PATH) - TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template' - templateLoader = jinja2.FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = "Constant.jinja2" - template = templateEnv.get_template(TEMPLATE_FILE) - code = template.render({"constants": constants}) - self.code += "\n\n\n" + code - diff --git a/src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 b/src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 deleted file mode 100644 index e361c9a..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/Constant.jinja2 +++ /dev/null @@ -1,9 +0,0 @@ - -{%- for constant in constants %} - {% if constant[1] is iterable and (constant[1] is not string and constant[1] is not mapping )%} -{{ constant[0] }} = "{{ constant[1] | join('", "') }}" - {% else %} -{{ constant[0] }} = "{{ constant[1] }}" - {% endif %} - -{%- endfor -%} diff --git a/src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 b/src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 deleted file mode 100644 index 04d53e3..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/Enum.jinja2 +++ /dev/null @@ -1,12 +0,0 @@ -{% for decorator in decorators -%} -{{ decorator }} -{% endfor -%} -class {{ class_name }}(Enum): -{%- if description %} - """ - {{ description }} - """ -{%- endif %} -{%- for field in fields %} - {{ field[0] }} = {{ field[1] }} -{%- endfor -%} diff --git a/src/fastapi_quickcrud_codegen/model/template/__init__.py b/src/fastapi_quickcrud_codegen/model/template/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/model/template/common/__init__.py b/src/fastapi_quickcrud_codegen/model/template/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 deleted file mode 100644 index ce1effb..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/api_route.jinja2 +++ /dev/null @@ -1,3 +0,0 @@ -from fastapi import APIRouter - -api_routes: List[APIRouter] = [] diff --git a/src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 deleted file mode 100644 index 2fd8bd5..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/app.jinja2 +++ /dev/null @@ -1,15 +0,0 @@ -import uvicorn -from fastapi import FastAPI -{% for model in model_list -%} -from fastapi_quick_crud_template.route.{{ model["model_name"] }} import api as {{ model["model_name"] }}_router - -{%- endfor %} -app = FastAPI() - -[app.include_router(api_route) for api_route in [ -{% for model in model_list -%} -{{ model["model_name"] }}_router, -{%- endfor %} -]] - -uvicorn.run(app, host="0.0.0.0", port=8000, debug=False) diff --git a/src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 deleted file mode 100644 index 711eda2..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/db.jinja2 +++ /dev/null @@ -1,4 +0,0 @@ -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() -metadata = Base.metadata \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 deleted file mode 100644 index e428c7e..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/http_exception.jinja2 +++ /dev/null @@ -1,71 +0,0 @@ -from fastapi import HTTPException - - -class FindOneApiNotRegister(HTTPException): - pass - - -class CRUDBuilderException(BaseException): - pass - - -class RequestMissing(CRUDBuilderException): - pass - - -class PrimaryMissing(CRUDBuilderException): - pass - - -class UnknownOrderType(CRUDBuilderException): - pass - - -class UpdateColumnEmptyException(CRUDBuilderException): - pass - - -class UnknownColumn(CRUDBuilderException): - pass - - -class QueryOperatorNotFound(CRUDBuilderException): - pass - - -class UnknownError(CRUDBuilderException): - pass - - -class ConflictColumnsCannotHit(CRUDBuilderException): - pass - - -class MultipleSingleUniqueNotSupportedException(CRUDBuilderException): - pass - - -class SchemaException(CRUDBuilderException): - pass - - -class CompositePrimaryKeyConstraintNotSupportedException(CRUDBuilderException): - pass - - -class MultiplePrimaryKeyNotSupportedException(CRUDBuilderException): - pass - - -class ColumnTypeNotSupportedException(CRUDBuilderException): - pass - - -class InvalidRequestMethod(CRUDBuilderException): - pass - -class FDDRestHTTPException(HTTPException): - """Baseclass for all HTTP exceptions in FDD Rest API. This exception can be called as WSGI - application to render a default error page or you can catch the subclasses - of it independently and render nicer error messages. - """ diff --git a/src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 deleted file mode 100644 index 436c01f..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/memory_sql_session.jinja2 +++ /dev/null @@ -1,91 +0,0 @@ -import asyncio -from typing import Generator - -from sqlalchemy import create_engine -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import declarative_base, sessionmaker -from sqlalchemy.pool import StaticPool - - -{% for model in model_list -%} - -from fastapi_quick_crud_template.model.{{ model["model_name"] }} import {{ model["file_name"] }} -{%- endfor %} - -{%- if is_memory_sql %} - # please manually update if don't want to use in-memory db -{%- endif %} - -{%- if async_mode %} - - -SQLALCHEMY_DATABASE_URL = f"sqlite+aiosqlite://" - -engine = create_async_engine(SQLALCHEMY_DATABASE_URL, - future=True, - echo=True, - pool_pre_ping=True, - pool_recycle=7200, - connect_args={"check_same_thread": False}, - poolclass=StaticPool) -session = sessionmaker(autocommit=False, - autoflush=False, - bind=engine, - class_=AsyncSession) -{%- else %} - - -SQLALCHEMY_DATABASE_URL = f"sqlite://" - -engine = create_engine(SQLALCHEMY_DATABASE_URL, - future=True, - echo=True, - pool_pre_ping=True, - pool_recycle=7200, - connect_args={"check_same_thread": False}, - poolclass=StaticPool) -session = sessionmaker(bind=engine, autocommit=False) -{%- endif %} - - - -{%- if async_mode %} - -{% for model in model_list -%} -async def create_table(engine, {{ model["file_name"] }}): - async with engine.begin() as conn: - wait conn.run_sync(model._sa_registry.metadata.create_all) - - loop = asyncio.get_event_loop() - loop.run_until_complete(create_table(engine, Mode)) -{%- endfor %} - - -{%- else %} - - -{% for model in model_list -%} -{{ model["file_name"] }}.__table__.create(engine, checkfirst=True) -{%- endfor %} -{%- endif %} - - -{%- if async_mode %} - - -async def db_session(): - async with session() as session: - yield session -{%- else %} - - -def db_session() -> Generator: - try: - db = session() - yield db - except Exception as e: - db.rollback() - raise e - finally: - db.close() -{%- endif %} diff --git a/src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 deleted file mode 100644 index ce1effb..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/route_container.jinja2 +++ /dev/null @@ -1,3 +0,0 @@ -from fastapi import APIRouter - -api_routes: List[APIRouter] = [] diff --git a/src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 deleted file mode 100644 index b51e42a..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/typing.jinja2 +++ /dev/null @@ -1,154 +0,0 @@ -from enum import Enum, auto -from itertools import chain -from sqlalchemy import or_ -from strenum import StrEnum - -class CrudMethods(Enum): - FIND_ONE = "FIND_ONE" - FIND_MANY = "FIND_MANY" - UPDATE_ONE = "UPDATE_ONE" - UPDATE_MANY = "UPDATE_MANY" - PATCH_ONE = "PATCH_ONE" - PATCH_MANY = "PATCH_MANY" - UPSERT_ONE = "UPSERT_ONE" - UPSERT_MANY = "UPSERT_MANY" - CREATE_ONE = "CREATE_ONE" - CREATE_MANY = "CREATE_MANY" - DELETE_ONE = "DELETE_ONE" - DELETE_MANY = "DELETE_MANY" - POST_REDIRECT_GET = "POST_REDIRECT_GET" - FIND_ONE_WITH_FOREIGN_TREE = "FIND_ONE_WITH_FOREIGN_TREE" - FIND_MANY_WITH_FOREIGN_TREE = "FIND_MANY_WITH_FOREIGN_TREE" - - @staticmethod - def get_table_full_crud_method(): - return [CrudMethods.FIND_MANY, CrudMethods.CREATE_MANY, CrudMethods.UPDATE_MANY, CrudMethods.PATCH_MANY, - CrudMethods.DELETE_MANY] - - @staticmethod - def get_declarative_model_full_crud_method(): - return [CrudMethods.FIND_MANY, CrudMethods.FIND_ONE, - CrudMethods.UPDATE_MANY, CrudMethods.UPDATE_ONE, - CrudMethods.PATCH_MANY, CrudMethods.PATCH_ONE, CrudMethods.CREATE_MANY, - CrudMethods.DELETE_MANY, CrudMethods.DELETE_ONE, CrudMethods.FIND_ONE_WITH_FOREIGN_TREE, - CrudMethods.FIND_MANY_WITH_FOREIGN_TREE] - - - -class ExtraFieldTypePrefix(StrEnum): - List = '____list' - From = '____from' - To = '____to' - Str = '____str' - - - -class ExtraFieldType(StrEnum): - Comparison_operator = '_____comparison_operator' - Matching_pattern = '_____matching_pattern' - - - -class MatchingPatternInStringBase(StrEnum): - case_insensitive = auto() - case_sensitive = auto() - not_case_insensitive = auto() - not_case_sensitive = auto() - contains = auto() - - -class PGSQLMatchingPattern(StrEnum): - match_regex_with_case_sensitive = auto() - match_regex_with_case_insensitive = auto() - does_not_match_regex_with_case_sensitive = auto() - does_not_match_regex_with_case_insensitive = auto() - similar_to = auto() - not_similar_to = auto() - - -PGSQLMatchingPatternInString = StrEnum('PGSQLMatchingPatternInString', - {Pattern: auto() for Pattern in - chain(MatchingPatternInStringBase, PGSQLMatchingPattern)}) - -process_type_map = { - ExtraFieldTypePrefix.List: ExtraFieldType.Comparison_operator, - ExtraFieldTypePrefix.From: ExtraFieldType.Comparison_operator, - ExtraFieldTypePrefix.To: ExtraFieldType.Comparison_operator, - ExtraFieldTypePrefix.Str: ExtraFieldType.Matching_pattern, -} - -class RangeFromComparisonOperators(StrEnum): - Greater_than = auto() - Greater_than_or_equal_to = auto() - - -class RangeToComparisonOperators(StrEnum): - Less_than = auto() - Less_than_or_equal_to = auto() - - -class ItemComparisonOperators(StrEnum): - Equal = auto() - Not_equal = auto() - In = auto() - Not_in = auto() - - -process_map = { - RangeFromComparisonOperators.Greater_than: - lambda field, value: field > value, - - RangeFromComparisonOperators.Greater_than_or_equal_to: - lambda field, value: field >= value, - - RangeToComparisonOperators.Less_than: - lambda field, value: field < value, - - RangeToComparisonOperators.Less_than_or_equal_to: - lambda field, value: field <= value, - - ItemComparisonOperators.Equal: - lambda field, values: or_(field == value for value in values), - - ItemComparisonOperators.Not_equal: - lambda field, values: or_(field != value for value in values), - - ItemComparisonOperators.In: - lambda field, values: or_(field.in_(values)), - - ItemComparisonOperators.Not_in: - lambda field, values: or_(field.notin_(values)), - - MatchingPatternInStringBase.case_insensitive: - lambda field, values: or_(field.ilike(value) for value in values), - - MatchingPatternInStringBase.case_sensitive: - lambda field, values: or_(field.like(value) for value in values), - - MatchingPatternInStringBase.not_case_insensitive: - lambda field, values: or_(field.not_ilike(value) for value in values), - - MatchingPatternInStringBase.not_case_sensitive: - lambda field, values: or_(field.not_like(value) for value in values), - - MatchingPatternInStringBase.contains: - lambda field, values: or_(field.contains(value) for value in values), - - PGSQLMatchingPatternInString.similar_to: - lambda field, values: or_(field.op("SIMILAR TO")(value) for value in values), - - PGSQLMatchingPatternInString.not_similar_to: - lambda field, values: or_(field.op("NOT SIMILAR TO")(value) for value in values), - - PGSQLMatchingPatternInString.match_regex_with_case_sensitive: - lambda field, values: or_(field.op("~")(value) for value in values), - - PGSQLMatchingPatternInString.match_regex_with_case_insensitive: - lambda field, values: or_(field.op("~*")(value) for value in values), - - PGSQLMatchingPatternInString.does_not_match_regex_with_case_sensitive: - lambda field, values: or_(field.op("!~")(value) for value in values), - - PGSQLMatchingPatternInString.does_not_match_regex_with_case_insensitive: - lambda field, values: or_(field.op("!~*")(value) for value in values) -} diff --git a/src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 b/src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 deleted file mode 100644 index 61a8201..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/common/utils.jinja2 +++ /dev/null @@ -1,103 +0,0 @@ -from typing import TypeVar, List, Union -from copy import deepcopy - -from sqlalchemy import or_ -from sqlalchemy.orm import declarative_base -from sqlalchemy.sql.elements import BinaryExpression -from pydantic import BaseModel - - -Base = TypeVar("Base", bound=declarative_base) - - -def find_query_builder(param: dict, model: Base) -> List[Union[BinaryExpression]]: - query = [] - for column_name, value in param.items(): - if ExtraFieldType.Comparison_operator in column_name or ExtraFieldType.Matching_pattern in column_name: - continue - if ExtraFieldTypePrefix.List in column_name: - type_ = ExtraFieldTypePrefix.List - elif ExtraFieldTypePrefix.From in column_name: - type_ = ExtraFieldTypePrefix.From - elif ExtraFieldTypePrefix.To in column_name: - type_ = ExtraFieldTypePrefix.To - elif ExtraFieldTypePrefix.Str in column_name: - type_ = ExtraFieldTypePrefix.Str - else: - query.append((getattr(model, column_name) == value)) - # raise Exception('known error') - continue - sub_query = [] - table_column_name = column_name.replace(type_, "") - operator_column_name = column_name + process_type_map[type_] - operators = param.get(operator_column_name, None) - if not operators: - raise QueryOperatorNotFound(f'The query operator of {column_name} not found!') - if not isinstance(operators, list): - operators = [operators] - for operator in operators: - sub_query.append(process_map[operator](getattr(model, table_column_name), value)) - query.append((or_(*sub_query))) - return query - - -def value_of_list_to_str(request_or_response_object, columns): - received_request = deepcopy(request_or_response_object.__dict__) - if isinstance(columns, str): - columns = [columns] - if 'insert' in request_or_response_object.__dict__: - insert_str_list = [] - for insert_item in request_or_response_object.__dict__['insert']: - for column in columns: - for insert_item_column, _ in insert_item.__dict__.items(): - if column in insert_item_column: - value_ = insert_item.__dict__[insert_item_column] - if value_ is not None: - if isinstance(value_, list): - str_value_ = [str(i) for i in value_] - else: - str_value_ = str(value_) - setattr(insert_item, insert_item_column, str_value_) - insert_str_list.append(insert_item) - setattr(request_or_response_object, 'insert', insert_str_list) - else: - for column in columns: - for received_column_name, _ in received_request.items(): - if column in received_column_name: - value_ = received_request[received_column_name] - if value_ is not None: - if isinstance(value_, list): - str_value_ = [str(i) for i in value_] - else: - str_value_ = str(value_) - setattr(request_or_response_object, received_column_name, str_value_) - - -def filter_none(request_or_response_object): - received_request = deepcopy(request_or_response_object.__dict__) - if 'insert' in received_request: - insert_item_without_null = [] - for received_insert in received_request['insert']: - received_insert_ = deepcopy(received_insert) - for received_insert_item, received_insert_value in received_insert_.__dict__.items(): - if hasattr(received_insert_value, '__module__'): - if received_insert_value.__module__ == 'fastapi.params' or received_insert_value is None: - delattr(received_insert, received_insert_item) - elif received_insert_value is None: - delattr(received_insert, received_insert_item) - - insert_item_without_null.append(received_insert) - setattr(request_or_response_object, 'insert', insert_item_without_null) - else: - for name, value in received_request.items(): - if hasattr(value, '__module__'): - if value.__module__ == 'fastapi.params' or value is None: - delattr(request_or_response_object, name) - elif value is None: - delattr(request_or_response_object, name) - -class ExcludeUnsetBaseModel(BaseModel): - def dict(self, *args, **kwargs): - if kwargs and kwargs.get("exclude_none") is not None: - kwargs["exclude_unset"] = True - return BaseModel.dict(self, *args, **kwargs) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 deleted file mode 100644 index d46ef23..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel.jinja2 +++ /dev/null @@ -1,38 +0,0 @@ -{% for decorator in decorators -%} -{{ decorator }} -{% endfor -%} -class {{ class_name }}(BaseModel): - """ - auto gen by FastApi quick CRUD - """ -{%- if not fields %} - pass -{%- endif %} -{%- if config %} -{%- filter indent(4) %} -{% include 'Config.jinja2' %} -{%- endfilter %} -{%- endif %} -{%- for field in fields -%} - {%- if field|length > 2 %} - {{ field[0] }}: {{ field[1] }} = {{field[2]}} - - {%- else %} - {{ field[0] }}: {{ field[1] }} - {%- endif %} -{%- endfor -%} - -{%- if value_of_list_to_str_columns or filter_none %} - def __init__(self): - {%- if value_of_list_to_str_columns %} - value_of_list_to_str(self, {{ value_of_list_to_str_columns }}) - {%- endif %} - {%- if filter_none %} - filter_none(self) - {%- endif %} -{%- endif %} - -{%- if orm_mode %} - class Config: - orm_mode = True -{%- endif %} \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 deleted file mode 100644 index 03dc8ca..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/pydantic/BaseModel_root.jinja2 +++ /dev/null @@ -1,31 +0,0 @@ -{% for decorator in decorators -%} -{{ decorator }} -{% endfor -%} -class {{ class_name }}({{ base_model }}): -{%- if description %} - """ - auto gen by FastApi quick CRUD - """ -{%- endif %} -{%- if config %} -{%- filter indent(4) %} -{% include 'Config.jinja2' %} -{%- endfilter %} -{%- endif %} -{%- if not field %} - pass -{%- else %} - __root__: List[{{ field[0] }}] -{%- endif %} - -{%- if value_of_list_to_str_columns or filter_none %} - def __init__(self): - {%- if value_of_list_to_str_columns %} - value_of_list_to_str(self, {{ value_of_list_to_str_columns }}) - {%- endif %} - {%- if filter_none %} - filter_none(self) - {%- endif %} -{%- endif %} - class Config: - orm_mode = True diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 deleted file mode 100644 index 3612790..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/pydantic/Config.jinja2 +++ /dev/null @@ -1,4 +0,0 @@ -class Config: -{%- for field_name, value in config.dict(exclude_unset=True).items() %} - {{ field_name }} = {{ value }} -{%- endfor %} \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/__init__.py b/src/fastapi_quickcrud_codegen/model/template/pydantic/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 deleted file mode 100644 index 1b147ff..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass.jinja2 +++ /dev/null @@ -1,38 +0,0 @@ -{% for decorator in decorators -%} -{{ decorator }} -{% endfor -%} -@dataclass -{%- if base_class %} -class {{ class_name }}({{ base_class }}): -{%- else %} -class {{ class_name }}: -{%- endif %} -{%- if description %} - """ - {{ description }} - """ -{%- endif %} -{%- if not fields %} - pass -{%- endif %} -{%- for field in fields -%} - {%- if field|length > 2 %} - {{ field[0] }}: {{ field[1] }} = {{field[2]}} - {%- else %} - {{ field[0] }}: {{ field[1] }} - {%- endif %} -{%- endfor -%} - - -{%- if value_of_list_to_str_columns or filter_none %} - def __post_init__(self): - """ - auto gen by FastApi quick CRUD - """ - {%- if value_of_list_to_str_columns %} - value_of_list_to_str(self, {{ value_of_list_to_str_columns }}) - {%- endif %} - {%- if filter_none %} - filter_none(self) - {%- endif %} -{%- endif %} diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 deleted file mode 100644 index ffdf367..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/pydantic/dataclass_method.jinja2 +++ /dev/null @@ -1,4 +0,0 @@ -def __post_init__(self): -{% for method in methods -%} - {{methods}}(self) -{% endfor -%} \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 b/src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 deleted file mode 100644 index ebaa0e0..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/pydantic/exclude_unset_baseModel.jinja2 +++ /dev/null @@ -1,6 +0,0 @@ - -class ExcludeUnsetBaseModel(BaseModel): - def dict(self, *args, **kwargs): - if kwargs and kwargs.get("exclude_none") is not None: - kwargs["exclude_unset"] = True - return BaseModel.dict(self, *args, **kwargs) \ No newline at end of file diff --git a/src/fastapi_quickcrud_codegen/model/template/route/__init__.py b/src/fastapi_quickcrud_codegen/model/template/route/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 b/src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 deleted file mode 100644 index 640bd4a..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/route/sync_find_one.jinja2 +++ /dev/null @@ -1,30 +0,0 @@ -@api.get("{{ path }}", status_code=200, response_model={{ model_name }}FindOneResponseModel) -def get_one_by_primary_key(response: Response, - url_param=Depends({{ model_name }}PrimaryKeyModel), - query=Depends({{ model_name }}FindOneRequestBody), - session=Depends(db_session)): - filter_list: List[BinaryExpression] = find_query_builder(param=query.__dict__, - model=UntitledTable256) - - extra_query_expression: List[BinaryExpression] = find_query_builder(param=url_param.__dict__, - model=UntitledTable256) - model = {{ model_name }} - stmt = select(*[model]).where(and_(*filter_list + extra_query_expression)) - sql_executed_result = session.execute(stmt) - - one_row_data = sql_executed_result.fetchall() - if not one_row_data: - return Response('specific data not found', status_code=HTTPStatus.NOT_FOUND) - response = [] - for i in one_row_data: - i = dict(i) - result__ = copy.deepcopy(i) - tmp = {} - for key_, value_ in result__.items(): - tmp[key_] = value_ - response.append(tmp) - if isinstance(response, list): - response = response[0] - response.headers["x-total-count"] = str(1) - session.commit() - return response diff --git a/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/__init__.py b/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 b/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 deleted file mode 100644 index ef3510f..0000000 --- a/src/fastapi_quickcrud_codegen/model/template/sqlalchemy/dataclass.jinja2 +++ /dev/null @@ -1,24 +0,0 @@ -{% for decorator in decorators -%} -{{ decorator }} -{% endfor -%} -@dataclass -{%- if base_class %} -class {{ class_name }}({{ base_class }}): -{%- else %} -class {{ class_name }}: -{%- endif %} -{%- if description %} - """ - {{ description }} - """ -{%- endif %} -{%- if not fields %} - pass -{%- endif %} -{%- for field in fields -%} - {%- if field|length > 2 %} - {{ field[0] }}: {{ field[1] }} = {{field[2]}} - {%- else %} - {{ field[0] }}: {{ field[1] }} - {%- endif %} -{%- endfor -%} diff --git a/src/fastapi_quickcrud_codegen/parse/__init__.py b/src/fastapi_quickcrud_codegen/parse/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/fastapi_quickcrud_codegen/parse/parse_import.py b/src/fastapi_quickcrud_codegen/parse/parse_import.py deleted file mode 100644 index 3d7a17a..0000000 --- a/src/fastapi_quickcrud_codegen/parse/parse_import.py +++ /dev/null @@ -1,24 +0,0 @@ -import ast -from collections import namedtuple - -Import = namedtuple("Import", ["module", "name", "alias"]) - -def get_imports(path): - with open(path) as fh: - root = ast.parse(fh.read(), path) - - for node in ast.iter_child_nodes(root): - if isinstance(node, ast.Import): - module = [] - elif isinstance(node, ast.ImportFrom): - module = node.module.split('.') - else: - continue - - for n in node.names: - if not module: - yield f'import {n.name} ' - elif n.asname: - yield f'from {".".join(module)} import {n.name} as {n.asname}' - else: - yield f'from {".".join(module)} import {n.name} '