FastAPI-fastkit 1.2.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_fastkit/__init__.py +1 -1
- fastapi_fastkit/backend/inspection/__init__.py +26 -0
- fastapi_fastkit/backend/inspection/checks.py +349 -0
- fastapi_fastkit/backend/inspection/consistency.py +125 -0
- fastapi_fastkit/backend/inspection/context.py +67 -0
- fastapi_fastkit/backend/inspection/core.py +347 -0
- fastapi_fastkit/backend/inspection/docker.py +185 -0
- fastapi_fastkit/backend/inspection/freshness.py +145 -0
- fastapi_fastkit/backend/inspection/fsutils.py +139 -0
- fastapi_fastkit/backend/inspection/lint.py +89 -0
- fastapi_fastkit/backend/inspection/report.py +58 -0
- fastapi_fastkit/backend/inspection/smoke.py +327 -0
- fastapi_fastkit/backend/inspection/strategies.py +336 -0
- fastapi_fastkit/backend/inspector.py +18 -1279
- fastapi_fastkit/backend/interactive/__init__.py +8 -0
- fastapi_fastkit/backend/interactive/config_builder.py +66 -65
- fastapi_fastkit/backend/interactive/prompts.py +205 -5
- fastapi_fastkit/backend/interactive/selectors.py +28 -2
- fastapi_fastkit/backend/main.py +956 -95
- fastapi_fastkit/backend/package_managers/base.py +172 -0
- fastapi_fastkit/backend/package_managers/factory.py +6 -5
- fastapi_fastkit/backend/package_managers/pdm_manager.py +32 -85
- fastapi_fastkit/backend/package_managers/pip_manager.py +31 -85
- fastapi_fastkit/backend/package_managers/poetry_manager.py +73 -113
- fastapi_fastkit/backend/package_managers/uv_manager.py +46 -115
- fastapi_fastkit/backend/project_builder/__init__.py +10 -0
- fastapi_fastkit/backend/project_builder/config_generator.py +813 -450
- fastapi_fastkit/backend/project_builder/config_schema.py +384 -0
- fastapi_fastkit/backend/project_builder/dependency_collector.py +105 -66
- fastapi_fastkit/backend/project_builder/preset_layout.py +429 -0
- fastapi_fastkit/backend/scaffolder.py +578 -0
- fastapi_fastkit/backend/transducer.py +54 -22
- fastapi_fastkit/cli.py +452 -284
- fastapi_fastkit/core/settings.py +248 -43
- fastapi_fastkit/fastapi_project_template/README.md +88 -23
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +25 -23
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +39 -36
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +9 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/.env-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/.gitignore-tpl +33 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/Dockerfile-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl +210 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/alembic.ini-tpl +119 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/pyproject.toml-tpl +86 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/requirements.txt-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/README-tpl +1 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/env.py-tpl +51 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/script.py.mako-tpl +26 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/versions/0001_initial_auth_schema.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/deps.py-tpl +91 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/router.py-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/config.py-tpl +64 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/security.py-tpl +100 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/db/session.py-tpl +36 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/models.py-tpl +25 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/repository.py-tpl +41 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/router.py-tpl +82 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/schemas.py-tpl +14 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/service.py-tpl +65 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/models.py-tpl +35 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/repository.py-tpl +33 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/router.py-tpl +71 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/schemas.py-tpl +29 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/service.py-tpl +55 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/main.py-tpl +69 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/template-config.yml-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/conftest.py-tpl +73 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_auth.py-tpl +201 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_health.py-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_users.py-tpl +77 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +25 -23
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +39 -36
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +3 -3
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +22 -20
- fastapi_fastkit/fastapi_project_template/fastapi-default/requirements.txt-tpl +37 -34
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/api/routes/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/main.py-tpl +9 -1
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +2 -3
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +0 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +25 -23
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +41 -36
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +9 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +2 -3
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +0 -2
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.env-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.gitignore-tpl +31 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl +128 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/pyproject.toml-tpl +69 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +37 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/__init__.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/models.py-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +47 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +58 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/schemas.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +51 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +28 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_items.py-tpl +77 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +2 -2
- fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +18 -16
- fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +36 -10
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +3 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.env-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.gitignore-tpl +31 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/Dockerfile-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl +193 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/pyproject.toml-tpl +76 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/requirements.txt-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/chat.sh-tpl +10 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/chat.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/deps.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/router.py-tpl +13 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/config.py-tpl +67 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/agent.py-tpl +136 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/client.py-tpl +30 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/tools.py-tpl +131 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/main.py-tpl +53 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/base.py-tpl +27 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/in_memory.py-tpl +32 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/chat.py-tpl +46 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/template-config.yml-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/conftest.py-tpl +158 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_chat.py-tpl +160 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_health.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_memory.py-tpl +38 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_tools.py-tpl +37 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +29 -26
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +68 -39
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +3 -4
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/router.py-tpl +3 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +6 -7
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +26 -24
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +44 -41
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +1 -3
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +2 -4
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +4 -6
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +1 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +0 -2
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/.env-tpl +1 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/.gitignore-tpl +30 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +7 -5
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/requirements.txt-tpl +26 -6
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/src/main.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.env-tpl +7 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.gitignore-tpl +35 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/Dockerfile-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl +166 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/alembic.ini-tpl +44 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/pyproject.toml-tpl +82 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/requirements.txt-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/migrate.sh-tpl +7 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/README-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/env.py-tpl +72 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/script.py.mako-tpl +27 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/versions/0001_create_items_table.py-tpl +38 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/router.py-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/config.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/base.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/pagination.py-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/base.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/session.py-tpl +50 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/crud.py-tpl +26 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/models.py-tpl +70 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/router.py-tpl +69 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/main.py-tpl +43 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/template-config.yml-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/conftest.py-tpl +60 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_crud_base.py-tpl +43 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_health.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_items.py-tpl +110 -0
- fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +2 -2
- fastapi_fastkit/fragments/README.md +90 -0
- fastapi_fastkit/fragments/auth/fastapi_users.py.j2 +8 -0
- fastapi_fastkit/fragments/auth/jwt.py.j2 +44 -0
- fastapi_fastkit/fragments/auth/oauth2.py.j2 +105 -0
- fastapi_fastkit/fragments/auth/session.py.j2 +67 -0
- fastapi_fastkit/fragments/db/mongodb.py.j2 +19 -0
- fastapi_fastkit/fragments/db/redis.py.j2 +26 -0
- fastapi_fastkit/fragments/db/sqlalchemy.py.j2 +45 -0
- fastapi_fastkit/fragments/docker/docker_compose.j2 +84 -0
- fastapi_fastkit/fragments/docker/dockerfile.j2 +19 -0
- fastapi_fastkit/fragments/features/cache.py.j2 +26 -0
- fastapi_fastkit/fragments/features/pagination.py.j2 +31 -0
- fastapi_fastkit/fragments/features/tasks_celery.py.j2 +50 -0
- fastapi_fastkit/fragments/features/tasks_dramatiq.py.j2 +59 -0
- fastapi_fastkit/fragments/features/websocket.py.j2 +66 -0
- fastapi_fastkit/fragments/header.j2 +4 -0
- fastapi_fastkit/fragments/logging/structured.py.j2 +88 -0
- fastapi_fastkit/fragments/main/imports/async_tasks.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/caching.j2 +5 -0
- fastapi_fastkit/fragments/main/imports/fastapi_users.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/jwt.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/loguru.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/mongodb.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/oauth2.j2 +4 -0
- fastapi_fastkit/fragments/main/imports/opentelemetry.j2 +6 -0
- fastapi_fastkit/fragments/main/imports/pagination.j2 +3 -0
- fastapi_fastkit/fragments/main/imports/prometheus.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/rate_limiting.j2 +3 -0
- fastapi_fastkit/fragments/main/imports/session_auth.j2 +4 -0
- fastapi_fastkit/fragments/main/imports/sqlalchemy.j2 +2 -0
- fastapi_fastkit/fragments/main/imports/structured_logging.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/websocket.j2 +1 -0
- fastapi_fastkit/fragments/main/lifespan/caching.j2 +7 -0
- fastapi_fastkit/fragments/main/lifespan/mongodb.j2 +2 -0
- fastapi_fastkit/fragments/main/lifespan/prometheus.j2 +2 -0
- fastapi_fastkit/fragments/main/lifespan/sqlalchemy.j2 +2 -0
- fastapi_fastkit/fragments/main/setup/async_tasks.j2 +1 -0
- fastapi_fastkit/fragments/main/setup/caching.j2 +1 -0
- fastapi_fastkit/fragments/main/setup/cors.j2 +8 -0
- fastapi_fastkit/fragments/main/setup/oauth2.j2 +3 -0
- fastapi_fastkit/fragments/main/setup/opentelemetry.j2 +19 -0
- fastapi_fastkit/fragments/main/setup/pagination.j2 +2 -0
- fastapi_fastkit/fragments/main/setup/rate_limiting.j2 +3 -0
- fastapi_fastkit/fragments/main/setup/session_auth.j2 +6 -0
- fastapi_fastkit/fragments/main/setup/structured_logging.j2 +2 -0
- fastapi_fastkit/fragments/main/setup/websocket.j2 +1 -0
- fastapi_fastkit/fragments/main.py.j2 +57 -0
- fastapi_fastkit/fragments/migrations/alembic_ini.j2 +42 -0
- fastapi_fastkit/fragments/migrations/env.py.j2 +76 -0
- fastapi_fastkit/fragments/migrations/initial_revision.py.j2 +27 -0
- fastapi_fastkit/fragments/migrations/migrate_sh.j2 +34 -0
- fastapi_fastkit/fragments/migrations/script.py.mako.j2 +26 -0
- fastapi_fastkit/fragments/tasks/celery_worker.py.j2 +49 -0
- fastapi_fastkit/fragments/tasks/dramatiq_worker.py.j2 +41 -0
- fastapi_fastkit/fragments/test/factories.py.j2 +68 -0
- fastapi_fastkit/fragments/test/pytest_ini.j2 +16 -0
- fastapi_fastkit/fragments/test/test_factories.py.j2 +27 -0
- fastapi_fastkit/fragments/test/test_main.py.j2 +46 -0
- fastapi_fastkit/fragments/tooling/devcontainer.j2 +26 -0
- fastapi_fastkit/fragments/tooling/github_actions.j2 +37 -0
- fastapi_fastkit/fragments/tooling/makefile.j2 +40 -0
- fastapi_fastkit/fragments/tooling/pre_commit.j2 +36 -0
- fastapi_fastkit/fragments/tooling/ruff_toml.j2 +16 -0
- fastapi_fastkit/utils/config_file.py +263 -0
- fastapi_fastkit/utils/main.py +111 -6
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/METADATA +33 -6
- fastapi_fastkit-1.4.0.dist-info/RECORD +484 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/WHEEL +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +0 -25
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +0 -27
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +0 -51
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +0 -25
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/setup.py-tpl +0 -17
- fastapi_fastkit-1.2.1.dist-info/RECORD +0 -256
- /fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/__init__.py-tpl +0 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/entry_points.txt +0 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Filesystem helpers shared by the inspection pipeline: shell script line
|
|
3
|
+
# ending normalisation and best-effort removal of temporary directories that
|
|
4
|
+
# Docker or a test run may have left behind with awkward permissions.
|
|
5
|
+
#
|
|
6
|
+
# @author bnbong
|
|
7
|
+
# --------------------------------------------------------------------------
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import stat
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from fastapi_fastkit.utils.logging import debug_log
|
|
15
|
+
|
|
16
|
+
CLEANUP_MAX_RETRIES = 5
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fix_script_line_endings(script_path: str) -> None:
|
|
20
|
+
"""Convert Windows line endings in a script to Unix ones."""
|
|
21
|
+
try:
|
|
22
|
+
with open(script_path, "rb") as f:
|
|
23
|
+
content = f.read()
|
|
24
|
+
|
|
25
|
+
content = content.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
26
|
+
|
|
27
|
+
with open(script_path, "wb") as f:
|
|
28
|
+
f.write(content)
|
|
29
|
+
|
|
30
|
+
debug_log(f"Fixed line endings in {script_path}", "debug")
|
|
31
|
+
except OSError as e:
|
|
32
|
+
debug_log(f"Failed to fix line endings in {script_path}: {e}", "warning")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fix_all_script_line_endings(directory: str) -> None:
|
|
36
|
+
"""Normalise line endings for every shell script below ``directory``."""
|
|
37
|
+
for root, _dirs, files in os.walk(directory):
|
|
38
|
+
for file_name in files:
|
|
39
|
+
if file_name.endswith((".sh", ".bash")):
|
|
40
|
+
fix_script_line_endings(os.path.join(root, file_name))
|
|
41
|
+
|
|
42
|
+
debug_log("Fixed line endings for all shell scripts", "info")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fix_permissions(path: str) -> None:
|
|
46
|
+
"""Grant full permissions to ``path``, ignoring failures."""
|
|
47
|
+
try:
|
|
48
|
+
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fix_directory_permissions(directory_path: str) -> None:
|
|
54
|
+
"""Make a directory tree writable so it can be removed."""
|
|
55
|
+
_fix_permissions(directory_path)
|
|
56
|
+
for root, dirs, files in os.walk(directory_path):
|
|
57
|
+
_fix_permissions(root)
|
|
58
|
+
for name in dirs + files:
|
|
59
|
+
_fix_permissions(os.path.join(root, name))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def remove_directory_contents(directory_path: str) -> None:
|
|
63
|
+
"""Remove a directory's contents entry by entry, ignoring failures."""
|
|
64
|
+
for root, dirs, files in os.walk(directory_path, topdown=False):
|
|
65
|
+
for file_name in files:
|
|
66
|
+
file_path = os.path.join(root, file_name)
|
|
67
|
+
_fix_permissions(file_path)
|
|
68
|
+
try:
|
|
69
|
+
os.remove(file_path)
|
|
70
|
+
except OSError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
for dir_name in dirs:
|
|
74
|
+
dir_path = os.path.join(root, dir_name)
|
|
75
|
+
_fix_permissions(dir_path)
|
|
76
|
+
try:
|
|
77
|
+
os.rmdir(dir_path)
|
|
78
|
+
except OSError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def force_cleanup_directory(directory_path: str) -> None:
|
|
83
|
+
"""Remove ``directory_path`` using progressively more forceful strategies."""
|
|
84
|
+
for attempt in range(CLEANUP_MAX_RETRIES):
|
|
85
|
+
try:
|
|
86
|
+
shutil.rmtree(directory_path)
|
|
87
|
+
debug_log(
|
|
88
|
+
f"Successfully cleaned up temp directory: {directory_path}", "info"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
except (OSError, PermissionError) as e:
|
|
92
|
+
if attempt == CLEANUP_MAX_RETRIES - 1:
|
|
93
|
+
break
|
|
94
|
+
debug_log(
|
|
95
|
+
f"Attempt {attempt + 1} failed to cleanup {directory_path}: {e}",
|
|
96
|
+
"warning",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
fix_directory_permissions(directory_path)
|
|
101
|
+
shutil.rmtree(directory_path)
|
|
102
|
+
debug_log(
|
|
103
|
+
"Successfully cleaned up temp directory after permission fix: "
|
|
104
|
+
f"{directory_path}",
|
|
105
|
+
"info",
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
remove_directory_contents(directory_path)
|
|
113
|
+
os.rmdir(directory_path)
|
|
114
|
+
debug_log(
|
|
115
|
+
"Successfully cleaned up temp directory by removing contents: "
|
|
116
|
+
f"{directory_path}",
|
|
117
|
+
"info",
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
time.sleep(2)
|
|
124
|
+
|
|
125
|
+
debug_log(
|
|
126
|
+
f"Failed to cleanup temp directory after {CLEANUP_MAX_RETRIES} attempts: "
|
|
127
|
+
f"{directory_path}",
|
|
128
|
+
"warning",
|
|
129
|
+
)
|
|
130
|
+
try:
|
|
131
|
+
if os.name == "nt": # pragma: no cover - Windows-only branch
|
|
132
|
+
subprocess.run(
|
|
133
|
+
["rmdir", "/s", "/q", directory_path], check=False, shell=True
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
subprocess.run(["rm", "-rf", directory_path], check=False)
|
|
137
|
+
debug_log(f"Force cleanup completed for: {directory_path}", "info")
|
|
138
|
+
except OSError:
|
|
139
|
+
debug_log(f"All cleanup attempts failed for: {directory_path}", "warning")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Static analysis of the generated project.
|
|
3
|
+
#
|
|
4
|
+
# ``compileall`` is mandatory - a template that cannot be byte-compiled is
|
|
5
|
+
# broken beyond argument. ``mypy`` is opt-in because it needs the template's
|
|
6
|
+
# own dependencies installed and is far slower.
|
|
7
|
+
#
|
|
8
|
+
# @author bnbong
|
|
9
|
+
# --------------------------------------------------------------------------
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
from fastapi_fastkit.utils.logging import debug_log
|
|
15
|
+
|
|
16
|
+
from .context import InspectionContext
|
|
17
|
+
|
|
18
|
+
COMPILE_TIMEOUT = 120
|
|
19
|
+
MYPY_TIMEOUT = 300
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _interpreter(ctx: InspectionContext) -> str:
|
|
23
|
+
"""Prefer the inspection venv interpreter, falling back to the host one."""
|
|
24
|
+
return ctx.python_executable() or sys.executable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_compileall(ctx: InspectionContext) -> bool:
|
|
28
|
+
"""Byte-compile every Python file of the generated project."""
|
|
29
|
+
command: List[str] = [
|
|
30
|
+
_interpreter(ctx),
|
|
31
|
+
"-m",
|
|
32
|
+
"compileall",
|
|
33
|
+
"-q",
|
|
34
|
+
"-x",
|
|
35
|
+
r"(\.venv|venv|node_modules)",
|
|
36
|
+
ctx.temp_dir,
|
|
37
|
+
]
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
command,
|
|
41
|
+
cwd=ctx.temp_dir,
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=COMPILE_TIMEOUT,
|
|
45
|
+
)
|
|
46
|
+
except subprocess.TimeoutExpired:
|
|
47
|
+
ctx.add_error("compileall timed out")
|
|
48
|
+
return False
|
|
49
|
+
except OSError as e:
|
|
50
|
+
ctx.add_error(f"Failed to run compileall: {e}")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
if result.returncode != 0:
|
|
54
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
55
|
+
ctx.add_error(f"Generated project failed to compile:\n{detail}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
debug_log("compileall check passed", "info")
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_mypy(ctx: InspectionContext) -> bool:
|
|
63
|
+
"""Run mypy inside the generated project (opt-in via ``--mypy``)."""
|
|
64
|
+
if not ctx.options.run_mypy:
|
|
65
|
+
debug_log("mypy check disabled", "info")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
[_interpreter(ctx), "-m", "mypy", "."],
|
|
71
|
+
cwd=ctx.temp_dir,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
timeout=MYPY_TIMEOUT,
|
|
75
|
+
)
|
|
76
|
+
except subprocess.TimeoutExpired:
|
|
77
|
+
ctx.add_error("mypy timed out")
|
|
78
|
+
return False
|
|
79
|
+
except OSError as e:
|
|
80
|
+
ctx.add_warning(f"Could not run mypy: {e}")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
if result.returncode != 0:
|
|
84
|
+
detail = (result.stdout or result.stderr or "").strip()
|
|
85
|
+
ctx.add_error(f"mypy reported errors in the generated project:\n{detail}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
debug_log("mypy check passed", "info")
|
|
89
|
+
return True
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Inspection reporting: turn an InspectionContext into the report dictionary
|
|
3
|
+
# consumed by the CLI, the local scripts and the CI workflows.
|
|
4
|
+
#
|
|
5
|
+
# @author bnbong
|
|
6
|
+
# --------------------------------------------------------------------------
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
from fastapi_fastkit.utils.logging import debug_log
|
|
10
|
+
from fastapi_fastkit.utils.main import print_error, print_success, print_warning
|
|
11
|
+
|
|
12
|
+
from .context import InspectionContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_report(ctx: InspectionContext) -> Dict[str, Any]:
|
|
16
|
+
"""Build the inspection report and log a summary of the findings."""
|
|
17
|
+
is_valid = len(ctx.errors) == 0
|
|
18
|
+
template_name = ctx.template_path.name
|
|
19
|
+
|
|
20
|
+
if is_valid:
|
|
21
|
+
debug_log(
|
|
22
|
+
f"Template inspection completed successfully for {template_name}", "info"
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
debug_log(
|
|
26
|
+
f"Template inspection failed for {template_name} "
|
|
27
|
+
f"with {len(ctx.errors)} errors",
|
|
28
|
+
"error",
|
|
29
|
+
)
|
|
30
|
+
for index, error in enumerate(ctx.errors, 1):
|
|
31
|
+
debug_log(f"Error {index}: {error}", "error")
|
|
32
|
+
|
|
33
|
+
if ctx.warnings:
|
|
34
|
+
debug_log(
|
|
35
|
+
f"Template {template_name} has {len(ctx.warnings)} warnings: "
|
|
36
|
+
f"{ctx.warnings}",
|
|
37
|
+
"warning",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
"template_path": str(ctx.template_path),
|
|
42
|
+
"errors": list(ctx.errors),
|
|
43
|
+
"warnings": list(ctx.warnings),
|
|
44
|
+
"is_valid": is_valid,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_report(ctx: InspectionContext, is_valid: bool) -> None:
|
|
49
|
+
"""Print the human-facing inspection outcome."""
|
|
50
|
+
if is_valid:
|
|
51
|
+
print_success(f"Template {ctx.template_path} is valid!")
|
|
52
|
+
else:
|
|
53
|
+
print_error(f"Template {ctx.template_path} validation failed")
|
|
54
|
+
for error in ctx.errors:
|
|
55
|
+
print_error(f" - {error}")
|
|
56
|
+
|
|
57
|
+
for warning in ctx.warnings:
|
|
58
|
+
print_warning(f" - {warning}")
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# HTTP smoke test for a generated project.
|
|
3
|
+
#
|
|
4
|
+
# Booting the application is the only honest way to prove a template works:
|
|
5
|
+
# a substring search for "FastAPI" in main.py cannot tell a working app from
|
|
6
|
+
# one that raises on import. The generated project is started with uvicorn in
|
|
7
|
+
# a subprocess on a free port; ``/docs`` must answer 200 and ``/health``, when
|
|
8
|
+
# the template exposes one, must answer 200 as well.
|
|
9
|
+
#
|
|
10
|
+
# @author bnbong
|
|
11
|
+
# --------------------------------------------------------------------------
|
|
12
|
+
import os
|
|
13
|
+
import signal
|
|
14
|
+
import socket
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
import tomllib
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from typing import IO, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from fastapi_fastkit.utils.logging import debug_log
|
|
24
|
+
|
|
25
|
+
from .context import InspectionContext
|
|
26
|
+
|
|
27
|
+
#: Probed in order when no explicit app_module is declared.
|
|
28
|
+
MAIN_MODULE_CANDIDATES = [
|
|
29
|
+
("src/app/main.py", "src.app.main"),
|
|
30
|
+
("src/main.py", "src.main"),
|
|
31
|
+
("app/main.py", "app.main"),
|
|
32
|
+
("main.py", "main"),
|
|
33
|
+
]
|
|
34
|
+
DEFAULT_APP_ATTRIBUTE = "app"
|
|
35
|
+
SHUTDOWN_TIMEOUT = 10
|
|
36
|
+
|
|
37
|
+
#: How many ports to try before giving up. ``find_free_port`` releases the
|
|
38
|
+
#: port before uvicorn binds it, so another process can win the race; on a
|
|
39
|
+
#: busy CI machine that is common enough to be worth retrying.
|
|
40
|
+
PORT_ATTEMPTS = 3
|
|
41
|
+
|
|
42
|
+
#: Bytes of captured server output attached to a failure report.
|
|
43
|
+
LOG_TAIL_BYTES = 4000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_free_port() -> int:
|
|
47
|
+
"""Reserve and release a port so uvicorn can bind it."""
|
|
48
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
49
|
+
sock.bind(("127.0.0.1", 0))
|
|
50
|
+
port: int = sock.getsockname()[1]
|
|
51
|
+
return port
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _app_module_from_pyproject(ctx: InspectionContext) -> Optional[str]:
|
|
55
|
+
"""Read ``[tool.fastapi-fastkit].app_module`` from the generated project."""
|
|
56
|
+
pyproject_path = ctx.temp_path("pyproject.toml")
|
|
57
|
+
if not os.path.exists(pyproject_path):
|
|
58
|
+
return None
|
|
59
|
+
try:
|
|
60
|
+
with open(pyproject_path, "rb") as f:
|
|
61
|
+
data = tomllib.load(f)
|
|
62
|
+
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
|
|
63
|
+
debug_log(f"Could not read generated pyproject.toml: {e}", "warning")
|
|
64
|
+
return None
|
|
65
|
+
value = data.get("tool", {}).get("fastapi-fastkit", {}).get("app_module")
|
|
66
|
+
return str(value) if value else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _app_module_from_template_config(ctx: InspectionContext) -> Optional[str]:
|
|
70
|
+
"""Read ``app_module`` from template-config.yml, if the template ships one."""
|
|
71
|
+
if not ctx.template_config:
|
|
72
|
+
return None
|
|
73
|
+
value = ctx.template_config.get("app_module")
|
|
74
|
+
return str(value) if value else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _app_module_from_layout(ctx: InspectionContext) -> Optional[str]:
|
|
78
|
+
"""Fall back to the conventional main module locations."""
|
|
79
|
+
for relative_path, dotted in MAIN_MODULE_CANDIDATES:
|
|
80
|
+
if os.path.exists(ctx.temp_path(*relative_path.split("/"))):
|
|
81
|
+
return f"{dotted}:{DEFAULT_APP_ATTRIBUTE}"
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_app_module(ctx: InspectionContext) -> Optional[str]:
|
|
86
|
+
"""Determine the ``module:attribute`` uvicorn target for the project.
|
|
87
|
+
|
|
88
|
+
Resolution order: generated ``pyproject.toml`` metadata, then
|
|
89
|
+
``template-config.yml``, then the conventional file layout.
|
|
90
|
+
"""
|
|
91
|
+
for resolver in (
|
|
92
|
+
_app_module_from_pyproject,
|
|
93
|
+
_app_module_from_template_config,
|
|
94
|
+
_app_module_from_layout,
|
|
95
|
+
):
|
|
96
|
+
value = resolver(ctx)
|
|
97
|
+
if value:
|
|
98
|
+
if ":" not in value:
|
|
99
|
+
value = f"{value}:{DEFAULT_APP_ATTRIBUTE}"
|
|
100
|
+
debug_log(f"Resolved app module: {value}", "info")
|
|
101
|
+
return value
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _probe(url: str, timeout: int = 5) -> Optional[int]:
|
|
106
|
+
"""GET ``url`` and return its status code, or ``None`` when unreachable."""
|
|
107
|
+
try:
|
|
108
|
+
with urllib.request.urlopen(url, timeout=timeout) as response:
|
|
109
|
+
status: int = response.status
|
|
110
|
+
return status
|
|
111
|
+
except urllib.error.HTTPError as e:
|
|
112
|
+
return int(e.code)
|
|
113
|
+
except (urllib.error.URLError, OSError, TimeoutError):
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _wait_for_server(
|
|
118
|
+
process: "subprocess.Popen[bytes]", base_url: str, timeout: int
|
|
119
|
+
) -> Tuple[bool, str]:
|
|
120
|
+
"""Poll ``/docs`` until the server answers, the process dies or time runs out."""
|
|
121
|
+
deadline = time.time() + timeout
|
|
122
|
+
while time.time() < deadline:
|
|
123
|
+
if process.poll() is not None:
|
|
124
|
+
return False, "server process exited before becoming reachable"
|
|
125
|
+
status = _probe(f"{base_url}/docs")
|
|
126
|
+
if status is not None:
|
|
127
|
+
if status == 200:
|
|
128
|
+
return True, ""
|
|
129
|
+
return False, f"/docs returned HTTP {status} (expected 200)"
|
|
130
|
+
time.sleep(0.5)
|
|
131
|
+
return False, f"server did not become reachable within {timeout}s"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _signal_group(process: "subprocess.Popen[bytes]", sig: int) -> bool:
|
|
135
|
+
"""Signal the server's whole process group, if it has one of its own.
|
|
136
|
+
|
|
137
|
+
uvicorn spawns reloader / worker children, and signalling only the
|
|
138
|
+
parent leaves them holding the port. The process is started with
|
|
139
|
+
``start_new_session=True`` so the group id equals its pid.
|
|
140
|
+
|
|
141
|
+
Only a group we created ourselves is ever signalled: the pid must be a
|
|
142
|
+
real positive integer and must be its own group leader. Anything else --
|
|
143
|
+
a mocked process object, a pid that has been reaped and rejoined another
|
|
144
|
+
group, the init group, or the group this interpreter itself belongs to --
|
|
145
|
+
falls back to signalling the single process, because signalling the wrong
|
|
146
|
+
group would tear down unrelated processes (a CI runner, for one).
|
|
147
|
+
|
|
148
|
+
:return: True when the group was signalled, False to fall back
|
|
149
|
+
"""
|
|
150
|
+
if not hasattr(os, "killpg"): # pragma: no cover - non-POSIX
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
pid = process.pid
|
|
154
|
+
if type(pid) is not int or pid <= 1:
|
|
155
|
+
debug_log(f"Refusing to signal process group for pid {pid!r}", "debug")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
pgid = os.getpgid(pid)
|
|
160
|
+
except OSError as e:
|
|
161
|
+
debug_log(f"Could not read smoke test process group: {e}", "debug")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
# start_new_session=True makes the child its own group leader, so a pgid
|
|
165
|
+
# that differs from the pid means the process is sharing someone else's
|
|
166
|
+
# group -- never ours to signal.
|
|
167
|
+
if pgid != pid or pgid <= 1 or pgid == os.getpgrp():
|
|
168
|
+
debug_log(
|
|
169
|
+
f"Smoke test process {pid} is not its own group leader "
|
|
170
|
+
f"(pgid {pgid}); signalling the process only",
|
|
171
|
+
"debug",
|
|
172
|
+
)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
os.killpg(pgid, sig)
|
|
177
|
+
return True
|
|
178
|
+
except OSError as e:
|
|
179
|
+
debug_log(f"Could not signal smoke test process group: {e}", "debug")
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _terminate(process: "subprocess.Popen[bytes]") -> None:
|
|
184
|
+
"""Stop the server process (and its group), escalating to SIGKILL."""
|
|
185
|
+
if process.poll() is not None:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if not _signal_group(process, signal.SIGTERM):
|
|
189
|
+
process.terminate()
|
|
190
|
+
try:
|
|
191
|
+
process.wait(timeout=SHUTDOWN_TIMEOUT)
|
|
192
|
+
return
|
|
193
|
+
except subprocess.TimeoutExpired:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
if not _signal_group(process, signal.SIGKILL):
|
|
197
|
+
process.kill()
|
|
198
|
+
try:
|
|
199
|
+
process.wait(timeout=SHUTDOWN_TIMEOUT)
|
|
200
|
+
except subprocess.TimeoutExpired: # pragma: no cover - defensive
|
|
201
|
+
debug_log("Smoke test server could not be killed", "warning")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
#: Phrases uvicorn / asyncio emit when the chosen port was taken between
|
|
205
|
+
#: ``find_free_port`` releasing it and uvicorn binding it.
|
|
206
|
+
_PORT_CONFLICT_MARKERS = (
|
|
207
|
+
"address already in use",
|
|
208
|
+
"error while attempting to bind on address",
|
|
209
|
+
"only one usage of each socket address",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_port_conflict(output: str) -> bool:
|
|
214
|
+
"""Return True when captured server output shows a failed port bind."""
|
|
215
|
+
lowered = output.lower()
|
|
216
|
+
return any(marker in lowered for marker in _PORT_CONFLICT_MARKERS)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _read_log_tail(log_file: IO[bytes]) -> str:
|
|
220
|
+
"""Return the tail of the captured server output."""
|
|
221
|
+
try:
|
|
222
|
+
log_file.flush()
|
|
223
|
+
size = os.fstat(log_file.fileno()).st_size
|
|
224
|
+
log_file.seek(max(0, size - LOG_TAIL_BYTES))
|
|
225
|
+
return log_file.read().decode("utf-8", errors="replace").strip()
|
|
226
|
+
except OSError as e: # pragma: no cover - defensive
|
|
227
|
+
debug_log(f"Could not read smoke test server log: {e}", "warning")
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def check_smoke_test(ctx: InspectionContext) -> bool:
|
|
232
|
+
"""Boot the generated project and verify its HTTP surface."""
|
|
233
|
+
if not ctx.options.run_smoke_test:
|
|
234
|
+
debug_log("Smoke test disabled", "info")
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
python_executable = ctx.python_executable()
|
|
238
|
+
if not python_executable or not os.path.exists(python_executable):
|
|
239
|
+
ctx.add_error(
|
|
240
|
+
"Smoke test requires an installed environment, but no virtual "
|
|
241
|
+
"environment was prepared for the generated project"
|
|
242
|
+
)
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
app_module = resolve_app_module(ctx)
|
|
246
|
+
if not app_module:
|
|
247
|
+
ctx.add_error(
|
|
248
|
+
"Could not determine the ASGI app module: declare "
|
|
249
|
+
"[tool.fastapi-fastkit].app_module or ship a conventional main.py"
|
|
250
|
+
)
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
env = os.environ.copy()
|
|
254
|
+
env["PYTHONPATH"] = ctx.temp_dir + os.pathsep + env.get("PYTHONPATH", "")
|
|
255
|
+
|
|
256
|
+
last_failure = ""
|
|
257
|
+
for attempt in range(1, PORT_ATTEMPTS + 1):
|
|
258
|
+
port = find_free_port()
|
|
259
|
+
base_url = f"http://127.0.0.1:{port}"
|
|
260
|
+
command: List[str] = [
|
|
261
|
+
python_executable,
|
|
262
|
+
"-m",
|
|
263
|
+
"uvicorn",
|
|
264
|
+
app_module,
|
|
265
|
+
"--host",
|
|
266
|
+
"127.0.0.1",
|
|
267
|
+
"--port",
|
|
268
|
+
str(port),
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
debug_log(f"Starting smoke test server: {' '.join(command)}", "info")
|
|
272
|
+
|
|
273
|
+
# The server's output goes to a temp file rather than a pipe: uvicorn
|
|
274
|
+
# keeps logging for as long as it runs, and a pipe nobody drains fills
|
|
275
|
+
# its buffer and blocks the server mid-request.
|
|
276
|
+
with tempfile.TemporaryFile() as log_file:
|
|
277
|
+
try:
|
|
278
|
+
process = subprocess.Popen(
|
|
279
|
+
command,
|
|
280
|
+
cwd=ctx.temp_dir,
|
|
281
|
+
stdout=log_file,
|
|
282
|
+
stderr=subprocess.STDOUT,
|
|
283
|
+
env=env,
|
|
284
|
+
# Own process group, so terminating the server also stops
|
|
285
|
+
# any worker it spawned instead of orphaning it.
|
|
286
|
+
start_new_session=True,
|
|
287
|
+
)
|
|
288
|
+
except OSError as e:
|
|
289
|
+
ctx.add_error(f"Failed to start the application server: {e}")
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
reachable, reason = _wait_for_server(
|
|
294
|
+
process, base_url, ctx.options.smoke_timeout
|
|
295
|
+
)
|
|
296
|
+
if not reachable:
|
|
297
|
+
output = _read_log_tail(log_file)
|
|
298
|
+
last_failure = f"Smoke test failed: {reason}\n{output}".rstrip()
|
|
299
|
+
if _is_port_conflict(output) and attempt < PORT_ATTEMPTS:
|
|
300
|
+
debug_log(
|
|
301
|
+
f"Port {port} was taken before uvicorn bound it; "
|
|
302
|
+
f"retrying on another port ({attempt}/{PORT_ATTEMPTS})",
|
|
303
|
+
"warning",
|
|
304
|
+
)
|
|
305
|
+
continue
|
|
306
|
+
ctx.add_error(last_failure)
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
health_status = _probe(f"{base_url}/health")
|
|
310
|
+
if health_status is None:
|
|
311
|
+
ctx.add_warning("Smoke test: /health did not respond")
|
|
312
|
+
elif health_status == 404:
|
|
313
|
+
debug_log("Template exposes no /health endpoint, skipping", "info")
|
|
314
|
+
elif health_status != 200:
|
|
315
|
+
ctx.add_error(
|
|
316
|
+
f"Smoke test failed: /health returned HTTP {health_status} "
|
|
317
|
+
"(expected 200)\n" + _read_log_tail(log_file)
|
|
318
|
+
)
|
|
319
|
+
return False
|
|
320
|
+
finally:
|
|
321
|
+
_terminate(process)
|
|
322
|
+
|
|
323
|
+
debug_log("Smoke test passed", "info")
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
ctx.add_error(last_failure or "Smoke test failed: no free port could be bound")
|
|
327
|
+
return False
|