diff --git a/.DS_Store b/.DS_Store index 54688a9..7128ca9 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/create_fastapi_project/templates/__init__.py b/create_fastapi_project/templates/__init__.py index 1a2897d..e8c4b8f 100644 --- a/create_fastapi_project/templates/__init__.py +++ b/create_fastapi_project/templates/__init__.py @@ -47,66 +47,64 @@ def install_template(root: str, template: ITemplate, app_name: str): if has_pyproject: dependencies = [ "fastapi[all]", - "fastapi-pagination[sqlalchemy]@^0.12.7", - "asyncer@^0.0.2", - "httpx@^0.24.1", + "fastapi-pagination[sqlalchemy]", + "asyncer", + "httpx", ] dev_dependencies = [ - "pytest@^7.4.0", - "mypy@^1.5.0", - "ruff@^0.0.284", - "black@^23.7.0", + "pytest", + "mypy", + "ruff", + "black", ] if template == ITemplate.langchain_basic: langchain_dependencies = [ - "langchain@^0.0.265", - "openai@^0.27.8", - "adaptive-cards-py@^0.0.7", - "google-search-results@^2.4.2", + "langchain", + "openai", + "adaptive-cards-py", + "google-search-results", ] frontend_dependencies = [ "streamlit", "websockets", ] - dependencies[0] = "fastapi[all]@^0.99.1" dependencies.extend(langchain_dependencies) if template == ITemplate.full: full_dependencies = [ - "alembic@^1.10.2", - "asyncpg@^0.27.0", - "sqlmodel@^0.0.8", - "python-jose@^3.3.0", - "cryptography@^38.0.3", - "passlib@^1.7.4", - "SQLAlchemy-Utils@^0.38.3", - "SQLAlchemy@^1.4.40", - "minio@^7.1.13", - "Pillow@^9.4.0", - "watchfiles@^0.18.1", - "asyncer@^0.0.2", - "httpx@^0.23.1", - "pandas@^1.5.3", - "openpyxl@^3.0.10", - "redis@^4.5.1", - "fastapi-async-sqlalchemy@^0.3.12", - "oso@^0.26.4", - "celery@^5.2.7", - "transformers@^4.28.1", - "requests@^2.29.0", - "wheel@^0.40.0", - "setuptools@^67.7.2", - "langchain@^0.0.262", - "openai@^0.27.5", - "celery-sqlalchemy-scheduler@^0.3.0", - "psycopg2-binary@^2.9.5", - "fastapi-limiter@^0.1.5 ", - "fastapi-pagination[sqlalchemy]@^0.11.4 ", - "fastapi-cache2[redis]@^0.2.1 ", + "alembic", + "asyncpg", + "sqlmodel", + "python-jose", + "cryptography", + "passlib", + "SQLAlchemy-Utils", + "SQLAlchemy", + "minio", + "Pillow", + "watchfiles", + "asyncer", + "httpx", + "pandas", + "openpyxl", + "redis", + "fastapi-async-sqlalchemy", + "oso", + "celery", + "transformers", + "requests", + "wheel", + "setuptools", + "langchain", + "openai", + "celery-sqlalchemy-scheduler", + "psycopg2-binary", + "fastapi-limiter", + "fastapi-pagination[sqlalchemy]", + "fastapi-cache2[redis]", ] full_dev_dependencies = [ - "pytest-asyncio@^0.21.1", + "pytest-asyncio", ] - dependencies[0] = "fastapi[all]@^0.95.2" dependencies.extend(full_dependencies) dev_dependencies.extend(full_dev_dependencies) diff --git a/create_fastapi_project/templates/basic/backend/Dockerfile b/create_fastapi_project/templates/basic/backend/Dockerfile index 491bf13..f3a4a9a 100644 --- a/create_fastapi_project/templates/basic/backend/Dockerfile +++ b/create_fastapi_project/templates/basic/backend/Dockerfile @@ -1,5 +1,6 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11-slim ENV PYTHONUNBUFFERED=1 +ENV PIP_DEFAULT_TIMEOUT=100 WORKDIR /code # Install Poetry RUN apt clean && apt update && apt install curl -y @@ -13,7 +14,7 @@ COPY app/pyproject.toml app/poetry.lock* /code/ # Allow installing dev dependencies to run tests ARG INSTALL_DEV=false -RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi" ENV PYTHONPATH=/code EXPOSE 8000 diff --git a/create_fastapi_project/templates/basic/backend/app/app/core/config.py b/create_fastapi_project/templates/basic/backend/app/app/core/config.py index 42525a7..a2f5ffb 100644 --- a/create_fastapi_project/templates/basic/backend/app/app/core/config.py +++ b/create_fastapi_project/templates/basic/backend/app/app/core/config.py @@ -10,7 +10,7 @@ class ModeEnum(str, Enum): testing = "testing" -class Settings(BaseSettings): +class Settings(BaseSettings, extra='ignore'): PROJECT_NAME: str = "app" BACKEND_CORS_ORIGINS: list[str] | list[AnyHttpUrl] MODE: ModeEnum = ModeEnum.development @@ -21,7 +21,7 @@ class Settings(BaseSettings): class Config: case_sensitive = True - env_file = os.path.expanduser("~/.env") + env_file = os.path.expanduser("../../.env") settings = Settings() diff --git a/create_fastapi_project/templates/full/backend/Dockerfile b/create_fastapi_project/templates/full/backend/Dockerfile index d096169..f3a4a9a 100644 --- a/create_fastapi_project/templates/full/backend/Dockerfile +++ b/create_fastapi_project/templates/full/backend/Dockerfile @@ -1,5 +1,6 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10-slim-2022-11-25 +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11-slim ENV PYTHONUNBUFFERED=1 +ENV PIP_DEFAULT_TIMEOUT=100 WORKDIR /code # Install Poetry RUN apt clean && apt update && apt install curl -y @@ -13,7 +14,7 @@ COPY app/pyproject.toml app/poetry.lock* /code/ # Allow installing dev dependencies to run tests ARG INSTALL_DEV=false -RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi" ENV PYTHONPATH=/code EXPOSE 8000 diff --git a/create_fastapi_project/templates/langchain_basic/backend/Dockerfile b/create_fastapi_project/templates/langchain_basic/backend/Dockerfile index 491bf13..f3a4a9a 100644 --- a/create_fastapi_project/templates/langchain_basic/backend/Dockerfile +++ b/create_fastapi_project/templates/langchain_basic/backend/Dockerfile @@ -1,5 +1,6 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11-slim ENV PYTHONUNBUFFERED=1 +ENV PIP_DEFAULT_TIMEOUT=100 WORKDIR /code # Install Poetry RUN apt clean && apt update && apt install curl -y @@ -13,7 +14,7 @@ COPY app/pyproject.toml app/poetry.lock* /code/ # Allow installing dev dependencies to run tests ARG INSTALL_DEV=false -RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi" ENV PYTHONPATH=/code EXPOSE 8000 diff --git a/create_fastapi_project/templates/langchain_basic/backend/app/app/core/config.py b/create_fastapi_project/templates/langchain_basic/backend/app/app/core/config.py index c660ac9..695239a 100644 --- a/create_fastapi_project/templates/langchain_basic/backend/app/app/core/config.py +++ b/create_fastapi_project/templates/langchain_basic/backend/app/app/core/config.py @@ -1,5 +1,6 @@ import os -from pydantic import AnyHttpUrl, BaseSettings +from pydantic import AnyHttpUrl, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict from enum import Enum @@ -10,18 +11,26 @@ class ModeEnum(str, Enum): class Settings(BaseSettings): - PROJECT_NAME: str = "app" - BACKEND_CORS_ORIGINS: list[str] | list[AnyHttpUrl] MODE: ModeEnum = ModeEnum.development + PROJECT_NAME: str = "app" API_VERSION: str = "v1" API_V1_STR: str = f"/api/{API_VERSION}" OPENAI_API_KEY: str UNSPLASH_API_KEY: str SERP_API_KEY: str - class Config: - case_sensitive = True - env_file = os.path.expanduser("~/.env") + BACKEND_CORS_ORIGINS: list[str] | list[AnyHttpUrl] + @field_validator("BACKEND_CORS_ORIGINS") + def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, (list, str)): + return v + raise ValueError(v) + + model_config = SettingsConfigDict( + case_sensitive=True, env_file=os.path.expanduser("~/.env") + ) settings = Settings() diff --git a/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/message_schema.py b/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/message_schema.py index cbacc6b..560dac8 100644 --- a/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/message_schema.py +++ b/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/message_schema.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from app.utils.uuid6 import uuid7 from typing import Any @@ -19,20 +19,20 @@ class IChatResponse(BaseModel): type: str suggested_responses: list[str] = [] - @validator("id", "message_id", pre=True, allow_reuse=True) + @field_validator("id", "message_id") def check_ids(cls, v): if v == "" or v is None: return str(uuid7()) return v - @validator("sender") + @field_validator("sender") def sender_must_be_bot_or_you(cls, v): if v not in ["bot", "you"]: raise ValueError("sender must be bot or you") return v - @validator("type") + @field_validator("type") def validate_message_type(cls, v): if v not in ["start", "stream", "end", "error", "info"]: raise ValueError("type must be start, stream or end") - return v + return v \ No newline at end of file diff --git a/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/response_schema.py b/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/response_schema.py index 8574fb9..c01c913 100644 --- a/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/response_schema.py +++ b/create_fastapi_project/templates/langchain_basic/backend/app/app/schemas/response_schema.py @@ -4,7 +4,7 @@ from fastapi_pagination import Params, Page from fastapi_pagination.bases import AbstractPage, AbstractParams from pydantic import Field -from pydantic.generics import GenericModel +from pydantic import BaseModel DataType = TypeVar("DataType") T = TypeVar("T") @@ -12,15 +12,17 @@ class PageBase(Page[T], Generic[T]): previous_page: int | None = Field( - None, description="Page number of the previous page" + default=None, description="Page number of the previous page" + ) + next_page: int | None = Field( + default=None, description="Page number of the next page" ) - next_page: int | None = Field(None, description="Page number of the next page") -class IResponseBase(GenericModel, Generic[T]): +class IResponseBase(BaseModel, Generic[T]): message: str = "" - meta: dict = {} - data: T | None + meta: dict | Any | None = {} + data: T | None = None class IGetResponsePaginated(AbstractPage[T], Generic[T]): diff --git a/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/exceptions/__init__.py b/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/exceptions/__init__.py deleted file mode 100644 index 10958ef..0000000 --- a/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/exceptions/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .common_exception import ( - ContentNoChangeException, - IdNotFoundException, - NameExistException, - NameNotFoundException, -) -from .user_exceptions import UserSelfDeleteException -from .user_follow_exceptions import ( - SelfFollowedException, - UserFollowedException, - UserNotFollowedException, -) diff --git a/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/exceptions/common_exception.py b/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/exceptions/common_exception.py deleted file mode 100644 index 55e7dad..0000000 --- a/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/exceptions/common_exception.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import Any, Dict, Generic, Optional, Type, TypeVar, Union -from uuid import UUID -from fastapi import HTTPException, status -from sqlmodel import SQLModel - -ModelType = TypeVar("ModelType", bound=SQLModel) - - -class ContentNoChangeException(HTTPException): - def __init__( - self, - detail: Any = None, - headers: Optional[Dict[str, Any]] = None, - ) -> None: - super().__init__( - status_code=status.HTTP_400_BAD_REQUEST, detail=detail, headers=headers - ) - - -class IdNotFoundException(HTTPException, Generic[ModelType]): - def __init__( - self, - model: Type[ModelType], - id: Optional[Union[UUID, str]] = None, - headers: Optional[Dict[str, Any]] = None, - ) -> None: - if id: - super().__init__( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Unable to find the {model.__name__} with id {id}.", - headers=headers, - ) - return - - super().__init__( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{model.__name__} id not found.", - headers=headers, - ) - - -class NameNotFoundException(HTTPException, Generic[ModelType]): - def __init__( - self, - model: Type[ModelType], - name: Optional[str] = None, - headers: Optional[Dict[str, Any]] = None, - ) -> None: - if name: - super().__init__( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Unable to find the {model.__name__} named {name}.", - headers=headers, - ) - else: - super().__init__( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{model.__name__} name not found.", - headers=headers, - ) - - -class NameExistException(HTTPException, Generic[ModelType]): - def __init__( - self, - model: Type[ModelType], - name: Optional[str] = None, - headers: Optional[Dict[str, Any]] = None, - ) -> None: - if name: - super().__init__( - status_code=status.HTTP_409_CONFLICT, - detail=f"The {model.__name__} name {name} already exists.", - headers=headers, - ) - return - - super().__init__( - status_code=status.HTTP_409_CONFLICT, - detail=f"The {model.__name__} name already exists.", - headers=headers, - ) diff --git a/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/partial.py b/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/partial.py index 9a4d300..b703a0d 100644 --- a/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/partial.py +++ b/create_fastapi_project/templates/langchain_basic/backend/app/app/utils/partial.py @@ -1,20 +1,50 @@ # /~https://github.com/pydantic/pydantic/issues/1223 # /~https://github.com/pydantic/pydantic/pull/3179 -# Todo migrate to pydanticv2 partial -import inspect -from pydantic import BaseModel - - -def optional(*fields): - def dec(_cls): - for field in fields: - _cls.__fields__[field].required = False - if _cls.__fields__[field].default: - _cls.__fields__[field].default = None - return _cls - - if fields and inspect.isclass(fields[0]) and issubclass(fields[0], BaseModel): - cls = fields[0] - fields = cls.__fields__ - return dec(cls) - return dec +# /~https://github.com/pydantic/pydantic/issues/1673 + +from copy import deepcopy +from typing import Any, Callable, Optional, Type, TypeVar +from pydantic import BaseModel, create_model +from pydantic.fields import FieldInfo + +Model = TypeVar("Model", bound=Type[BaseModel]) + + +def optional(without_fields: list[str] | None = None) -> Callable[[Model], Model]: + """A decorator that create a partial model. + + Args: + model (Type[BaseModel]): BaseModel model. + + Returns: + Type[BaseModel]: ModelBase partial model. + """ + if without_fields is None: + without_fields = [] + + def wrapper(model: Type[Model]) -> Type[Model]: + base_model: Type[Model] = model + + def make_field_optional( + field: FieldInfo, default: Any = None + ) -> tuple[Any, FieldInfo]: + new = deepcopy(field) + new.default = default + new.annotation = Optional[field.annotation] + return new.annotation, new + + if without_fields: + base_model = BaseModel + + return create_model( + model.__name__, + __base__=base_model, + __module__=model.__module__, + **{ + field_name: make_field_optional(field_info) + for field_name, field_info in model.model_fields.items() + if field_name not in without_fields + }, + ) + + return wrapper diff --git a/poetry.lock b/poetry.lock index ac7c3b7..4a02ad8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "black" @@ -329,27 +329,53 @@ files = [ [[package]] name = "typer" -version = "0.9.0" +version = "0.12.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "typer-0.12.0-py3-none-any.whl", hash = "sha256:0441a0bb8962fb4383b8537ada9f7eb2d0deda0caa2cfe7387cc221290f617e4"}, + {file = "typer-0.12.0.tar.gz", hash = "sha256:900fe786ce2d0ea44653d3c8ee4594a22a496a3104370ded770c992c5e3c542d"}, +] + +[package.dependencies] +typer-cli = "0.12.0" +typer-slim = {version = "0.12.0", extras = ["standard"]} + +[[package]] +name = "typer-cli" +version = "0.12.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" files = [ - {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, - {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, + {file = "typer_cli-0.12.0-py3-none-any.whl", hash = "sha256:7b7e2dd49f59974bb5a869747045d5444b17bffb851e006cd424f602d3578104"}, + {file = "typer_cli-0.12.0.tar.gz", hash = "sha256:603ed3d5a278827bd497e4dc73a39bb714b230371c8724090b0de2abdcdd9f6e"}, ] [package.dependencies] -click = ">=7.1.1,<9.0.0" -colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} -rich = {version = ">=10.11.0,<14.0.0", optional = true, markers = "extra == \"all\""} -shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} +typer-slim = {version = "0.12.0", extras = ["standard"]} + +[[package]] +name = "typer-slim" +version = "0.12.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer_slim-0.12.0-py3-none-any.whl", hash = "sha256:ddd7042b29a32140528caa415750bcae54113ba0c32270ca11a6f64069ddadf9"}, + {file = "typer_slim-0.12.0.tar.gz", hash = "sha256:3e8a3f17286b173d76dca0fd4e02651c9a2ce1467b3754876b1ac4bd72572beb"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = {version = ">=10.11.0", optional = true, markers = "extra == \"standard\""} +shellingham = {version = ">=1.3.0", optional = true, markers = "extra == \"standard\""} typing-extensions = ">=3.7.4.3" [package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +all = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] +standard = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] [[package]] name = "typing-extensions" @@ -376,4 +402,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "6838aaa0eb297bf7e0010661d9126527119779b725539479016c1f27f99402cd" +content-hash = "2ce9e362353b307cffb6fdcae5867d4c085529053081b5b8d68becbb37750211" diff --git a/pyproject.toml b/pyproject.toml index 9ef31c3..7773bae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ create-fastapi-project = "create_fastapi_project.main:app" [tool.poetry.dependencies] python = "^3.10" -typer = {extras = ["all"], version = "^0.9.0"} +typer = {extras = ["all"], version = "^0.12.0"} questionary = "^2.0.0" toml = "^0.10.2" python-dotenv = "^1.0.0"