commit da3b1a9f34ccb6de26b738ed586e592098cdf3b1 Author: leo Date: Sat Nov 4 18:10:56 2023 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96a8a11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +data/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1fa59bd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM tiangolo/uvicorn-gunicorn:python3.10 + +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple +COPY . /app + +EXPOSE 80 + +ENV PATH="/usr/bin:/app:${PATH}" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d59ec28 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# fastapi 简单项目模板 + +## 初始化 + +```shell +pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +修改 `alembic.ini:63` 更改为你的mysql配置 +修改 `app/core/config.py:11` 为同样的配置 +执行 + +```shell +alembic upgrade head +``` + +后续如果要对数据表进行修改, 建议使用alembic +基本使用请查阅官方文档 + +项目入口: main.py + +## 提一嘴 + +复杂项目请参考[full-stack-fastapi-postgresql](https://github.com/tiangolo/full-stack-fastapi-postgresql) + +或直接运行 + +```shell +pip install cookiecutter +cookiecutter https://github.com/tiangolo/full-stack-fastapi-postgresql +``` + diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..13bff03 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,110 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1:3306/model?charset=utf8mb4 + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console +qualname = + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..36112a3 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/67efc49a0bfa_add_field_prompt_to_form.py b/alembic/versions/67efc49a0bfa_add_field_prompt_to_form.py new file mode 100644 index 0000000..58e0a82 --- /dev/null +++ b/alembic/versions/67efc49a0bfa_add_field_prompt_to_form.py @@ -0,0 +1,26 @@ +"""add field prompt to form + +Revision ID: 67efc49a0bfa +Revises: bb12537357fe +Create Date: 2023-10-31 15:46:54.641544 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '67efc49a0bfa' +down_revision: Union[str, None] = 'bb12537357fe' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('form', sa.Column('prompt', sa.String(length=2048), nullable=True)) + + +def downgrade() -> None: + op.drop_column('form', 'prompt') diff --git a/alembic/versions/bb12537357fe_add_table_form_result.py b/alembic/versions/bb12537357fe_add_table_form_result.py new file mode 100644 index 0000000..c2d8501 --- /dev/null +++ b/alembic/versions/bb12537357fe_add_table_form_result.py @@ -0,0 +1,55 @@ +"""add_table_form_result + +Revision ID: bb12537357fe +Revises: +Create Date: 2023-10-15 21:41:49.174995 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import TIMESTAMP, func, text + +# revision identifiers, used by Alembic. +revision: str = 'bb12537357fe' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + op.create_table('form', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('base_prompt', sa.String(length=1024), nullable=True), + sa.Column('p_choice', sa.String(length=255), nullable=True), + sa.Column('role', sa.String(length=255), nullable=True), + sa.Column('uuid', sa.String(length=255), nullable=True), + sa.Column('name', sa.String(length=1024), nullable=True), + sa.Column('desc', sa.String(length=2048), nullable=True), + sa.Column('price', sa.Float(), nullable=True), + sa.Column('favorable', sa.String(length=1024), nullable=True), + sa.Column('remark', sa.String(length=1024), nullable=True), + sa.Column('otherPrompt', sa.String(length=1024), nullable=True), + sa.Column('lang', sa.String(length=1024), nullable=True), + sa.Column('type', sa.String(length=1024), nullable=True), + sa.Column('created_at', TIMESTAMP, server_default=func.now()), + sa.Column('updated_at', TIMESTAMP, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('result', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('prompt', sa.String(length=2048), nullable=True), + sa.Column('uuid', sa.String(length=255), nullable=True), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('value', sa.String(length=4096), nullable=True), + sa.Column('lang', sa.String(length=1024), nullable=True), + sa.Column('created_at', TIMESTAMP, server_default=func.now()), + sa.Column('updated_at', TIMESTAMP, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('form') + op.drop_table('result') diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..76fd35c --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from fastapi import APIRouter + +import app.api.form +import app.api.result +import app.api.bench + +api_router = APIRouter() + +api_router.include_router(bench.router, prefix='/benches', tags=['benches']) +api_router.include_router(form.router, prefix='/forms', tags=['forms']) +api_router.include_router(result.router, prefix='/results', tags=['results']) diff --git a/app/api/bench.py b/app/api/bench.py new file mode 100644 index 0000000..ac6e7ac --- /dev/null +++ b/app/api/bench.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from fastapi import APIRouter, Body, Depends +from loguru import logger +from sqlalchemy.orm import Session + +from app.db.base import get_db +from app.crud import form_crud, result_crud +from app.schemas import Bench +from app.schemas.form import Form + +router = APIRouter() + + +@router.get("/forms", response_model=list[Form]) +def read_bench_forms(db: Session = Depends(get_db), limit: int = 10): + return form_crud.form.get_last_bench(db, num=limit) + + +@router.post("/result") +def read_bench_forms(uuid: str = Body(), db: Session = Depends(get_db), ): + logger.debug(f"{uuid=}") + result_dbs = result_crud.result.get_by_uuid(db, uuid=uuid) + res = { + "百川": {"value": ""}, + "ChatGPT": {"value": ""}, + "MyTwins": {"value": ""}, + } + for result in result_dbs: + res[result.name] = result + return res + + +@router.get("/", response_model=list[Bench]) +def read_forms(db: Session = Depends(get_db), limit: int = 10): + res = [] + form_dbs = form_crud.form.get_last_bench(db, num=limit) + for form_db in form_dbs: + res.append( + Bench( + form=form_db, + results=result_crud.result.get_by_uuid(db, uuid=form_db.uuid) + )) + return res diff --git a/app/api/form.py b/app/api/form.py new file mode 100644 index 0000000..7f2cd1f --- /dev/null +++ b/app/api/form.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.db.base import get_db +from app.crud import form_crud +from app.schemas import Form, FormCreate, FormUpdate + +router = APIRouter() + + +@router.put("/{item_id}", response_model=Form) +def update_form(item_id: int, form_in: FormUpdate, db: Session = Depends(get_db)): + form_db = form_crud.form.get(db, item_id) + if not form_db: + raise HTTPException(status_code=404, detail="Form not found") + return form_crud.form.update(db=db, db_obj=form_db, obj_in=form_in) + + +@router.delete("/{item_id}") +def delete_form(item_id: int, db: Session = Depends(get_db)): + form_db = form_crud.form.get(db, item_id) + if not form_db: + raise HTTPException(status_code=404, detail="Form not found") + return form_crud.form.remove(db=db, id=item_id) + + +@router.get("/{item_id}", response_model=Form) +def read_form(item_id: int, db: Session = Depends(get_db)): + return form_crud.form.get(db, item_id) + + +@router.post("/", response_model=Form) +def create_form(form_in: FormCreate, db: Session = Depends(get_db)): + form_db = form_crud.form.create(db=db, obj_in=form_in) + return form_db + + +@router.get("/", response_model=list[Form]) +def read_forms(db: Session = Depends(get_db), skip: int = 0, limit: int = 100): + return form_crud.form.get_multi(db, skip=skip, limit=limit) diff --git a/app/api/result.py b/app/api/result.py new file mode 100644 index 0000000..e288177 --- /dev/null +++ b/app/api/result.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.db.base import get_db +from app.crud import result_crud +from app.schemas import Result, ResultCreate, ResultUpdate + +router = APIRouter() + + +@router.put("/{item_id}", response_model=Result) +def update_result(item_id: int, result_in: ResultUpdate, db: Session = Depends(get_db)): + result_db = result_crud.result.get(db, item_id) + if not result_db: + raise HTTPException(status_code=404, detail="Result not found") + return result_crud.result.update(db=db, db_obj=result_db, obj_in=result_in) + + +@router.delete("/{item_id}") +def delete_result(item_id: int, db: Session = Depends(get_db)): + result_db = result_crud.result.get(db, item_id) + if not result_db: + raise HTTPException(status_code=404, detail="Result not found") + return result_crud.result.remove(db=db, id=item_id) + + +@router.get("/{item_id}", response_model=Result) +def read_result(item_id: int, db: Session = Depends(get_db)): + return result_crud.result.get(db, item_id) + + +@router.post("/", response_model=Result) +def create_result(result_in: ResultCreate, db: Session = Depends(get_db)): + result_db = result_crud.result.create(db=db, obj_in=result_in) + return result_db + + +@router.get("/", response_model=list[Result]) +def read_results(db: Session = Depends(get_db), skip: int = 0, limit: int = 100): + return result_crud.result.get_multi(db, skip=skip, limit=limit) diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..c53f601 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..f4f6790 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +import os + +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + mysql_dsn: str = 'mysql+pymysql://root:123456@127.0.0.1:3306/model?charset=utf8mb4' + + +settings = Settings() diff --git a/app/crud/__init__.py b/app/crud/__init__.py new file mode 100644 index 0000000..c53f601 --- /dev/null +++ b/app/crud/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- diff --git a/app/crud/base.py b/app/crud/base.py new file mode 100644 index 0000000..8a7d703 --- /dev/null +++ b/app/crud/base.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union + +from loguru import logger +from pydantic import BaseModel +from sqlalchemy import select +from sqlalchemy.orm import Session +from sqlalchemy.sql import Select + +from app.db.base import Base + +ModelType = TypeVar("ModelType", bound=Base) +CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) + + +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + def __init__(self, model: Type[ModelType]): + """ + CRUD object with default methods to Create, Read, Update, Delete (CRUD). + + **Parameters** + + * `model`: A SQLAlchemy model class + * `schema`: A Pydantic model (schema) class + """ + self.model = model + + def get(self, db: Session, id: Any) -> Optional[ModelType]: + return db.query(self.model).filter(self.model.id == id).first() + + def page_query(self) -> Select: + return select(self.model) + + def get_multi( + self, db: Session, *, skip: int = 0, limit: int = 100 + ) -> List[ModelType]: + return db.query(self.model).offset(skip).limit(limit).all() + + def get_multi_query(self) -> Select: + return select(self.model) + + def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: + obj_in_data = obj_in.model_dump() + logger.debug(f"{obj_in_data=}") + db_obj = self.model(**obj_in_data) # type: ignore + db.add(db_obj) + db.commit() + db.refresh(db_obj) + logger.debug(f"created {self.model.__name__}: {db_obj.id=}") + return db_obj + + def update( + self, + db: Session, + *, + db_obj: ModelType, + obj_in: Union[UpdateSchemaType, Dict[str, Any]] + ) -> ModelType: + obj_data = obj_in.model_dump() + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.model_dump(exclude_unset=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def remove(self, db: Session, *, id: int) -> ModelType: + obj = db.query(self.model).get(id) + db.delete(obj) + db.commit() + return obj + + def get_by_ids(self, db: Session, ids: list[int]): + return db.query(self.model).filter(self.model.id.in_(ids)).all() diff --git a/app/crud/form_crud.py b/app/crud/form_crud.py new file mode 100644 index 0000000..101f3f7 --- /dev/null +++ b/app/crud/form_crud.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +from sqlalchemy import and_, func +from sqlalchemy.orm import Session + +from app.models.form import Form +from app.schemas import FormCreate, FormUpdate +from app.crud.base import CRUDBase + + +class CRUDForm(CRUDBase[Form, FormUpdate, FormCreate]): + def get_last_bench(self, db: Session, num: int = 10): + # 创建子查询,获取每个不同的name的最大id + subquery = ( + db.query(self.model.name, func.max(self.model.id).label('max_id')) + .group_by(self.model.name) + .subquery() + ) + + # 主查询,与子查询连接,获取每个name的最后一条数据 + results = ( + db.query(self.model) + .join(subquery, and_(self.model.id == subquery.c.max_id)) + .all() + ) + return results + + +form = CRUDForm(Form) diff --git a/app/crud/result_crud.py b/app/crud/result_crud.py new file mode 100644 index 0000000..253daad --- /dev/null +++ b/app/crud/result_crud.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from sqlalchemy.orm import Session + +from app.models.result import Result +from app.schemas import ResultCreate, ResultUpdate +from app.crud.base import CRUDBase + + +class CRUDResult(CRUDBase[Result, ResultUpdate, ResultCreate]): + def get_by_uuid(self, db: Session, uuid: str) -> Result: + return db.query(self.model).filter(self.model.uuid == uuid).all() + + +result = CRUDResult(Result) diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..c53f601 --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- diff --git a/app/db/base.py b/app/db/base.py new file mode 100644 index 0000000..4ee5b75 --- /dev/null +++ b/app/db/base.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from contextlib import contextmanager +from typing import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import declarative_base + +from app.core.config import settings + +Base = declarative_base() +SQLALCHEMY_DATABASE_URL = settings.mysql_dsn + +engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, echo=False, pool_size=10, max_overflow=20) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +# @contextmanager +# def get_db() -> Generator: +# db = SessionLocal() +# try: +# yield db +# finally: +# db.close() + + +def get_db() -> Generator: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +if __name__ == '__main__': + with get_db() as db: + print(db.info) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..c53f601 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- diff --git a/app/models/form.py b/app/models/form.py new file mode 100644 index 0000000..9189cb6 --- /dev/null +++ b/app/models/form.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +from sqlalchemy import Column, Integer, String, Float, TIMESTAMP, text, func +from app.db.base import Base + + +class Form(Base): + __tablename__ = 'form' + + id = Column(Integer, primary_key=True, nullable=False) + base_prompt = Column(String(length=1024)) + prompt = Column(String(length=2048)) + p_choice = Column(String(length=255)) + role = Column(String(length=255)) + name = Column(String(length=1024)) + uuid = Column(String(length=1024)) + desc = Column(String(length=2048)) + price = Column(Float) + favorable = Column(String(length=1024)) + remark = Column(String(length=1024)) + otherPrompt = Column(String(length=1024)) + lang = Column(String(length=1024)) + type = Column(String(length=1024)) + created_at = Column(TIMESTAMP, server_default=func.now()) + updated_at = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')) diff --git a/app/models/result.py b/app/models/result.py new file mode 100644 index 0000000..8986aa5 --- /dev/null +++ b/app/models/result.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from sqlalchemy import Column, Integer, String, TIMESTAMP, func, text + +from app.db.base import Base + + +class Result(Base): + __tablename__ = 'result' + + id = Column(Integer, primary_key=True, nullable=False) + prompt = Column(String()) + name = Column(String()) + uuid = Column(String()) + value = Column(String()) + lang = Column(String()) + created_at = Column(TIMESTAMP, server_default=func.now()) + updated_at = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..c7e3938 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from .form import Form, FormCreate, FormUpdate +from .result import Result, ResultCreate, ResultUpdate +from .bench import Bench diff --git a/app/schemas/bench.py b/app/schemas/bench.py new file mode 100644 index 0000000..18e181c --- /dev/null +++ b/app/schemas/bench.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from pydantic import BaseModel + +from app.schemas import Form, Result + + +class Bench(BaseModel): + form: Form + results: list[Result] diff --git a/app/schemas/form.py b/app/schemas/form.py new file mode 100644 index 0000000..7d94f7a --- /dev/null +++ b/app/schemas/form.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +from datetime import datetime + +from pydantic import BaseModel +from pydantic.v1 import validator + + +class FormBase(BaseModel): + base_prompt: str | None = None + prompt: str | None = None + p_choice: str | None = None + role: str | None = None + name: str | None = None + uuid: str | None = None + desc: str | None = None + price: float | None = None + favorable: str | None = None + remark: str | None = None + otherPrompt: str | None = None + lang: str | None = None + type: str | None = None + + +class Form(FormBase): + id: int + created_at: datetime = datetime.now() + updated_at: datetime = datetime.now() + # custom input conversion for that field + _normalize_datetimes = validator( + "created_at", "updated_at", + allow_reuse=True)(lambda v: v.timestamp()) + + class Config: + from_attributes = True + + +class FormCreate(FormBase): + ... + + +class FormUpdate(FormBase): + ... diff --git a/app/schemas/result.py b/app/schemas/result.py new file mode 100644 index 0000000..47abdad --- /dev/null +++ b/app/schemas/result.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +from datetime import datetime + +from pydantic import BaseModel +from pydantic.v1 import validator + + +class ResultBase(BaseModel): + prompt: str | None = None + name: str | None = None + uuid: str | None = None + value: str | None = None + lang: str | None = None + + +class Result(ResultBase): + id: int + created_at: datetime = datetime.now() + updated_at: datetime = datetime.now() + _normalize_datetimes = validator( + "created_at", "updated_at", + allow_reuse=True)(lambda v: v.timestamp()) + + class Config: + from_attributes = True + + +class ResultCreate(ResultBase): + ... + + +class ResultUpdate(ResultBase): + ... diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e33a1f8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3' + +services: + + model-api: + image: model-api:1.0.0 + container_name: model-api + restart: always + environment: + - TZ=Asia/Shanghai + ports: + - "8000:80" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ae68293 --- /dev/null +++ b/main.py @@ -0,0 +1,61 @@ +from fastapi import FastAPI, status +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError, ResponseValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from loguru import logger +from starlette import requests +from starlette.middleware.errors import ServerErrorMiddleware +from starlette.requests import Request +from starlette.types import ASGIApp + +from app.api import api_router + +app = FastAPI() + + +# fix cors error when 500 occurs according to https://github.com/tiangolo/fastapi/issues/4071#issuecomment-950833326 +def global_execution_handler(request: requests.Request, exc: Exception) -> ASGIApp: + logger.exception(exc) + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=f"Unknown Error: {exc.args[0]}", + ) + + +app.add_middleware( + ServerErrorMiddleware, + handler=global_execution_handler, +) + +origins = [ + '*' +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router, prefix="") + + +@app.exception_handler(RequestValidationError) +def validation_exception_handler(request: Request, exc: RequestValidationError): + logger.exception(exc) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), + ) + + +@app.exception_handler(ResponseValidationError) +def validation_exception_handler(request: Request, exc: ResponseValidationError): + logger.exception(exc) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), + ) diff --git a/record.md b/record.md new file mode 100644 index 0000000..858b281 --- /dev/null +++ b/record.md @@ -0,0 +1,5 @@ + +授予用户mydata可以在任何主机上连接到mydata数据库,并拥有对该数据库中所有表的增删改查等操作的权限。 +```mysql +GRANT ALL PRIVILEGES ON mydata.* TO 'mydata'@'%'; +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4feeba3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +pydantic~=2.4.2 +pydantic-settings~=2.0.3 +sqlalchemy~=2.0.4 +alembic~=1.12.0 +cryptography~=41.0.4 +pandas~=2.1.1 +xlrd~=2.0.1 +loguru~=0.7.0 +pymysql~=1.1.0 +fastapi~=0.104.1 +uvicorn~=0.23.2 +starlette~=0.27.0 \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..c53f601 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- diff --git a/utils/str_translator.py b/utils/str_translator.py new file mode 100644 index 0000000..4316935 --- /dev/null +++ b/utils/str_translator.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import re + + +# noinspection PyPep8Naming +def snake_case_to_PascalCase(s: str): + """ + Convert snake_case string to PascalCase. + """ + return ''.join(word.capitalize() for word in s.split('_')) + + +# noinspection PyPep8Naming +def PascalCase_to_snake_case(s: str): + """ + Convert PascalCase string to snake_case. + """ + return re.sub(r'(?