FastAPI-fastkit 1.3.0__py3-none-any.whl → 1.4.1__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 +71 -0
- fastapi_fastkit/backend/inspection/core.py +351 -0
- fastapi_fastkit/backend/inspection/docker.py +320 -0
- fastapi_fastkit/backend/inspection/freshness.py +145 -0
- fastapi_fastkit/backend/inspection/fsutils.py +139 -0
- fastapi_fastkit/backend/inspection/lint.py +103 -0
- fastapi_fastkit/backend/inspection/report.py +58 -0
- fastapi_fastkit/backend/inspection/smoke.py +381 -0
- fastapi_fastkit/backend/inspection/strategies.py +367 -0
- fastapi_fastkit/backend/inspector.py +18 -1400
- fastapi_fastkit/backend/interactive/__init__.py +6 -0
- fastapi_fastkit/backend/interactive/config_builder.py +45 -63
- fastapi_fastkit/backend/interactive/prompts.py +155 -6
- fastapi_fastkit/backend/interactive/selectors.py +18 -2
- fastapi_fastkit/backend/main.py +887 -99
- fastapi_fastkit/backend/package_managers/base.py +172 -0
- fastapi_fastkit/backend/package_managers/factory.py +5 -4
- fastapi_fastkit/backend/package_managers/pdm_manager.py +29 -82
- fastapi_fastkit/backend/package_managers/pip_manager.py +31 -85
- fastapi_fastkit/backend/package_managers/poetry_manager.py +70 -110
- fastapi_fastkit/backend/package_managers/uv_manager.py +43 -112
- fastapi_fastkit/backend/project_builder/__init__.py +8 -1
- fastapi_fastkit/backend/project_builder/config_generator.py +806 -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 +254 -28
- fastapi_fastkit/backend/scaffolder.py +578 -0
- fastapi_fastkit/backend/transducer.py +154 -22
- fastapi_fastkit/cli.py +434 -311
- fastapi_fastkit/core/settings.py +234 -45
- fastapi_fastkit/fastapi_project_template/README.md +26 -10
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +21 -22
- 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 +21 -22
- 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 +18 -19
- 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 +21 -22
- 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/pyproject.toml-tpl +13 -14
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +36 -10
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +4 -4
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +4 -3
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +2 -3
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +2 -3
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +14 -15
- 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/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +2 -2
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +25 -26
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +65 -40
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl +2 -3
- 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 +13 -14
- 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 +6 -7
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +5 -6
- 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/.env-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +22 -23
- 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 +3 -4
- 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 +38 -0
- {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/METADATA +49 -6
- fastapi_fastkit-1.4.1.dist-info/RECORD +484 -0
- {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.1.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.3.0.dist-info/RECORD +0 -287
- /fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/__init__.py-tpl +0 -0
- {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/entry_points.txt +0 -0
- {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
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: Optional["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
|
+
|
|
122
|
+
``process`` is ``None`` when the server is not ours to watch - a container
|
|
123
|
+
started by the Docker strategy, for one - in which case only the timeout
|
|
124
|
+
bounds the wait.
|
|
125
|
+
"""
|
|
126
|
+
deadline = time.time() + timeout
|
|
127
|
+
while time.time() < deadline:
|
|
128
|
+
if process is not None and process.poll() is not None:
|
|
129
|
+
return False, "server process exited before becoming reachable"
|
|
130
|
+
status = _probe(f"{base_url}/docs")
|
|
131
|
+
if status is not None:
|
|
132
|
+
if status == 200:
|
|
133
|
+
return True, ""
|
|
134
|
+
return False, f"/docs returned HTTP {status} (expected 200)"
|
|
135
|
+
time.sleep(0.5)
|
|
136
|
+
return False, f"server did not become reachable within {timeout}s"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _signal_group(process: "subprocess.Popen[bytes]", sig: int) -> bool:
|
|
140
|
+
"""Signal the server's whole process group, if it has one of its own.
|
|
141
|
+
|
|
142
|
+
uvicorn spawns reloader / worker children, and signalling only the
|
|
143
|
+
parent leaves them holding the port. The process is started with
|
|
144
|
+
``start_new_session=True`` so the group id equals its pid.
|
|
145
|
+
|
|
146
|
+
Only a group we created ourselves is ever signalled: the pid must be a
|
|
147
|
+
real positive integer and must be its own group leader. Anything else --
|
|
148
|
+
a mocked process object, a pid that has been reaped and rejoined another
|
|
149
|
+
group, the init group, or the group this interpreter itself belongs to --
|
|
150
|
+
falls back to signalling the single process, because signalling the wrong
|
|
151
|
+
group would tear down unrelated processes (a CI runner, for one).
|
|
152
|
+
|
|
153
|
+
:return: True when the group was signalled, False to fall back
|
|
154
|
+
"""
|
|
155
|
+
if not hasattr(os, "killpg"): # pragma: no cover - non-POSIX
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
pid = process.pid
|
|
159
|
+
if type(pid) is not int or pid <= 1:
|
|
160
|
+
debug_log(f"Refusing to signal process group for pid {pid!r}", "debug")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
pgid = os.getpgid(pid)
|
|
165
|
+
except OSError as e:
|
|
166
|
+
debug_log(f"Could not read smoke test process group: {e}", "debug")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
# start_new_session=True makes the child its own group leader, so a pgid
|
|
170
|
+
# that differs from the pid means the process is sharing someone else's
|
|
171
|
+
# group -- never ours to signal.
|
|
172
|
+
if pgid != pid or pgid <= 1 or pgid == os.getpgrp():
|
|
173
|
+
debug_log(
|
|
174
|
+
f"Smoke test process {pid} is not its own group leader "
|
|
175
|
+
f"(pgid {pgid}); signalling the process only",
|
|
176
|
+
"debug",
|
|
177
|
+
)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
os.killpg(pgid, sig)
|
|
182
|
+
return True
|
|
183
|
+
except OSError as e:
|
|
184
|
+
debug_log(f"Could not signal smoke test process group: {e}", "debug")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _terminate(process: "subprocess.Popen[bytes]") -> None:
|
|
189
|
+
"""Stop the server process (and its group), escalating to SIGKILL."""
|
|
190
|
+
if process.poll() is not None:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if not _signal_group(process, signal.SIGTERM):
|
|
194
|
+
process.terminate()
|
|
195
|
+
try:
|
|
196
|
+
process.wait(timeout=SHUTDOWN_TIMEOUT)
|
|
197
|
+
return
|
|
198
|
+
except subprocess.TimeoutExpired:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
if not _signal_group(process, signal.SIGKILL):
|
|
202
|
+
process.kill()
|
|
203
|
+
try:
|
|
204
|
+
process.wait(timeout=SHUTDOWN_TIMEOUT)
|
|
205
|
+
except subprocess.TimeoutExpired: # pragma: no cover - defensive
|
|
206
|
+
debug_log("Smoke test server could not be killed", "warning")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
#: Phrases uvicorn / asyncio emit when the chosen port was taken between
|
|
210
|
+
#: ``find_free_port`` releasing it and uvicorn binding it.
|
|
211
|
+
_PORT_CONFLICT_MARKERS = (
|
|
212
|
+
"address already in use",
|
|
213
|
+
"error while attempting to bind on address",
|
|
214
|
+
"only one usage of each socket address",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_port_conflict(output: str) -> bool:
|
|
219
|
+
"""Return True when captured server output shows a failed port bind."""
|
|
220
|
+
lowered = output.lower()
|
|
221
|
+
return any(marker in lowered for marker in _PORT_CONFLICT_MARKERS)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _read_log_tail(log_file: IO[bytes]) -> str:
|
|
225
|
+
"""Return the tail of the captured server output."""
|
|
226
|
+
try:
|
|
227
|
+
log_file.flush()
|
|
228
|
+
size = os.fstat(log_file.fileno()).st_size
|
|
229
|
+
log_file.seek(max(0, size - LOG_TAIL_BYTES))
|
|
230
|
+
return log_file.read().decode("utf-8", errors="replace").strip()
|
|
231
|
+
except OSError as e: # pragma: no cover - defensive
|
|
232
|
+
debug_log(f"Could not read smoke test server log: {e}", "warning")
|
|
233
|
+
return ""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _check_health_endpoint(ctx: InspectionContext, base_url: str) -> Optional[str]:
|
|
237
|
+
"""Probe ``/health``; return a failure message, or ``None`` when acceptable.
|
|
238
|
+
|
|
239
|
+
A template without a ``/health`` route answers 404, which is fine; an
|
|
240
|
+
unreachable endpoint is only worth a warning, but a route that exists and
|
|
241
|
+
answers with anything other than 200 is a real failure.
|
|
242
|
+
"""
|
|
243
|
+
health_status = _probe(f"{base_url}/health")
|
|
244
|
+
if health_status is None:
|
|
245
|
+
ctx.add_warning("Smoke test: /health did not respond")
|
|
246
|
+
elif health_status == 404:
|
|
247
|
+
debug_log("Template exposes no /health endpoint, skipping", "info")
|
|
248
|
+
elif health_status != 200:
|
|
249
|
+
return (
|
|
250
|
+
f"Smoke test failed: /health returned HTTP {health_status} (expected 200)"
|
|
251
|
+
)
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def run_http_smoke(ctx: InspectionContext, base_url: str) -> bool:
|
|
256
|
+
"""Verify the HTTP surface of a server someone else already started.
|
|
257
|
+
|
|
258
|
+
Used by the Docker strategy, which has the real application running in a
|
|
259
|
+
container: probing its published port is more honest than booting a second
|
|
260
|
+
copy on the host, and templates that require Docker never get a host venv
|
|
261
|
+
to boot one with in the first place.
|
|
262
|
+
"""
|
|
263
|
+
debug_log(f"Running smoke test against {base_url}", "info")
|
|
264
|
+
reachable, reason = _wait_for_server(None, base_url, ctx.options.smoke_timeout)
|
|
265
|
+
if not reachable:
|
|
266
|
+
ctx.add_error(f"Smoke test failed: {reason}")
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
failure = _check_health_endpoint(ctx, base_url)
|
|
270
|
+
if failure:
|
|
271
|
+
ctx.add_error(failure)
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
debug_log("Smoke test passed", "info")
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _requires_docker(ctx: InspectionContext) -> bool:
|
|
279
|
+
"""Whether the template declares that it can only run under Docker."""
|
|
280
|
+
return bool((ctx.template_config or {}).get("requires_docker", False))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def check_smoke_test(ctx: InspectionContext) -> bool:
|
|
284
|
+
"""Boot the generated project and verify its HTTP surface."""
|
|
285
|
+
if not ctx.options.run_smoke_test:
|
|
286
|
+
debug_log("Smoke test disabled", "info")
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
if ctx.smoke_result is not None:
|
|
290
|
+
debug_log("Reusing the smoke test result recorded while testing", "info")
|
|
291
|
+
return ctx.smoke_result
|
|
292
|
+
|
|
293
|
+
python_executable = ctx.python_executable()
|
|
294
|
+
if not python_executable or not os.path.exists(python_executable):
|
|
295
|
+
if _requires_docker(ctx):
|
|
296
|
+
ctx.add_warning(
|
|
297
|
+
"smoke test skipped: Docker template without published port"
|
|
298
|
+
)
|
|
299
|
+
return True
|
|
300
|
+
ctx.add_error(
|
|
301
|
+
"Smoke test requires an installed environment, but no virtual "
|
|
302
|
+
"environment was prepared for the generated project"
|
|
303
|
+
)
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
app_module = resolve_app_module(ctx)
|
|
307
|
+
if not app_module:
|
|
308
|
+
ctx.add_error(
|
|
309
|
+
"Could not determine the ASGI app module: declare "
|
|
310
|
+
"[tool.fastapi-fastkit].app_module or ship a conventional main.py"
|
|
311
|
+
)
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
env = os.environ.copy()
|
|
315
|
+
env["PYTHONPATH"] = ctx.temp_dir + os.pathsep + env.get("PYTHONPATH", "")
|
|
316
|
+
|
|
317
|
+
last_failure = ""
|
|
318
|
+
for attempt in range(1, PORT_ATTEMPTS + 1):
|
|
319
|
+
port = find_free_port()
|
|
320
|
+
base_url = f"http://127.0.0.1:{port}"
|
|
321
|
+
command: List[str] = [
|
|
322
|
+
python_executable,
|
|
323
|
+
"-m",
|
|
324
|
+
"uvicorn",
|
|
325
|
+
app_module,
|
|
326
|
+
"--host",
|
|
327
|
+
"127.0.0.1",
|
|
328
|
+
"--port",
|
|
329
|
+
str(port),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
debug_log(f"Starting smoke test server: {' '.join(command)}", "info")
|
|
333
|
+
|
|
334
|
+
# The server's output goes to a temp file rather than a pipe: uvicorn
|
|
335
|
+
# keeps logging for as long as it runs, and a pipe nobody drains fills
|
|
336
|
+
# its buffer and blocks the server mid-request.
|
|
337
|
+
with tempfile.TemporaryFile() as log_file:
|
|
338
|
+
try:
|
|
339
|
+
process = subprocess.Popen(
|
|
340
|
+
command,
|
|
341
|
+
cwd=ctx.temp_dir,
|
|
342
|
+
stdout=log_file,
|
|
343
|
+
stderr=subprocess.STDOUT,
|
|
344
|
+
env=env,
|
|
345
|
+
# Own process group, so terminating the server also stops
|
|
346
|
+
# any worker it spawned instead of orphaning it.
|
|
347
|
+
start_new_session=True,
|
|
348
|
+
)
|
|
349
|
+
except OSError as e:
|
|
350
|
+
ctx.add_error(f"Failed to start the application server: {e}")
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
reachable, reason = _wait_for_server(
|
|
355
|
+
process, base_url, ctx.options.smoke_timeout
|
|
356
|
+
)
|
|
357
|
+
if not reachable:
|
|
358
|
+
output = _read_log_tail(log_file)
|
|
359
|
+
last_failure = f"Smoke test failed: {reason}\n{output}".rstrip()
|
|
360
|
+
if _is_port_conflict(output) and attempt < PORT_ATTEMPTS:
|
|
361
|
+
debug_log(
|
|
362
|
+
f"Port {port} was taken before uvicorn bound it; "
|
|
363
|
+
f"retrying on another port ({attempt}/{PORT_ATTEMPTS})",
|
|
364
|
+
"warning",
|
|
365
|
+
)
|
|
366
|
+
continue
|
|
367
|
+
ctx.add_error(last_failure)
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
failure = _check_health_endpoint(ctx, base_url)
|
|
371
|
+
if failure:
|
|
372
|
+
ctx.add_error(f"{failure}\n{_read_log_tail(log_file)}".rstrip())
|
|
373
|
+
return False
|
|
374
|
+
finally:
|
|
375
|
+
_terminate(process)
|
|
376
|
+
|
|
377
|
+
debug_log("Smoke test passed", "info")
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
ctx.add_error(last_failure or "Smoke test failed: no free port could be bound")
|
|
381
|
+
return False
|