diff --git a/.ruff.toml b/.ruff.toml deleted file mode 100644 index 9f4d8c36..00000000 --- a/.ruff.toml +++ /dev/null @@ -1,175 +0,0 @@ -line-length = 120 -preview = true -fix = true -unsafe-fixes = true -show-fixes = true -required-version = ">=0.13.0" - -[lint] -select = [ - "FAST", - "ANN001", - "ANN201", - "ANN202", - "ANN204", - "ANN205", - "ANN206", - "ASYNC110", - "ASYNC116", - "ASYNC210", - "ASYNC212", - "ASYNC230", - "ASYNC240", - "ASYNC250", - "ASYNC251", - "S310", - "FBT001", - "FBT002", - "B002", - "B005", - "B006", - "B007", - "B008", - "B009", - "B010", - "B013", - "B014", - "B019", - "B020", - "B021", - "B024", - "B025", - "B026", - "B027", - "B039", - "COM", - "C402", - "C403", - "C404", - "C408", - "C410", - "C411", - "C414", - "C416", - "C417", - "C418", - "C419", - "C420", - "DTZ", - "EXE", - "ISC001", - "ISC002", - "ISC003", - "PIE", - "PYI009", - "PYI010", - "PYI011", - "PYI012", - "PYI013", - "PYI016", - "PYI017", - "PYI019", - "PYI020", - "PYI021", - "PYI024", - "PYI026", - "PYI030", - "PYI033", - "PYI034", - "PYI036", - "PYI041", - "PYI042", - "PYI055", - "PYI061", - "PYI062", - "PYI063", - "Q001", - "Q002", - "RSE102", - "RET501", - "RET505", - "RET506", - "RET507", - "RET508", - "SIM101", - "SIM102", - "SIM103", - "SIM107", - "SIM108", - "SIM109", - "SIM110", - "SIM114", - "SIM115", - "SIM201", - "SIM202", - "SIM210", - "SIM211", - "SIM212", - "SIM300", - "SIM401", - "SIM910", - "TID252", - "TC", - "FLY", - "I", - "C901", - "N", - "PERF", - "E", - "W", - "D404", - "D417", - "D419", - "F", - "PGH", - "PLC1901", - "UP", - "FURB", - "RUF", - "TRY", -] -ignore = [ - "COM812", - "PGH003", - "RUF001", - "RUF002", - "RUF003", - "RUF006", - "RUF012", - "RUF067", - "TRY400", - "TRY003", - "TRY301" -] - -[lint.per-file-ignores] -"**/model/*.py" = ["TC003"] -"backend/common/socketio/server.py" = ["ANN001"] -"backend/common/exception/exception_handler.py" = ["ANN202","RUF029"] - -[lint.flake8-pytest-style] -parametrize-names-type = "list" -parametrize-values-row-type = "list" -parametrize-values-type = "list" - -[lint.flake8-quotes] -inline-quotes = "single" - -[lint.flake8-type-checking] -runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"] - -[lint.flake8-unused-arguments] -ignore-variadic-names = true - -[lint.isort] -case-sensitive = true -lines-between-types = 1 -order-by-type = true - -[lint.pylint] -allow-dunder-method-names = ["__tablename__", "__table_args__"] - -[format] -docstring-code-format = true -preview = true -quote-style = "single" diff --git a/backend/alembic.ini b/backend/alembic.ini index 912b03d9..28e6085f 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -1,84 +1,3 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts. -# Use forward slashes (/) also on windows to provide an os agnostic path -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 -file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d_%%(minute).2d_%%(second).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>=3.9 or backports.zoneinfo library. -# Any required deps can installed by adding `alembic[tz]` to the pip requirements -# string value is passed to ZoneInfo() -# 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 = newline -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 = driver://user:pass@localhost/dbname - - -[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 - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - # Logging configuration [loggers] keys = root,sqlalchemy,alembic diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 1e4b865c..58a4ffe6 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import async_engine_from_config from backend.common.model import MappedBase from backend.core import path_conf +from backend.core.path_conf import BASE_PATH from backend.database.db import SQLALCHEMY_DATABASE_URL if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR): @@ -17,19 +18,22 @@ if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR): # this is the Alembic Config object, which provides # access to the values within the .ini file in use. -alembic_config = context.config +config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. -if alembic_config.config_file_name is not None: - fileConfig(alembic_config.config_file_name) +if config.config_file_name is not None: + fileConfig(BASE_PATH / config.config_file_name) -# model's MetaData object +# add your model's MetaData object here # for 'autogenerate' support target_metadata = MappedBase.metadata # other values from the config, defined by the needs of env.py, -alembic_config.set_main_option( +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. +config.set_main_option( 'sqlalchemy.url', SQLALCHEMY_DATABASE_URL.render_as_string(hide_password=False).replace('%', '%%'), ) @@ -47,7 +51,7 @@ def run_migrations_offline() -> None: script output. """ - url = alembic_config.get_main_option('sqlalchemy.url') + url = config.get_main_option('sqlalchemy.url') context.configure( url=url, target_metadata=target_metadata, @@ -63,9 +67,9 @@ def run_migrations_offline() -> None: def do_run_migrations(connection: Connection) -> None: - # 当迁移无变化时,不生成迁移记录 def process_revision_directives(context, revision, directives) -> None: # noqa: ANN001 - if alembic_config.cmd_opts.autogenerate: + """当迁移无变化时,不生成迁移记录""" + if config.cmd_opts.autogenerate: script = directives[0] if script.upgrade_ops.is_empty(): directives[:] = [] @@ -91,7 +95,7 @@ async def run_async_migrations() -> None: """ connectable = async_engine_from_config( - alembic_config.get_section(alembic_config.config_ini_section, {}), + config.get_section(config.config_ini_section, {}), prefix='sqlalchemy.', poolclass=pool.NullPool, ) diff --git a/backend/cli.py b/backend/cli.py index 81ff7338..31b52d2b 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -500,7 +500,7 @@ async def generate(*, preview: bool = False) -> None: def run_alembic(*args: str) -> None: """执行 alembic 命令""" try: - subprocess.run(['alembic', *args], cwd=BASE_PATH, check=True) + subprocess.run(['alembic', *args], cwd=BASE_PATH.parent, check=True) except subprocess.CalledProcessError as e: raise cappa.Exit('Alembic 命令执行失败', code=e.returncode) diff --git a/pyproject.toml b/pyproject.toml index f374a2bf..760612e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,81 +1,280 @@ [project] name = "fastapi_best_architecture" description = "FastAPI 企业级后端架构解决方案" -authors = [ - { name = "Wu Clan", email = "jianhengwu0407@gmail.com" }, -] readme = "README.md" -license = { text = "MIT" } requires-python = ">=3.10" -dynamic = ['version'] -dependencies = [ - "alembic>=1.18.3", - "asgiref>=3.11.0", - "asyncmy>=0.2.11", - "asyncpg>=0.31.0", - "bcrypt>=5.0.0", - # If there is a serious problem, we will try `moka-py` - "cachebox>=5.2.2", - "cappa>=0.31.0", - "celery>=5.6.2", - # When celery version < 6.0.0 - # https://github.com/celery/celery/issues/7874 - "celery-aio-pool>=0.1.0rc8", - "cryptography>=46.0.4", - "dulwich>=1.0.0", - "fast-captcha>=0.3.2", - "fastapi-limiter>=0.1.6", - "fastapi-pagination>=0.15.9", - "fastapi[standard-no-fastapi-cloud-cli]>=0.128.0", - "flower>=2.0.1", - "gevent>=25.9.1", - "granian>=2.7.0", - "ip2loc>=1.0.0", - "itsdangerous>=2.2.0", - "jinja2>=3.1.6", - "loguru>=0.7.3", - "msgspec>=0.20.0", - "opentelemetry-exporter-otlp-proto-grpc>=1.39.1", - "opentelemetry-instrumentation-asyncio>=0.60b1", - "opentelemetry-instrumentation-celery>=0.60b1", - "opentelemetry-instrumentation-fastapi>=0.60b1", - "opentelemetry-instrumentation-httpx>=0.60b1", - "opentelemetry-instrumentation-logging>=0.60b1", - "opentelemetry-instrumentation-redis>=0.60b1", - "opentelemetry-instrumentation-sqlalchemy>=0.60b1", - "opentelemetry-sdk>=1.39.1", - "prometheus-client>=0.24.1", - "psutil>=7.2.2", - # https://github.com/fastapi-practices/fastapi_best_architecture/issues/887 - "psycopg[binary]==3.2.10", - "pwdlib>=0.3.0", - "pydantic>=2.12.5", - "pydantic-settings>=2.12.0", - "pymysql>=1.1.2", - "python-jose>=3.5.0", - "python-socketio>=5.16.0", - "redis[hiredis]>=7.1.0", - "rtoml>=0.13.0", - "sqlalchemy-crud-plus>=1.13.1", - "sqlalchemy[asyncio]>=2.0.46", - "sqlparse>=0.5.5", - "starlette-context>=0.4.0", - "user-agents>=2.2.0", +license = { text = "MIT" } +authors = [ + { name = "Wu Clan", email = "jianhengwu0407@gmail.com" }, ] +dependencies = [ + "alembic>=1.18.3", + "asgiref>=3.11.0", + "asyncmy>=0.2.11", + "asyncpg>=0.31.0", + "bcrypt>=5.0.0", + # If there is a serious problem, we will try `moka-py` + "cachebox>=5.2.2", + "cappa>=0.31.0", + "celery>=5.6.2", + # When celery version < 6.0.0 + # https://github.com/celery/celery/issues/7874 + "celery-aio-pool>=0.1.0rc8", + "cryptography>=46.0.4", + "dulwich>=1.0.0", + "fast-captcha>=0.3.2", + "fastapi-limiter>=0.1.6", + "fastapi-pagination>=0.15.9", + "fastapi[standard-no-fastapi-cloud-cli]>=0.128.0", + "flower>=2.0.1", + "gevent>=25.9.1", + "granian>=2.7.0", + "ip2loc>=1.0.0", + "itsdangerous>=2.2.0", + "jinja2>=3.1.6", + "loguru>=0.7.3", + "msgspec>=0.20.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.39.1", + "opentelemetry-instrumentation-asyncio>=0.60b1", + "opentelemetry-instrumentation-celery>=0.60b1", + "opentelemetry-instrumentation-fastapi>=0.60b1", + "opentelemetry-instrumentation-httpx>=0.60b1", + "opentelemetry-instrumentation-logging>=0.60b1", + "opentelemetry-instrumentation-redis>=0.60b1", + "opentelemetry-instrumentation-sqlalchemy>=0.60b1", + "opentelemetry-sdk>=1.39.1", + "prometheus-client>=0.24.1", + "psutil>=7.2.2", + # https://github.com/fastapi-practices/fastapi_best_architecture/issues/887 + "psycopg[binary]==3.2.10", + "pwdlib>=0.3.0", + "pydantic>=2.12.5", + "pydantic-settings>=2.12.0", + "pymysql>=1.1.2", + "python-jose>=3.5.0", + "python-socketio>=5.16.0", + "redis[hiredis]>=7.1.0", + "rtoml>=0.13.0", + "sqlalchemy-crud-plus>=1.13.1", + "sqlalchemy[asyncio]>=2.0.46", + "sqlparse>=0.5.5", + "starlette-context>=0.4.0", + "user-agents>=2.2.0", +] +dynamic = ["version"] + +[project.scripts] +fba = "backend.cli:main" [dependency-groups] dev = [ - "pytest>=9.0.2", - "pytest-sugar>=1.1.1", + "pytest>=9.0.2", + "pytest-sugar>=1.1.1", ] lint = [ - "prek>=0.3.1", + "prek>=0.3.1", ] server = [ - "aio-pika>=9.5.8", - "wait-for-it>=2.3.0", + "aio-pika>=9.5.8", + "wait-for-it>=2.3.0", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.alembic] +script_location = "%(here)s/backend/alembic" +file_template = "%%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d_%%(minute).2d_%%(second).2d-%%(rev)s_%%(slug)s" +prepend_sys_path = ["."] +version_locations = ["%(here)s/backend/alembic/versions"] + +[tool.hatch.build.targets.wheel] +packages = ["backend"] + +[tool.hatch.version] +path = "backend/__init__.py" + +[tool.ruff] +line-length = 120 +preview = true +fix = true +unsafe-fixes = true +show-fixes = true +required-version = ">=0.13.0" + +[tool.ruff.lint] +select = [ + "FAST", + "ANN001", + "ANN201", + "ANN202", + "ANN204", + "ANN205", + "ANN206", + "ASYNC110", + "ASYNC116", + "ASYNC210", + "ASYNC212", + "ASYNC230", + "ASYNC240", + "ASYNC250", + "ASYNC251", + "S310", + "FBT001", + "FBT002", + "B002", + "B005", + "B006", + "B007", + "B008", + "B009", + "B010", + "B013", + "B014", + "B019", + "B020", + "B021", + "B024", + "B025", + "B026", + "B027", + "B039", + "COM", + "C402", + "C403", + "C404", + "C408", + "C410", + "C411", + "C414", + "C416", + "C417", + "C418", + "C419", + "C420", + "DTZ", + "EXE", + "ISC001", + "ISC002", + "ISC003", + "PIE", + "PYI009", + "PYI010", + "PYI011", + "PYI012", + "PYI013", + "PYI016", + "PYI017", + "PYI019", + "PYI020", + "PYI021", + "PYI024", + "PYI026", + "PYI030", + "PYI033", + "PYI034", + "PYI036", + "PYI041", + "PYI042", + "PYI055", + "PYI061", + "PYI062", + "PYI063", + "Q001", + "Q002", + "RSE102", + "RET501", + "RET505", + "RET506", + "RET507", + "RET508", + "SIM101", + "SIM102", + "SIM103", + "SIM107", + "SIM108", + "SIM109", + "SIM110", + "SIM114", + "SIM115", + "SIM201", + "SIM202", + "SIM210", + "SIM211", + "SIM212", + "SIM300", + "SIM401", + "SIM910", + "TID252", + "TC", + "FLY", + "I", + "C901", + "N", + "PERF", + "E", + "W", + "D404", + "D417", + "D419", + "F", + "PGH", + "PLC1901", + "UP", + "FURB", + "RUF", + "TRY", +] +ignore = [ + "COM812", + "PGH003", + "RUF001", + "RUF002", + "RUF003", + "RUF006", + "RUF012", + "RUF067", + "TRY400", + "TRY003", + "TRY301" +] + +[tool.ruff.lint.per-file-ignores] +"**/model/*.py" = ["TC003"] +"backend/common/socketio/server.py" = ["ANN001"] +"backend/common/exception/exception_handler.py" = ["ANN202", "RUF029"] + +[tool.ruff.lint.flake8-pytest-style] +parametrize-names-type = "list" +parametrize-values-row-type = "list" +parametrize-values-type = "list" + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.lint.flake8-type-checking] +runtime-evaluated-base-classes = [ + "pydantic.BaseModel", + "sqlalchemy.orm.DeclarativeBase" +] + +[tool.ruff.lint.flake8-unused-arguments] +ignore-variadic-names = true + +[tool.ruff.lint.isort] +case-sensitive = true +lines-between-types = 1 +order-by-type = true + +[tool.ruff.lint.pylint] +allow-dunder-method-names = ["__tablename__", "__table_args__"] + +[tool.ruff.format] +docstring-code-format = true +preview = true +quote-style = "single" + [tool.uv] python-downloads = "manual" default-groups = ["dev", "lint"] @@ -83,16 +282,3 @@ default-groups = ["dev", "lint"] [[tool.uv.index]] name = "aliyun" url = "https://mirrors.aliyun.com/pypi/simple" - -[tool.hatch.build.targets.wheel] -packages = ["backend"] - -[tool.hatch.version] -path = "backend/__init__.py" - -[project.scripts] -fba = "backend.cli:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build"