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,351 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The TemplateInspector orchestrator.
|
|
3
|
+
#
|
|
4
|
+
# The inspector generates a real project from the template (copy + convert +
|
|
5
|
+
# metadata injection), runs every check against that generated project, and
|
|
6
|
+
# cleans the temporary directory up afterwards. Individual checks live in
|
|
7
|
+
# sibling modules; this class only sequences them and owns the lifecycle.
|
|
8
|
+
#
|
|
9
|
+
# @author bnbong
|
|
10
|
+
# --------------------------------------------------------------------------
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import SimpleNamespace
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from fastapi_fastkit.backend.scaffolder import ProjectScaffolder, ScaffoldOptions
|
|
19
|
+
from fastapi_fastkit.core.settings import settings
|
|
20
|
+
from fastapi_fastkit.utils.logging import debug_log
|
|
21
|
+
|
|
22
|
+
from . import checks, consistency, freshness, lint, smoke, strategies
|
|
23
|
+
from .context import InspectionContext, InspectionOptions
|
|
24
|
+
from .docker import DockerCompose
|
|
25
|
+
from .fsutils import force_cleanup_directory
|
|
26
|
+
from .report import build_report, print_report
|
|
27
|
+
|
|
28
|
+
#: Metadata used to generate the throwaway project that gets inspected.
|
|
29
|
+
DUMMY_METADATA = {
|
|
30
|
+
"project_name": "test-template",
|
|
31
|
+
"author": "Template Inspector",
|
|
32
|
+
"author_email": "inspector@fastapi-fastkit.dev",
|
|
33
|
+
"description": "Test project for template inspection",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TemplateInspector:
|
|
38
|
+
"""Validate a FastAPI template by generating and exercising a project.
|
|
39
|
+
|
|
40
|
+
Uses the context manager protocol so the temporary project directory is
|
|
41
|
+
always removed, even when a check raises.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
template_path: str,
|
|
47
|
+
temp_base_dir: Optional[str] = None,
|
|
48
|
+
options: Optional[InspectionOptions] = None,
|
|
49
|
+
):
|
|
50
|
+
template_name = Path(template_path).name
|
|
51
|
+
base_dir = temp_base_dir or os.path.dirname(os.path.dirname(__file__))
|
|
52
|
+
self.ctx = InspectionContext(
|
|
53
|
+
template_path=Path(template_path),
|
|
54
|
+
temp_dir=os.path.join(base_dir, f"temp_{template_name}"),
|
|
55
|
+
options=options or InspectionOptions(),
|
|
56
|
+
)
|
|
57
|
+
self._cleanup_needed = False
|
|
58
|
+
|
|
59
|
+
# -- state exposed for callers and tests -------------------------------
|
|
60
|
+
@property
|
|
61
|
+
def template_path(self) -> Path:
|
|
62
|
+
"""The on-disk template directory being inspected."""
|
|
63
|
+
return self.ctx.template_path
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def temp_dir(self) -> str:
|
|
67
|
+
"""The generated project directory used for the inspection."""
|
|
68
|
+
return self.ctx.temp_dir
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def errors(self) -> List[str]:
|
|
72
|
+
"""Fatal findings accumulated so far."""
|
|
73
|
+
return self.ctx.errors
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def warnings(self) -> List[str]:
|
|
77
|
+
"""Non-fatal findings accumulated so far."""
|
|
78
|
+
return self.ctx.warnings
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def template_config(self) -> Optional[Dict[str, Any]]:
|
|
82
|
+
"""Parsed ``template-config.yml`` contents, when the template ships one."""
|
|
83
|
+
return self.ctx.template_config
|
|
84
|
+
|
|
85
|
+
# -- lifecycle ---------------------------------------------------------
|
|
86
|
+
def __enter__(self) -> "TemplateInspector":
|
|
87
|
+
"""Create the temporary directory and generate a project from the template."""
|
|
88
|
+
try:
|
|
89
|
+
if os.path.exists(self.ctx.temp_dir):
|
|
90
|
+
debug_log(
|
|
91
|
+
f"Cleaning up existing temp directory: {self.ctx.temp_dir}", "info"
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
shutil.rmtree(self.ctx.temp_dir)
|
|
95
|
+
except OSError as e:
|
|
96
|
+
debug_log(
|
|
97
|
+
f"Failed to cleanup existing temp directory: {e}", "warning"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
os.makedirs(self.ctx.temp_dir, exist_ok=True)
|
|
101
|
+
# Mark for cleanup immediately: a failure below must not leak the
|
|
102
|
+
# directory that was just created.
|
|
103
|
+
self._cleanup_needed = True
|
|
104
|
+
self._generate_project()
|
|
105
|
+
|
|
106
|
+
self.ctx.template_config = self._load_template_config()
|
|
107
|
+
debug_log(f"Created temporary directory at {self.ctx.temp_dir}", "debug")
|
|
108
|
+
return self
|
|
109
|
+
except Exception as e:
|
|
110
|
+
debug_log(f"Failed to setup template inspector: {e}", "error")
|
|
111
|
+
self._cleanup()
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
115
|
+
"""Remove the generated project directory."""
|
|
116
|
+
self._cleanup()
|
|
117
|
+
|
|
118
|
+
def _cleanup(self) -> None:
|
|
119
|
+
"""Tear down Docker services and delete the temporary directory."""
|
|
120
|
+
if not (self._cleanup_needed and os.path.exists(self.ctx.temp_dir)):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
if self._cleanup_docker_services():
|
|
125
|
+
# Give the daemon a moment to release bind-mounted files.
|
|
126
|
+
time.sleep(3)
|
|
127
|
+
force_cleanup_directory(self.ctx.temp_dir)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
debug_log(
|
|
130
|
+
f"Warning: Unexpected error during cleanup of "
|
|
131
|
+
f"{self.ctx.temp_dir}: {e}",
|
|
132
|
+
"warning",
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
force_cleanup_directory(self.ctx.temp_dir)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
finally:
|
|
139
|
+
self._cleanup_needed = False
|
|
140
|
+
|
|
141
|
+
def _cleanup_docker_services(self) -> bool:
|
|
142
|
+
"""Stop any Docker Compose stack started for this template.
|
|
143
|
+
|
|
144
|
+
Returns ``True`` when a teardown was actually attempted - templates
|
|
145
|
+
without a compose file skip the (slow) docker invocations entirely.
|
|
146
|
+
"""
|
|
147
|
+
compose_files = [
|
|
148
|
+
name
|
|
149
|
+
for name in ("docker-compose.yml", "docker-compose.yaml", "compose.yml")
|
|
150
|
+
if os.path.exists(self.ctx.temp_path(name))
|
|
151
|
+
]
|
|
152
|
+
if not compose_files:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
DockerCompose(self.ctx.temp_dir, compose_files[0]).cleanup()
|
|
157
|
+
return True
|
|
158
|
+
except Exception as e:
|
|
159
|
+
debug_log(f"Failed to cleanup Docker services: {e}", "warning")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def _generate_project(self) -> None:
|
|
163
|
+
"""Produce the project to inspect through the real ``startdemo`` path.
|
|
164
|
+
|
|
165
|
+
Running the actual ``ProjectScaffolder`` (rather than re-implementing
|
|
166
|
+
copy + inject here) is the point: whatever generation does for a user
|
|
167
|
+
- placeholder substitution, the ``[tool.fastapi-fastkit]`` metadata
|
|
168
|
+
block, the template-only files it filters out - is exactly what the
|
|
169
|
+
checks then see. Virtualenv creation and dependency installation are
|
|
170
|
+
switched off, so no package manager is ever invoked.
|
|
171
|
+
|
|
172
|
+
A generation failure is propagated: there is nothing meaningful to
|
|
173
|
+
inspect when the template cannot even be deployed, and ``__enter__``
|
|
174
|
+
already removes the half-written directory.
|
|
175
|
+
"""
|
|
176
|
+
template_path = self.ctx.template_path.resolve()
|
|
177
|
+
project_dir = Path(self.ctx.temp_dir).resolve()
|
|
178
|
+
|
|
179
|
+
scaffold_settings = SimpleNamespace(
|
|
180
|
+
FASTKIT_TEMPLATE_ROOT=str(template_path.parent),
|
|
181
|
+
USER_WORKSPACE=str(project_dir.parent),
|
|
182
|
+
PACKAGE_MANAGER_CONFIG=settings.PACKAGE_MANAGER_CONFIG,
|
|
183
|
+
)
|
|
184
|
+
options = ScaffoldOptions(
|
|
185
|
+
project_name=project_dir.name,
|
|
186
|
+
author=str(DUMMY_METADATA["author"]),
|
|
187
|
+
author_email=str(DUMMY_METADATA["author_email"]),
|
|
188
|
+
description=str(DUMMY_METADATA["description"]),
|
|
189
|
+
package_manager="uv",
|
|
190
|
+
template=template_path.name,
|
|
191
|
+
create_project_folder=True,
|
|
192
|
+
with_venv=False,
|
|
193
|
+
with_install=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
ProjectScaffolder(scaffold_settings, options).run()
|
|
197
|
+
|
|
198
|
+
def _load_template_config(self) -> Optional[Dict[str, Any]]:
|
|
199
|
+
"""Load ``template-config.yml`` from the template, if it ships one.
|
|
200
|
+
|
|
201
|
+
The file is read from the template directory rather than the generated
|
|
202
|
+
project: it is inspector-only metadata and is deliberately not copied
|
|
203
|
+
into a user's project by the transducer.
|
|
204
|
+
"""
|
|
205
|
+
config_file = str(self.ctx.template_path / "template-config.yml-tpl")
|
|
206
|
+
if not os.path.exists(config_file):
|
|
207
|
+
# Legacy layout: some templates shipped the config without the
|
|
208
|
+
# ``-tpl`` marker, in which case it still lands in the copy.
|
|
209
|
+
config_file = self.ctx.temp_path("template-config.yml")
|
|
210
|
+
if not os.path.exists(config_file):
|
|
211
|
+
debug_log(
|
|
212
|
+
"No template-config.yml found, using default testing strategy", "info"
|
|
213
|
+
)
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
import yaml
|
|
218
|
+
except ImportError: # pragma: no cover - PyYAML is an optional extra
|
|
219
|
+
debug_log(
|
|
220
|
+
"PyYAML is not installed; ignoring template-config.yml", "warning"
|
|
221
|
+
)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
226
|
+
config = yaml.safe_load(f)
|
|
227
|
+
except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
|
|
228
|
+
debug_log(f"Failed to load template configuration: {e}", "warning")
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
if not isinstance(config, dict):
|
|
232
|
+
debug_log("Invalid template configuration format", "warning")
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
debug_log(
|
|
236
|
+
f"Loaded template configuration: {config.get('name', 'Unknown')}", "info"
|
|
237
|
+
)
|
|
238
|
+
return config
|
|
239
|
+
|
|
240
|
+
# -- checks ------------------------------------------------------------
|
|
241
|
+
def inspect_template(self) -> bool:
|
|
242
|
+
"""Run every inspection step, stopping at the first failure.
|
|
243
|
+
|
|
244
|
+
:return: True if template is valid, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
check_list: List[Tuple[str, Callable[[], bool]]] = [
|
|
247
|
+
("File Structure", self._check_file_structure),
|
|
248
|
+
("File Extensions", self._check_file_extensions),
|
|
249
|
+
("Test Suite Presence", self._check_tests_present),
|
|
250
|
+
("Junk Files", self._check_no_junk_files),
|
|
251
|
+
("Dependencies", self._check_dependencies),
|
|
252
|
+
("Configuration Consistency", self._check_configuration_consistency),
|
|
253
|
+
("FastAPI Implementation", self._check_fastapi_implementation),
|
|
254
|
+
("Placeholder Substitution", self._check_no_placeholder_residue),
|
|
255
|
+
# Compile Check is a pure static check: it runs before the (much
|
|
256
|
+
# heavier) Template Tests so a syntax error is reported without
|
|
257
|
+
# waiting for an environment, and so it never observes files a
|
|
258
|
+
# containerised test run left behind.
|
|
259
|
+
("Compile Check", self._check_compileall),
|
|
260
|
+
("Template Tests", self._test_template),
|
|
261
|
+
("Type Check", self._check_mypy),
|
|
262
|
+
("Smoke Test", self._check_smoke_test),
|
|
263
|
+
("Dependency Freshness", self._check_dependency_freshness),
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
for check_name, check_func in check_list:
|
|
267
|
+
debug_log(f"Running check: {check_name}", "info")
|
|
268
|
+
if not check_func():
|
|
269
|
+
debug_log(f"Check failed: {check_name}", "error")
|
|
270
|
+
return False
|
|
271
|
+
debug_log(f"Check passed: {check_name}", "info")
|
|
272
|
+
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
def _check_file_structure(self) -> bool:
|
|
276
|
+
return checks.check_file_structure(self.ctx)
|
|
277
|
+
|
|
278
|
+
def _check_file_extensions(self) -> bool:
|
|
279
|
+
return checks.check_file_extensions(self.ctx)
|
|
280
|
+
|
|
281
|
+
def _check_dependencies(self) -> bool:
|
|
282
|
+
return checks.check_dependencies(self.ctx)
|
|
283
|
+
|
|
284
|
+
def _check_fastapi_implementation(self) -> bool:
|
|
285
|
+
return checks.check_fastapi_implementation(self.ctx)
|
|
286
|
+
|
|
287
|
+
def _check_tests_present(self) -> bool:
|
|
288
|
+
return checks.check_tests_present(self.ctx)
|
|
289
|
+
|
|
290
|
+
def _check_no_junk_files(self) -> bool:
|
|
291
|
+
return checks.check_no_junk_files(self.ctx)
|
|
292
|
+
|
|
293
|
+
def _check_no_placeholder_residue(self) -> bool:
|
|
294
|
+
return checks.check_no_placeholder_residue(self.ctx)
|
|
295
|
+
|
|
296
|
+
def _check_configuration_consistency(self) -> bool:
|
|
297
|
+
return consistency.check_configuration_consistency(self.ctx)
|
|
298
|
+
|
|
299
|
+
def _check_compileall(self) -> bool:
|
|
300
|
+
return lint.check_compileall(self.ctx)
|
|
301
|
+
|
|
302
|
+
def _check_mypy(self) -> bool:
|
|
303
|
+
return lint.check_mypy(self.ctx)
|
|
304
|
+
|
|
305
|
+
def _check_smoke_test(self) -> bool:
|
|
306
|
+
return smoke.check_smoke_test(self.ctx)
|
|
307
|
+
|
|
308
|
+
def _check_dependency_freshness(self) -> bool:
|
|
309
|
+
return freshness.check_dependency_freshness(self.ctx)
|
|
310
|
+
|
|
311
|
+
def _test_template(self) -> bool:
|
|
312
|
+
return strategies.run_template_tests(self.ctx)
|
|
313
|
+
|
|
314
|
+
def get_report(self) -> Dict[str, Any]:
|
|
315
|
+
"""
|
|
316
|
+
Get inspection report with errors and warnings.
|
|
317
|
+
|
|
318
|
+
:return: Dictionary containing inspection results
|
|
319
|
+
"""
|
|
320
|
+
return build_report(self.ctx)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def inspect_fastapi_template(
|
|
324
|
+
template_path: str,
|
|
325
|
+
temp_base_dir: Optional[str] = None,
|
|
326
|
+
options: Optional[InspectionOptions] = None,
|
|
327
|
+
) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Convenience function to inspect a FastAPI template.
|
|
330
|
+
|
|
331
|
+
:param template_path: Path to the template to inspect
|
|
332
|
+
:param temp_base_dir: Base directory for temporary files (defaults to backend directory)
|
|
333
|
+
:param options: Optional switches (offline mode, smoke test, mypy, ...)
|
|
334
|
+
:return: Inspection report dictionary
|
|
335
|
+
"""
|
|
336
|
+
template_name = Path(template_path).name
|
|
337
|
+
debug_log(
|
|
338
|
+
f"Starting template inspection for {template_name} at {template_path}", "info"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
with TemplateInspector(template_path, temp_base_dir, options) as inspector:
|
|
342
|
+
is_valid = inspector.inspect_template()
|
|
343
|
+
report = inspector.get_report()
|
|
344
|
+
print_report(inspector.ctx, is_valid)
|
|
345
|
+
|
|
346
|
+
debug_log(
|
|
347
|
+
f"Template inspection completed for {template_name}. Valid: {is_valid}, "
|
|
348
|
+
f"Errors: {len(report['errors'])}, Warnings: {len(report['warnings'])}",
|
|
349
|
+
"info",
|
|
350
|
+
)
|
|
351
|
+
return report
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Docker Compose orchestration for templates that declare requires_docker.
|
|
3
|
+
#
|
|
4
|
+
# ``DockerCompose`` owns every ``docker``/``docker-compose`` invocation so the
|
|
5
|
+
# test strategies stay free of process plumbing.
|
|
6
|
+
#
|
|
7
|
+
# @author bnbong
|
|
8
|
+
# --------------------------------------------------------------------------
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from fastapi_fastkit.utils.logging import debug_log
|
|
16
|
+
|
|
17
|
+
COMPOSE_COMMAND = "docker-compose"
|
|
18
|
+
#: Fallback for hosts that only ship the ``docker compose`` CLI plugin.
|
|
19
|
+
COMPOSE_PLUGIN_COMMAND = ["docker", "compose"]
|
|
20
|
+
#: Resolved compose invocation, switched by :meth:`DockerCompose.is_available`.
|
|
21
|
+
_compose_prefix: List[str] = [COMPOSE_COMMAND]
|
|
22
|
+
#: Throwaway image used to give bind-mounted files back to the host user.
|
|
23
|
+
RECLAIM_IMAGE = "alpine:3.20"
|
|
24
|
+
SHORT_TIMEOUT = 10
|
|
25
|
+
STATUS_TIMEOUT = 30
|
|
26
|
+
CLEANUP_TIMEOUT = 60
|
|
27
|
+
TEST_TIMEOUT = 300
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DockerCompose:
|
|
31
|
+
"""Thin wrapper around the docker-compose CLI for one project directory."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_dir: str, compose_file: str = "docker-compose.yml"):
|
|
34
|
+
self.project_dir = project_dir
|
|
35
|
+
self.compose_file = compose_file
|
|
36
|
+
|
|
37
|
+
def _run(
|
|
38
|
+
self, args: Sequence[str], timeout: int
|
|
39
|
+
) -> Optional[subprocess.CompletedProcess[str]]:
|
|
40
|
+
"""Run a command in the project directory, returning ``None`` on failure."""
|
|
41
|
+
try:
|
|
42
|
+
return subprocess.run(
|
|
43
|
+
list(args),
|
|
44
|
+
cwd=self.project_dir,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
)
|
|
49
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
50
|
+
debug_log(f"Command {' '.join(args)} failed: {e}", "warning")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def compose_command() -> List[str]:
|
|
55
|
+
"""Return the compose invocation resolved by :meth:`is_available`."""
|
|
56
|
+
return list(_compose_prefix)
|
|
57
|
+
|
|
58
|
+
def _compose(
|
|
59
|
+
self, args: Sequence[str], timeout: int
|
|
60
|
+
) -> Optional[subprocess.CompletedProcess[str]]:
|
|
61
|
+
"""Run a docker-compose subcommand against the configured compose file."""
|
|
62
|
+
return self._run(
|
|
63
|
+
[*self.compose_command(), "-f", self.compose_file, *args], timeout=timeout
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _probe(command: Sequence[str]) -> bool:
|
|
68
|
+
"""Return whether ``command`` exits successfully."""
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
list(command), capture_output=True, text=True, timeout=SHORT_TIMEOUT
|
|
72
|
+
)
|
|
73
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
74
|
+
return False
|
|
75
|
+
return result.returncode == 0
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def is_available() -> bool:
|
|
79
|
+
"""Check that docker responds and a compose implementation exists.
|
|
80
|
+
|
|
81
|
+
``docker-compose`` stays the preferred command; hosts that only ship
|
|
82
|
+
the ``docker compose`` CLI plugin fall back to it.
|
|
83
|
+
"""
|
|
84
|
+
global _compose_prefix
|
|
85
|
+
|
|
86
|
+
if not DockerCompose._probe(["docker", "--version"]):
|
|
87
|
+
return False
|
|
88
|
+
if DockerCompose._probe([COMPOSE_COMMAND, "--version"]):
|
|
89
|
+
_compose_prefix = [COMPOSE_COMMAND]
|
|
90
|
+
return True
|
|
91
|
+
if DockerCompose._probe([*COMPOSE_PLUGIN_COMMAND, "version"]):
|
|
92
|
+
debug_log("Falling back to the 'docker compose' CLI plugin", "info")
|
|
93
|
+
_compose_prefix = list(COMPOSE_PLUGIN_COMMAND)
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def _services(self, timeout: int) -> List[Dict[str, Any]]:
|
|
98
|
+
"""Return the parsed ``docker-compose ps --format json`` entries."""
|
|
99
|
+
result = self._compose(["ps", "--format", "json"], timeout=timeout)
|
|
100
|
+
if result is None or result.returncode != 0:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
# Compose sometimes interleaves ``time="..." level=warning`` log lines
|
|
104
|
+
# with the JSON payload, so only JSON-looking lines are considered.
|
|
105
|
+
payload = "\n".join(
|
|
106
|
+
line
|
|
107
|
+
for line in result.stdout.splitlines()
|
|
108
|
+
if line.strip().startswith(("[", "{"))
|
|
109
|
+
).strip()
|
|
110
|
+
if not payload:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
# Compose < v2.21 prints a single JSON array, newer versions print one
|
|
114
|
+
# JSON object per line (NDJSON).
|
|
115
|
+
try:
|
|
116
|
+
document = json.loads(payload)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
return DockerCompose._parse_json_lines(payload)
|
|
119
|
+
|
|
120
|
+
if isinstance(document, dict):
|
|
121
|
+
return [document]
|
|
122
|
+
if isinstance(document, list):
|
|
123
|
+
return [entry for entry in document if isinstance(entry, dict)]
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _parse_json_lines(payload: str) -> List[Dict[str, Any]]:
|
|
128
|
+
"""Parse NDJSON output, skipping malformed or non-object lines."""
|
|
129
|
+
services: List[Dict[str, Any]] = []
|
|
130
|
+
for line in payload.split("\n"):
|
|
131
|
+
if not line.strip():
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
entry = json.loads(line)
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
continue
|
|
137
|
+
if isinstance(entry, dict):
|
|
138
|
+
services.append(entry)
|
|
139
|
+
return services
|
|
140
|
+
|
|
141
|
+
def containers_running(self) -> bool:
|
|
142
|
+
"""Check whether every container of the project is already up."""
|
|
143
|
+
result = self._compose(["ps", "-q"], timeout=STATUS_TIMEOUT)
|
|
144
|
+
if result is None or result.returncode != 0:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
container_ids = [line.strip() for line in result.stdout.strip().split("\n")]
|
|
148
|
+
container_ids = [cid for cid in container_ids if cid]
|
|
149
|
+
if not container_ids:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
for container_id in container_ids:
|
|
153
|
+
status = self._run(
|
|
154
|
+
["docker", "inspect", "-f", "{{.State.Running}}", container_id],
|
|
155
|
+
timeout=SHORT_TIMEOUT,
|
|
156
|
+
)
|
|
157
|
+
if status is None or status.returncode != 0:
|
|
158
|
+
return False
|
|
159
|
+
if status.stdout.strip() != "true":
|
|
160
|
+
return False
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
def up(self, timeout: int) -> subprocess.CompletedProcess[str]:
|
|
164
|
+
"""Build and start the compose services in the background."""
|
|
165
|
+
result = self._compose(["up", "-d", "--build"], timeout=timeout)
|
|
166
|
+
if result is None:
|
|
167
|
+
raise subprocess.TimeoutExpired(" ".join(self.compose_command()), timeout)
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
def wait_until_healthy(self, timeout: int) -> None:
|
|
171
|
+
"""Block until every service reports ``running`` or ``timeout`` elapses."""
|
|
172
|
+
debug_log("Waiting for services to be healthy...", "info")
|
|
173
|
+
deadline = time.time() + timeout
|
|
174
|
+
|
|
175
|
+
while time.time() < deadline:
|
|
176
|
+
services = self._services(timeout=SHORT_TIMEOUT)
|
|
177
|
+
if services and all(
|
|
178
|
+
service.get("State") == "running" for service in services
|
|
179
|
+
):
|
|
180
|
+
debug_log("All services are running", "info")
|
|
181
|
+
# Give the application a moment to finish booting.
|
|
182
|
+
time.sleep(10)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
debug_log("Services not ready yet, waiting...", "info")
|
|
186
|
+
time.sleep(5)
|
|
187
|
+
|
|
188
|
+
debug_log(
|
|
189
|
+
f"Services did not become healthy within {timeout} seconds", "warning"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def verify_services_running(self) -> Optional[str]:
|
|
193
|
+
"""Return ``None`` when db and app are up, else a descriptive error."""
|
|
194
|
+
services = self._services(timeout=STATUS_TIMEOUT)
|
|
195
|
+
if not services:
|
|
196
|
+
return "Failed to check service status"
|
|
197
|
+
|
|
198
|
+
db_running = False
|
|
199
|
+
app_running = False
|
|
200
|
+
for service in services:
|
|
201
|
+
name = str(service.get("Name", ""))
|
|
202
|
+
state = str(service.get("State", ""))
|
|
203
|
+
debug_log(f"Service {name}: {state}", "info")
|
|
204
|
+
if "db" in name and state == "running":
|
|
205
|
+
db_running = True
|
|
206
|
+
elif "app" in name and state == "running":
|
|
207
|
+
app_running = True
|
|
208
|
+
|
|
209
|
+
if not db_running:
|
|
210
|
+
return "Database service is not running"
|
|
211
|
+
if not app_running:
|
|
212
|
+
logs = self._compose(
|
|
213
|
+
["logs", "--tail", "50", "app"], timeout=STATUS_TIMEOUT
|
|
214
|
+
)
|
|
215
|
+
if logs is not None and logs.returncode == 0:
|
|
216
|
+
return (
|
|
217
|
+
"Application service is not running. " f"Logs: {logs.stdout[-500:]}"
|
|
218
|
+
)
|
|
219
|
+
return "Application service is not running"
|
|
220
|
+
|
|
221
|
+
debug_log("All required services are running", "info")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def published_port(self, service_hint: str = "app") -> Optional[int]:
|
|
225
|
+
"""Return the host port the app container publishes, if any.
|
|
226
|
+
|
|
227
|
+
``docker-compose ps --format json`` reports a ``Publishers`` list per
|
|
228
|
+
service; an entry with a non-zero ``PublishedPort`` is a port bound on
|
|
229
|
+
the host, which is what a smoke test can reach. Services whose name
|
|
230
|
+
contains ``service_hint`` are preferred, so a database that happens to
|
|
231
|
+
publish a port is never mistaken for the application.
|
|
232
|
+
"""
|
|
233
|
+
services = self._services(timeout=STATUS_TIMEOUT)
|
|
234
|
+
if not services:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def _matches(service: Dict[str, Any]) -> bool:
|
|
238
|
+
name = f"{service.get('Service', '')} {service.get('Name', '')}"
|
|
239
|
+
return service_hint in name
|
|
240
|
+
|
|
241
|
+
for candidate in (
|
|
242
|
+
[entry for entry in services if _matches(entry)],
|
|
243
|
+
services,
|
|
244
|
+
):
|
|
245
|
+
for service in candidate:
|
|
246
|
+
port = self._first_published_port(service)
|
|
247
|
+
if port is not None:
|
|
248
|
+
return port
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _first_published_port(service: Dict[str, Any]) -> Optional[int]:
|
|
253
|
+
"""Extract the first host-bound port from one ``ps`` entry."""
|
|
254
|
+
publishers = service.get("Publishers")
|
|
255
|
+
if not isinstance(publishers, list):
|
|
256
|
+
return None
|
|
257
|
+
for publisher in publishers:
|
|
258
|
+
if not isinstance(publisher, dict):
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
port = int(publisher.get("PublishedPort", 0))
|
|
262
|
+
except (TypeError, ValueError):
|
|
263
|
+
continue
|
|
264
|
+
if port > 0:
|
|
265
|
+
return port
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def exec_tests(
|
|
269
|
+
self, use_test_script: bool
|
|
270
|
+
) -> Optional[subprocess.CompletedProcess[str]]:
|
|
271
|
+
"""Run the template's tests inside the app container."""
|
|
272
|
+
if use_test_script:
|
|
273
|
+
command = ["exec", "-T", "app", "bash", "scripts/test.sh"]
|
|
274
|
+
else:
|
|
275
|
+
command = ["exec", "-T", "app", "python", "-m", "pytest", "tests/", "-v"]
|
|
276
|
+
return self._compose(command, timeout=TEST_TIMEOUT)
|
|
277
|
+
|
|
278
|
+
def reclaim_bind_mount_ownership(self) -> None:
|
|
279
|
+
"""Give the bind-mounted project directory back to the host user.
|
|
280
|
+
|
|
281
|
+
Containers run as root, so a test run inside the stack leaves
|
|
282
|
+
root-owned artefacts (``__pycache__``, ``.pytest_cache``, ...) in the
|
|
283
|
+
mounted project. Later host-side steps and the temp directory cleanup
|
|
284
|
+
cannot touch those and fail with a PermissionError, so the ownership
|
|
285
|
+
is reset from inside a throwaway container. Best effort: a failure
|
|
286
|
+
here is never worth failing an inspection over.
|
|
287
|
+
"""
|
|
288
|
+
if not hasattr(os, "getuid"): # pragma: no cover - Windows hosts
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
uid, gid = os.getuid(), os.getgid()
|
|
292
|
+
if uid == 0:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
debug_log("Reclaiming ownership of bind-mounted project files", "info")
|
|
296
|
+
self._run(
|
|
297
|
+
[
|
|
298
|
+
"docker",
|
|
299
|
+
"run",
|
|
300
|
+
"--rm",
|
|
301
|
+
"-v",
|
|
302
|
+
f"{self.project_dir}:/mnt",
|
|
303
|
+
RECLAIM_IMAGE,
|
|
304
|
+
"chown",
|
|
305
|
+
"-R",
|
|
306
|
+
f"{uid}:{gid}",
|
|
307
|
+
"/mnt",
|
|
308
|
+
],
|
|
309
|
+
timeout=CLEANUP_TIMEOUT,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def cleanup(self) -> None:
|
|
313
|
+
"""Tear down services and volumes, ignoring any failure."""
|
|
314
|
+
debug_log("Cleaning up Docker services", "info")
|
|
315
|
+
self.reclaim_bind_mount_ownership()
|
|
316
|
+
self._run(
|
|
317
|
+
[*self.compose_command(), "down", "-v", "--remove-orphans"],
|
|
318
|
+
timeout=CLEANUP_TIMEOUT,
|
|
319
|
+
)
|
|
320
|
+
self._run(["docker", "system", "prune", "-f"], timeout=STATUS_TIMEOUT)
|