FastAPI-fastkit 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_fastkit/__init__.py +1 -1
- fastapi_fastkit/backend/inspection/__init__.py +26 -0
- fastapi_fastkit/backend/inspection/checks.py +349 -0
- fastapi_fastkit/backend/inspection/consistency.py +125 -0
- fastapi_fastkit/backend/inspection/context.py +67 -0
- fastapi_fastkit/backend/inspection/core.py +347 -0
- fastapi_fastkit/backend/inspection/docker.py +185 -0
- fastapi_fastkit/backend/inspection/freshness.py +145 -0
- fastapi_fastkit/backend/inspection/fsutils.py +139 -0
- fastapi_fastkit/backend/inspection/lint.py +89 -0
- fastapi_fastkit/backend/inspection/report.py +58 -0
- fastapi_fastkit/backend/inspection/smoke.py +327 -0
- fastapi_fastkit/backend/inspection/strategies.py +336 -0
- fastapi_fastkit/backend/inspector.py +18 -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 +54 -21
- fastapi_fastkit/cli.py +434 -311
- fastapi_fastkit/core/settings.py +232 -43
- 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/README.md-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +25 -25
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +68 -39
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +3 -4
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/router.py-tpl +3 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +6 -7
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +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.0.dist-info}/METADATA +26 -2
- fastapi_fastkit-1.4.0.dist-info/RECORD +484 -0
- {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.0.dist-info}/WHEEL +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +0 -25
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +0 -27
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +0 -51
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +0 -25
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/setup.py-tpl +0 -17
- fastapi_fastkit-1.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.0.dist-info}/entry_points.txt +0 -0
- {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1410 +1,29 @@
|
|
|
1
1
|
# --------------------------------------------------------------------------
|
|
2
|
-
#
|
|
3
|
-
# This module will be used by maintainers of FastAPI-fastkit when anyone
|
|
4
|
-
# makes a PR of adding new FastAPI template.
|
|
2
|
+
# Backwards-compatible facade for the template inspector.
|
|
5
3
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# Third, check a FastAPI template has passed all the tests.
|
|
11
|
-
#
|
|
12
|
-
# This module create temporary named 'temp' directory at src/fastapi_fastkit/backend
|
|
13
|
-
# and copy a template to Funtional FastAPI application into the temp directory.
|
|
14
|
-
# After the inspection, it will be deleted.
|
|
15
|
-
#
|
|
16
|
-
# This module include virtual environment creation & installation of dependencies.
|
|
17
|
-
# Depending on the volume in which the template is implemented and the number of dependencies,
|
|
18
|
-
# it may take some time to complete the inspection.
|
|
4
|
+
# The implementation lives in ``fastapi_fastkit.backend.inspection``; this
|
|
5
|
+
# module keeps the historical import path (`from fastapi_fastkit.backend.
|
|
6
|
+
# inspector import inspect_fastapi_template`) working and doubles as the
|
|
7
|
+
# command line entry point.
|
|
19
8
|
#
|
|
20
9
|
# @author bnbong
|
|
21
10
|
# --------------------------------------------------------------------------
|
|
22
|
-
import os
|
|
23
|
-
import shutil
|
|
24
|
-
import subprocess
|
|
25
11
|
import sys
|
|
26
|
-
import tomllib
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
29
|
-
|
|
30
|
-
import yaml
|
|
31
12
|
|
|
32
|
-
from fastapi_fastkit.backend.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
install_dependencies_with_manager,
|
|
13
|
+
from fastapi_fastkit.backend.inspection import (
|
|
14
|
+
InspectionContext,
|
|
15
|
+
InspectionOptions,
|
|
16
|
+
TemplateInspector,
|
|
17
|
+
inspect_fastapi_template,
|
|
38
18
|
)
|
|
39
|
-
from fastapi_fastkit.
|
|
40
|
-
_parse_pip_requirement,
|
|
41
|
-
)
|
|
42
|
-
from fastapi_fastkit.backend.transducer import copy_and_convert_template
|
|
43
|
-
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
44
|
-
from fastapi_fastkit.utils.main import print_error, print_success, print_warning
|
|
45
|
-
|
|
46
|
-
logger = get_logger(__name__)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class TemplateInspector:
|
|
50
|
-
"""
|
|
51
|
-
Template inspector for validating FastAPI templates.
|
|
52
|
-
|
|
53
|
-
Uses context manager protocol for proper resource cleanup.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(self, template_path: str, temp_base_dir: Optional[str] = None):
|
|
57
|
-
self.template_path = Path(template_path)
|
|
58
|
-
self.errors: List[str] = []
|
|
59
|
-
self.warnings: List[str] = []
|
|
60
|
-
template_name = Path(template_path).name
|
|
61
|
-
|
|
62
|
-
# use temp_base_dir or fall back to backend directory (temp)
|
|
63
|
-
if temp_base_dir:
|
|
64
|
-
self.temp_dir = os.path.join(temp_base_dir, f"temp_{template_name}")
|
|
65
|
-
else:
|
|
66
|
-
self.temp_dir = os.path.join(
|
|
67
|
-
os.path.dirname(__file__), f"temp_{template_name}"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
self._cleanup_needed = False
|
|
71
|
-
self.template_config: Optional[Dict[str, Any]] = None
|
|
72
|
-
|
|
73
|
-
def __enter__(self) -> "TemplateInspector":
|
|
74
|
-
"""Enter context manager - create temp directory and copy template."""
|
|
75
|
-
try:
|
|
76
|
-
# Clean up any existing temp directory for this template
|
|
77
|
-
if os.path.exists(self.temp_dir):
|
|
78
|
-
debug_log(
|
|
79
|
-
f"Cleaning up existing temp directory: {self.temp_dir}", "info"
|
|
80
|
-
)
|
|
81
|
-
try:
|
|
82
|
-
shutil.rmtree(self.temp_dir)
|
|
83
|
-
except OSError as e:
|
|
84
|
-
debug_log(
|
|
85
|
-
f"Failed to cleanup existing temp directory: {e}", "warning"
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
os.makedirs(self.temp_dir, exist_ok=True)
|
|
89
|
-
copy_and_convert_template(str(self.template_path), self.temp_dir)
|
|
90
|
-
|
|
91
|
-
# Inject dummy metadata for inspection
|
|
92
|
-
self._inject_dummy_metadata()
|
|
93
|
-
|
|
94
|
-
self._cleanup_needed = True
|
|
95
|
-
self.template_config = self._load_template_config()
|
|
96
|
-
debug_log(f"Created temporary directory at {self.temp_dir}", "debug")
|
|
97
|
-
return self
|
|
98
|
-
except Exception as e:
|
|
99
|
-
debug_log(f"Failed to setup template inspector: {e}", "error")
|
|
100
|
-
self._cleanup()
|
|
101
|
-
raise
|
|
102
|
-
|
|
103
|
-
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
104
|
-
"""Exit context manager - cleanup temp directory."""
|
|
105
|
-
self._cleanup()
|
|
106
|
-
|
|
107
|
-
def _cleanup(self) -> None:
|
|
108
|
-
"""Cleanup temp directory."""
|
|
109
|
-
if self._cleanup_needed and os.path.exists(self.temp_dir):
|
|
110
|
-
temp_dir_path = self.temp_dir
|
|
111
|
-
try:
|
|
112
|
-
self._cleanup_docker_services()
|
|
113
|
-
|
|
114
|
-
import time
|
|
115
|
-
|
|
116
|
-
time.sleep(3)
|
|
117
|
-
|
|
118
|
-
self._force_cleanup_directory(temp_dir_path)
|
|
119
|
-
|
|
120
|
-
except Exception as e:
|
|
121
|
-
debug_log(
|
|
122
|
-
f"Warning: Unexpected error during cleanup of {temp_dir_path}: {e}",
|
|
123
|
-
"warning",
|
|
124
|
-
)
|
|
125
|
-
try:
|
|
126
|
-
self._force_cleanup_directory(temp_dir_path)
|
|
127
|
-
except Exception:
|
|
128
|
-
pass
|
|
129
|
-
finally:
|
|
130
|
-
self._cleanup_needed = False
|
|
131
|
-
|
|
132
|
-
def _force_cleanup_directory(self, directory_path: str) -> None:
|
|
133
|
-
"""Force cleanup of directory with multiple strategies."""
|
|
134
|
-
import time
|
|
135
|
-
|
|
136
|
-
max_retries = 5
|
|
137
|
-
for attempt in range(max_retries):
|
|
138
|
-
try:
|
|
139
|
-
shutil.rmtree(directory_path)
|
|
140
|
-
debug_log(
|
|
141
|
-
f"Successfully cleaned up temp directory: {directory_path}", "info"
|
|
142
|
-
)
|
|
143
|
-
return
|
|
144
|
-
except (OSError, PermissionError) as e:
|
|
145
|
-
if attempt < max_retries - 1:
|
|
146
|
-
debug_log(
|
|
147
|
-
f"Attempt {attempt + 1} failed to cleanup {directory_path}: {e}",
|
|
148
|
-
"warning",
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
try:
|
|
152
|
-
self._fix_directory_permissions(directory_path)
|
|
153
|
-
shutil.rmtree(directory_path)
|
|
154
|
-
debug_log(
|
|
155
|
-
f"Successfully cleaned up temp directory after permission fix: {directory_path}",
|
|
156
|
-
"info",
|
|
157
|
-
)
|
|
158
|
-
return
|
|
159
|
-
except Exception:
|
|
160
|
-
pass
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
self._remove_directory_contents(directory_path)
|
|
164
|
-
os.rmdir(directory_path)
|
|
165
|
-
debug_log(
|
|
166
|
-
f"Successfully cleaned up temp directory by removing contents: {directory_path}",
|
|
167
|
-
"info",
|
|
168
|
-
)
|
|
169
|
-
return
|
|
170
|
-
except Exception:
|
|
171
|
-
pass
|
|
172
|
-
|
|
173
|
-
time.sleep(2)
|
|
174
|
-
else:
|
|
175
|
-
debug_log(
|
|
176
|
-
f"Failed to cleanup temp directory after {max_retries} attempts: {directory_path}",
|
|
177
|
-
"warning",
|
|
178
|
-
)
|
|
179
|
-
try:
|
|
180
|
-
import subprocess
|
|
181
|
-
|
|
182
|
-
if os.name == "nt": # Windows
|
|
183
|
-
subprocess.run(
|
|
184
|
-
["rmdir", "/s", "/q", directory_path],
|
|
185
|
-
check=False,
|
|
186
|
-
shell=True,
|
|
187
|
-
)
|
|
188
|
-
else: # Unix-like
|
|
189
|
-
subprocess.run(["rm", "-rf", directory_path], check=False)
|
|
190
|
-
debug_log(
|
|
191
|
-
f"Force cleanup completed for: {directory_path}", "info"
|
|
192
|
-
)
|
|
193
|
-
except Exception:
|
|
194
|
-
debug_log(
|
|
195
|
-
f"All cleanup attempts failed for: {directory_path}",
|
|
196
|
-
"warning",
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
def _fix_directory_permissions(self, directory_path: str) -> None:
|
|
200
|
-
"""Fix directory permissions to allow removal."""
|
|
201
|
-
import os
|
|
202
|
-
import stat
|
|
203
|
-
|
|
204
|
-
def fix_permissions(path: str) -> None:
|
|
205
|
-
try:
|
|
206
|
-
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
207
|
-
except Exception:
|
|
208
|
-
pass
|
|
209
|
-
|
|
210
|
-
# Fix permissions for directory and all contents
|
|
211
|
-
fix_permissions(directory_path)
|
|
212
|
-
for root, dirs, files in os.walk(directory_path):
|
|
213
|
-
fix_permissions(root)
|
|
214
|
-
for d in dirs:
|
|
215
|
-
fix_permissions(os.path.join(root, d))
|
|
216
|
-
for f in files:
|
|
217
|
-
fix_permissions(os.path.join(root, f))
|
|
218
|
-
|
|
219
|
-
def _remove_directory_contents(self, directory_path: str) -> None:
|
|
220
|
-
"""Remove directory contents file by file."""
|
|
221
|
-
import os
|
|
222
|
-
import stat
|
|
223
|
-
|
|
224
|
-
for root, dirs, files in os.walk(directory_path, topdown=False):
|
|
225
|
-
# Remove files first
|
|
226
|
-
for file in files:
|
|
227
|
-
file_path = os.path.join(root, file)
|
|
228
|
-
try:
|
|
229
|
-
os.chmod(file_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
230
|
-
os.remove(file_path)
|
|
231
|
-
except Exception:
|
|
232
|
-
pass
|
|
233
|
-
|
|
234
|
-
# Remove directories
|
|
235
|
-
for dir_name in dirs:
|
|
236
|
-
dir_path = os.path.join(root, dir_name)
|
|
237
|
-
try:
|
|
238
|
-
os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
239
|
-
os.rmdir(dir_path)
|
|
240
|
-
except Exception:
|
|
241
|
-
pass
|
|
242
|
-
|
|
243
|
-
def _load_template_config(self) -> Optional[Dict[str, Any]]:
|
|
244
|
-
"""Load template configuration from template-config.yml if available."""
|
|
245
|
-
config_file = os.path.join(self.temp_dir, "template-config.yml")
|
|
246
|
-
if not os.path.exists(config_file):
|
|
247
|
-
debug_log(
|
|
248
|
-
"No template-config.yml found, using default testing strategy", "info"
|
|
249
|
-
)
|
|
250
|
-
return None
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
with open(config_file, "r", encoding="utf-8") as f:
|
|
254
|
-
config = yaml.safe_load(f)
|
|
255
|
-
if isinstance(config, dict):
|
|
256
|
-
debug_log(
|
|
257
|
-
f"Loaded template configuration: {config.get('name', 'Unknown')}",
|
|
258
|
-
"info",
|
|
259
|
-
)
|
|
260
|
-
return config
|
|
261
|
-
else:
|
|
262
|
-
debug_log("Invalid template configuration format", "warning")
|
|
263
|
-
return None
|
|
264
|
-
except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
|
|
265
|
-
debug_log(f"Failed to load template configuration: {e}", "warning")
|
|
266
|
-
return None
|
|
267
|
-
|
|
268
|
-
def _inject_dummy_metadata(self) -> None:
|
|
269
|
-
"""Inject dummy metadata for template inspection."""
|
|
270
|
-
try:
|
|
271
|
-
# Use dummy metadata for inspection
|
|
272
|
-
dummy_metadata = {
|
|
273
|
-
"project_name": "test-template",
|
|
274
|
-
"author": "Template Inspector",
|
|
275
|
-
"author_email": "inspector@fastapi-fastkit.dev",
|
|
276
|
-
"description": "Test project for template inspection",
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
inject_project_metadata(
|
|
280
|
-
self.temp_dir,
|
|
281
|
-
dummy_metadata["project_name"],
|
|
282
|
-
dummy_metadata["author"],
|
|
283
|
-
dummy_metadata["author_email"],
|
|
284
|
-
dummy_metadata["description"],
|
|
285
|
-
)
|
|
286
|
-
debug_log("Injected dummy metadata for template inspection", "info")
|
|
287
|
-
|
|
288
|
-
except Exception as e:
|
|
289
|
-
debug_log(f"Failed to inject dummy metadata: {e}", "warning")
|
|
290
|
-
self.warnings.append(f"Failed to inject metadata: {str(e)}")
|
|
291
|
-
|
|
292
|
-
def _detect_package_manager(self) -> str:
|
|
293
|
-
"""Detect the appropriate package manager for the template."""
|
|
294
|
-
# Check for pyproject.toml (modern Python packaging)
|
|
295
|
-
if os.path.exists(os.path.join(self.temp_dir, "pyproject.toml")):
|
|
296
|
-
return "uv" # Use UV for pyproject.toml based projects
|
|
297
|
-
|
|
298
|
-
# Check for requirements.txt (traditional pip)
|
|
299
|
-
if os.path.exists(os.path.join(self.temp_dir, "requirements.txt")):
|
|
300
|
-
return "pip"
|
|
301
|
-
|
|
302
|
-
# Default to pip if no dependency file found
|
|
303
|
-
return "pip"
|
|
304
|
-
|
|
305
|
-
def _check_docker_available(self) -> bool:
|
|
306
|
-
"""Check if Docker and Docker Compose are available."""
|
|
307
|
-
try:
|
|
308
|
-
# Check Docker
|
|
309
|
-
result = subprocess.run(
|
|
310
|
-
["docker", "--version"], capture_output=True, text=True, timeout=10
|
|
311
|
-
)
|
|
312
|
-
if result.returncode != 0:
|
|
313
|
-
return False
|
|
314
|
-
|
|
315
|
-
# Check Docker Compose
|
|
316
|
-
result = subprocess.run(
|
|
317
|
-
["docker-compose", "--version"],
|
|
318
|
-
capture_output=True,
|
|
319
|
-
text=True,
|
|
320
|
-
timeout=10,
|
|
321
|
-
)
|
|
322
|
-
return result.returncode == 0
|
|
323
|
-
except (
|
|
324
|
-
subprocess.CalledProcessError,
|
|
325
|
-
subprocess.TimeoutExpired,
|
|
326
|
-
FileNotFoundError,
|
|
327
|
-
):
|
|
328
|
-
return False
|
|
329
|
-
|
|
330
|
-
def _check_containers_running(self, compose_file: str) -> bool:
|
|
331
|
-
"""Check if Docker Compose containers are already running."""
|
|
332
|
-
try:
|
|
333
|
-
result = subprocess.run(
|
|
334
|
-
["docker-compose", "-f", compose_file, "ps", "-q"],
|
|
335
|
-
cwd=self.temp_dir,
|
|
336
|
-
capture_output=True,
|
|
337
|
-
text=True,
|
|
338
|
-
timeout=30,
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
if result.returncode != 0:
|
|
342
|
-
return False
|
|
343
|
-
|
|
344
|
-
# Check if any containers are running
|
|
345
|
-
container_ids = result.stdout.strip().split("\n")
|
|
346
|
-
if not container_ids or container_ids == [""]:
|
|
347
|
-
return False
|
|
348
|
-
|
|
349
|
-
# Check if containers are actually running (not just exist)
|
|
350
|
-
for container_id in container_ids:
|
|
351
|
-
if container_id.strip():
|
|
352
|
-
status_result = subprocess.run(
|
|
353
|
-
[
|
|
354
|
-
"docker",
|
|
355
|
-
"inspect",
|
|
356
|
-
"-f",
|
|
357
|
-
"{{.State.Running}}",
|
|
358
|
-
container_id.strip(),
|
|
359
|
-
],
|
|
360
|
-
capture_output=True,
|
|
361
|
-
text=True,
|
|
362
|
-
timeout=10,
|
|
363
|
-
)
|
|
364
|
-
if (
|
|
365
|
-
status_result.returncode != 0
|
|
366
|
-
or status_result.stdout.strip() != "true"
|
|
367
|
-
):
|
|
368
|
-
return False
|
|
369
|
-
|
|
370
|
-
return True
|
|
371
|
-
except (
|
|
372
|
-
subprocess.CalledProcessError,
|
|
373
|
-
subprocess.TimeoutExpired,
|
|
374
|
-
FileNotFoundError,
|
|
375
|
-
):
|
|
376
|
-
return False
|
|
377
|
-
|
|
378
|
-
def inspect_template(self) -> bool:
|
|
379
|
-
"""
|
|
380
|
-
Inspect the template to validate it's a proper FastAPI application.
|
|
381
|
-
|
|
382
|
-
:return: True if template is valid, False otherwise
|
|
383
|
-
"""
|
|
384
|
-
checks: List[Tuple[str, Callable[[], bool]]] = [
|
|
385
|
-
("File Structure", self._check_file_structure),
|
|
386
|
-
("File Extensions", self._check_file_extensions),
|
|
387
|
-
("Dependencies", self._check_dependencies),
|
|
388
|
-
("FastAPI Implementation", self._check_fastapi_implementation),
|
|
389
|
-
("Template Tests", self._test_template),
|
|
390
|
-
]
|
|
391
|
-
|
|
392
|
-
for check_name, check_func in checks:
|
|
393
|
-
debug_log(f"Running check: {check_name}", "info")
|
|
394
|
-
if not check_func():
|
|
395
|
-
debug_log(f"Check failed: {check_name}", "error")
|
|
396
|
-
return False
|
|
397
|
-
debug_log(f"Check passed: {check_name}", "info")
|
|
398
|
-
|
|
399
|
-
return True
|
|
400
|
-
|
|
401
|
-
def _check_file_structure(self) -> bool:
|
|
402
|
-
"""Check the required file and directory structure.
|
|
403
|
-
|
|
404
|
-
Modern templates may ship ``pyproject.toml-tpl`` as the primary metadata
|
|
405
|
-
file; ``setup.py-tpl`` remains accepted for backward compatibility.
|
|
406
|
-
A template must provide at least one of the two. ``requirements.txt-tpl``
|
|
407
|
-
is no longer strictly required when ``pyproject.toml-tpl`` declares
|
|
408
|
-
``[project].dependencies``.
|
|
409
|
-
"""
|
|
410
|
-
always_required = ["tests", "README.md-tpl"]
|
|
411
|
-
metadata_candidates = ["pyproject.toml-tpl", "setup.py-tpl"]
|
|
412
|
-
|
|
413
|
-
missing_required = [
|
|
414
|
-
path for path in always_required if not (self.template_path / path).exists()
|
|
415
|
-
]
|
|
416
|
-
has_metadata = any(
|
|
417
|
-
(self.template_path / name).exists() for name in metadata_candidates
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
if missing_required:
|
|
421
|
-
for path in missing_required:
|
|
422
|
-
error_msg = f"Missing required path: {path}"
|
|
423
|
-
self.errors.append(error_msg)
|
|
424
|
-
debug_log(f"File structure check failed: {error_msg}", "error")
|
|
425
|
-
if not has_metadata:
|
|
426
|
-
error_msg = (
|
|
427
|
-
"Missing metadata file: expected pyproject.toml-tpl "
|
|
428
|
-
"(preferred) or setup.py-tpl"
|
|
429
|
-
)
|
|
430
|
-
self.errors.append(error_msg)
|
|
431
|
-
debug_log(f"File structure check failed: {error_msg}", "error")
|
|
432
|
-
|
|
433
|
-
if missing_required or not has_metadata:
|
|
434
|
-
return False
|
|
435
|
-
|
|
436
|
-
debug_log("File structure check passed", "info")
|
|
437
|
-
return True
|
|
438
|
-
|
|
439
|
-
def _check_file_extensions(self) -> bool:
|
|
440
|
-
"""Check all Python files have .py-tpl extension."""
|
|
441
|
-
for path in self.template_path.rglob("*"):
|
|
442
|
-
if path.is_file() and path.suffix == ".py":
|
|
443
|
-
error_msg = f"Found .py file instead of .py-tpl: {path}"
|
|
444
|
-
self.errors.append(error_msg)
|
|
445
|
-
debug_log(f"File extension check failed: {error_msg}", "error")
|
|
446
|
-
return False
|
|
447
|
-
|
|
448
|
-
debug_log("File extension check passed", "info")
|
|
449
|
-
return True
|
|
450
|
-
|
|
451
|
-
def _check_dependencies(self) -> bool:
|
|
452
|
-
"""Check that FastAPI is declared in at least one supported source.
|
|
453
|
-
|
|
454
|
-
All three metadata sources are consulted independently:
|
|
455
|
-
|
|
456
|
-
- ``requirements.txt-tpl``
|
|
457
|
-
- ``pyproject.toml-tpl`` ``[project].dependencies``
|
|
458
|
-
- ``setup.py-tpl`` ``install_requires``
|
|
459
|
-
|
|
460
|
-
The check passes if *any* source declares ``fastapi``, so a template
|
|
461
|
-
with a stale ``requirements.txt-tpl`` still passes when
|
|
462
|
-
``pyproject.toml-tpl`` is authoritative. It fails only when every
|
|
463
|
-
present source is missing ``fastapi`` (or when no source exists at
|
|
464
|
-
all — that also implies no metadata file, which
|
|
465
|
-
``_check_file_structure`` already rejects).
|
|
466
|
-
"""
|
|
467
|
-
req_path = self.template_path / "requirements.txt-tpl"
|
|
468
|
-
pyproject_path = self.template_path / "pyproject.toml-tpl"
|
|
469
|
-
setup_path = self.template_path / "setup.py-tpl"
|
|
470
|
-
|
|
471
|
-
sources_checked: List[str] = []
|
|
472
|
-
fastapi_declared = False
|
|
473
|
-
|
|
474
|
-
if req_path.exists():
|
|
475
|
-
sources_checked.append("requirements.txt-tpl")
|
|
476
|
-
try:
|
|
477
|
-
with open(req_path, encoding="utf-8") as f:
|
|
478
|
-
deps = f.read().splitlines()
|
|
479
|
-
package_names = {
|
|
480
|
-
_parse_pip_requirement(dep)[0].lower()
|
|
481
|
-
for dep in deps
|
|
482
|
-
if dep.strip() and not dep.strip().startswith("#")
|
|
483
|
-
}
|
|
484
|
-
debug_log(
|
|
485
|
-
f"requirements.txt-tpl dependencies: {sorted(package_names)}",
|
|
486
|
-
"debug",
|
|
487
|
-
)
|
|
488
|
-
if "fastapi" in package_names:
|
|
489
|
-
fastapi_declared = True
|
|
490
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
491
|
-
error_msg = f"Error reading requirements.txt-tpl: {e}"
|
|
492
|
-
self.errors.append(error_msg)
|
|
493
|
-
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
494
|
-
return False
|
|
495
|
-
|
|
496
|
-
if pyproject_path.exists():
|
|
497
|
-
sources_checked.append("pyproject.toml-tpl")
|
|
498
|
-
package_names, parse_error = self._extract_pyproject_dependency_names(
|
|
499
|
-
pyproject_path
|
|
500
|
-
)
|
|
501
|
-
if parse_error is not None:
|
|
502
|
-
self.errors.append(parse_error)
|
|
503
|
-
debug_log(f"Dependencies check failed: {parse_error}", "error")
|
|
504
|
-
return False
|
|
505
|
-
debug_log(
|
|
506
|
-
f"pyproject.toml-tpl dependencies: {sorted(package_names)}", "debug"
|
|
507
|
-
)
|
|
508
|
-
if "fastapi" in package_names:
|
|
509
|
-
fastapi_declared = True
|
|
510
|
-
|
|
511
|
-
if setup_path.exists():
|
|
512
|
-
sources_checked.append("setup.py-tpl")
|
|
513
|
-
try:
|
|
514
|
-
with open(setup_path, encoding="utf-8") as f:
|
|
515
|
-
content = f.read()
|
|
516
|
-
deps = _parse_setup_dependencies(content)
|
|
517
|
-
package_names = {
|
|
518
|
-
_parse_pip_requirement(dep)[0].lower() for dep in deps if dep
|
|
519
|
-
}
|
|
520
|
-
debug_log(
|
|
521
|
-
f"setup.py-tpl dependencies: {sorted(package_names)}", "debug"
|
|
522
|
-
)
|
|
523
|
-
if "fastapi" in package_names:
|
|
524
|
-
fastapi_declared = True
|
|
525
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
526
|
-
error_msg = f"Error reading setup.py-tpl: {e}"
|
|
527
|
-
self.errors.append(error_msg)
|
|
528
|
-
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
529
|
-
return False
|
|
530
|
-
|
|
531
|
-
if not sources_checked:
|
|
532
|
-
error_msg = (
|
|
533
|
-
"No dependency source found: expected one of requirements.txt-tpl, "
|
|
534
|
-
"pyproject.toml-tpl, or setup.py-tpl"
|
|
535
|
-
)
|
|
536
|
-
self.errors.append(error_msg)
|
|
537
|
-
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
538
|
-
return False
|
|
539
|
-
|
|
540
|
-
if not fastapi_declared:
|
|
541
|
-
error_msg = (
|
|
542
|
-
"FastAPI dependency not found in any source ("
|
|
543
|
-
+ ", ".join(sources_checked)
|
|
544
|
-
+ ")"
|
|
545
|
-
)
|
|
546
|
-
self.errors.append(error_msg)
|
|
547
|
-
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
548
|
-
return False
|
|
549
|
-
|
|
550
|
-
debug_log(
|
|
551
|
-
f"Dependencies check passed (sources: {', '.join(sources_checked)})",
|
|
552
|
-
"info",
|
|
553
|
-
)
|
|
554
|
-
return True
|
|
555
|
-
|
|
556
|
-
@staticmethod
|
|
557
|
-
def _extract_pyproject_dependency_names(
|
|
558
|
-
pyproject_path: Path,
|
|
559
|
-
) -> Tuple[set, Optional[str]]: # type: ignore
|
|
560
|
-
"""Parse a pyproject.toml-tpl and return (lowercased dep names, error).
|
|
561
|
-
|
|
562
|
-
``error`` is ``None`` on success. Returns an empty set with a descriptive
|
|
563
|
-
error string when the file is unreadable or malformed.
|
|
564
|
-
"""
|
|
565
|
-
try:
|
|
566
|
-
with open(pyproject_path, "rb") as f:
|
|
567
|
-
data = tomllib.load(f)
|
|
568
|
-
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
|
|
569
|
-
return set(), f"Invalid pyproject.toml-tpl: {e}"
|
|
570
|
-
|
|
571
|
-
project = data.get("project", {})
|
|
572
|
-
raw_deps = project.get("dependencies", []) or []
|
|
573
|
-
if not isinstance(raw_deps, list):
|
|
574
|
-
return set(), "pyproject.toml-tpl [project].dependencies must be a list"
|
|
575
|
-
|
|
576
|
-
names: set = set() # type: ignore
|
|
577
|
-
for dep in raw_deps:
|
|
578
|
-
if not isinstance(dep, str) or not dep.strip():
|
|
579
|
-
continue
|
|
580
|
-
name = _parse_pip_requirement(dep)[0]
|
|
581
|
-
if name:
|
|
582
|
-
names.add(name.lower())
|
|
583
|
-
return names, None
|
|
584
|
-
|
|
585
|
-
def _check_fastapi_implementation(self) -> bool:
|
|
586
|
-
"""Check if the template has a proper FastAPI server implementation."""
|
|
587
|
-
try:
|
|
588
|
-
core_modules = find_template_core_modules(self.temp_dir)
|
|
589
|
-
debug_log(f"Found core modules: {core_modules}", "debug")
|
|
590
|
-
|
|
591
|
-
if not core_modules["main"]:
|
|
592
|
-
error_msg = "main.py not found in template"
|
|
593
|
-
self.errors.append(error_msg)
|
|
594
|
-
debug_log(f"FastAPI implementation check failed: {error_msg}", "error")
|
|
595
|
-
return False
|
|
596
|
-
|
|
597
|
-
with open(core_modules["main"], encoding="utf-8") as f:
|
|
598
|
-
content = f.read()
|
|
599
|
-
if "FastAPI" not in content or "app" not in content:
|
|
600
|
-
error_msg = "FastAPI app creation not found in main.py"
|
|
601
|
-
self.errors.append(error_msg)
|
|
602
|
-
debug_log(
|
|
603
|
-
f"FastAPI implementation check failed: {error_msg}", "error"
|
|
604
|
-
)
|
|
605
|
-
debug_log(f"main.py content preview: {content[:200]}...", "debug")
|
|
606
|
-
return False
|
|
607
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
608
|
-
error_msg = f"Error checking FastAPI implementation: {e}"
|
|
609
|
-
self.errors.append(error_msg)
|
|
610
|
-
debug_log(f"FastAPI implementation check failed: {error_msg}", "error")
|
|
611
|
-
return False
|
|
612
|
-
|
|
613
|
-
debug_log("FastAPI implementation check passed", "info")
|
|
614
|
-
return True
|
|
615
|
-
|
|
616
|
-
def _test_template(self) -> bool:
|
|
617
|
-
"""Run tests on the template using appropriate strategy based on configuration."""
|
|
618
|
-
test_dir = os.path.join(self.temp_dir, "tests")
|
|
619
|
-
if not os.path.exists(test_dir):
|
|
620
|
-
warning_msg = "No tests directory found"
|
|
621
|
-
self.warnings.append(warning_msg)
|
|
622
|
-
debug_log(f"Template warning: {warning_msg}", "warning")
|
|
623
|
-
return True
|
|
624
|
-
|
|
625
|
-
# Determine test strategy based on template configuration
|
|
626
|
-
if self.template_config and self.template_config.get("requires_docker", False):
|
|
627
|
-
return self._test_with_docker_strategy()
|
|
628
|
-
else:
|
|
629
|
-
return self._test_with_standard_strategy()
|
|
630
|
-
|
|
631
|
-
def _test_with_docker_strategy(self) -> bool:
|
|
632
|
-
"""Run tests using Docker Compose strategy."""
|
|
633
|
-
docker_available = self._check_docker_available()
|
|
634
|
-
|
|
635
|
-
if not docker_available:
|
|
636
|
-
debug_log("Docker not available, trying fallback strategy", "warning")
|
|
637
|
-
warning_msg = "Docker not available, using fallback testing strategy"
|
|
638
|
-
self.warnings.append(warning_msg)
|
|
639
|
-
debug_log(f"Template warning: {warning_msg}", "warning")
|
|
640
|
-
return self._test_with_fallback_strategy()
|
|
641
|
-
|
|
642
|
-
try:
|
|
643
|
-
# Set up environment variables
|
|
644
|
-
self._setup_test_environment()
|
|
645
|
-
|
|
646
|
-
# Run Docker Compose based tests
|
|
647
|
-
testing_config = (
|
|
648
|
-
self.template_config.get("testing", {}) if self.template_config else {}
|
|
649
|
-
)
|
|
650
|
-
compose_file = testing_config.get("compose_file", "docker-compose.yml")
|
|
651
|
-
timeout = testing_config.get("health_check_timeout", 120)
|
|
652
|
-
|
|
653
|
-
# Check if containers are already running
|
|
654
|
-
if not self._check_containers_running(compose_file):
|
|
655
|
-
debug_log("Starting Docker Compose services for testing", "info")
|
|
656
|
-
|
|
657
|
-
# Start services
|
|
658
|
-
result = subprocess.run(
|
|
659
|
-
["docker-compose", "-f", compose_file, "up", "-d", "--build"],
|
|
660
|
-
cwd=self.temp_dir,
|
|
661
|
-
capture_output=True,
|
|
662
|
-
text=True,
|
|
663
|
-
timeout=timeout,
|
|
664
|
-
)
|
|
665
|
-
|
|
666
|
-
if result.returncode != 0:
|
|
667
|
-
self.errors.append(
|
|
668
|
-
f"Failed to start Docker services: {result.stderr}"
|
|
669
|
-
)
|
|
670
|
-
return False
|
|
671
|
-
|
|
672
|
-
# Wait for services to be healthy
|
|
673
|
-
self._wait_for_services_healthy(compose_file, timeout)
|
|
674
|
-
else:
|
|
675
|
-
debug_log(
|
|
676
|
-
"Docker Compose services already running, skipping startup", "info"
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
# Verify services are actually running before attempting tests
|
|
680
|
-
if not self._verify_services_running(compose_file):
|
|
681
|
-
self.errors.append("Services failed to start properly")
|
|
682
|
-
return False
|
|
683
|
-
|
|
684
|
-
# Run tests using docker exec
|
|
685
|
-
test_result = self._run_docker_exec_tests(compose_file)
|
|
686
|
-
|
|
687
|
-
return test_result
|
|
688
|
-
|
|
689
|
-
except subprocess.TimeoutExpired:
|
|
690
|
-
self.errors.append("Docker Compose setup timed out")
|
|
691
|
-
return False
|
|
692
|
-
except Exception as e:
|
|
693
|
-
self.errors.append(f"Unexpected error during Docker testing: {e}")
|
|
694
|
-
return False
|
|
695
|
-
finally:
|
|
696
|
-
# Clean up Docker services
|
|
697
|
-
self._cleanup_docker_services()
|
|
698
|
-
|
|
699
|
-
def _test_with_standard_strategy(self) -> bool:
|
|
700
|
-
"""Run tests using standard virtual environment strategy."""
|
|
701
|
-
try:
|
|
702
|
-
# Create virtual environment for testing
|
|
703
|
-
venv_path = create_venv(self.temp_dir)
|
|
704
|
-
|
|
705
|
-
# Detect and use appropriate package manager
|
|
706
|
-
package_manager = self._detect_package_manager()
|
|
707
|
-
debug_log(f"Using package manager: {package_manager}", "info")
|
|
708
|
-
|
|
709
|
-
try:
|
|
710
|
-
install_dependencies_with_manager(
|
|
711
|
-
self.temp_dir, venv_path, package_manager
|
|
712
|
-
)
|
|
713
|
-
except Exception as dep_error:
|
|
714
|
-
# Capture detailed dependency installation error
|
|
715
|
-
error_msg = f"Failed to install dependencies: {str(dep_error)}"
|
|
716
|
-
debug_log(f"Dependency installation failed: {dep_error}", "error")
|
|
717
|
-
self.errors.append(error_msg)
|
|
718
|
-
return False
|
|
719
|
-
|
|
720
|
-
# Check if scripts/test.sh exists
|
|
721
|
-
test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
|
|
722
|
-
if os.path.exists(test_script_path):
|
|
723
|
-
debug_log("Found scripts/test.sh, using template test script", "info")
|
|
724
|
-
result = self._run_test_script(test_script_path, venv_path)
|
|
725
|
-
else:
|
|
726
|
-
debug_log("No scripts/test.sh found, running pytest directly", "info")
|
|
727
|
-
result = self._run_pytest_directly(venv_path)
|
|
728
|
-
|
|
729
|
-
if result.returncode != 0:
|
|
730
|
-
error_msg = f"Tests failed with return code {result.returncode}\n"
|
|
731
|
-
if result.stderr:
|
|
732
|
-
error_msg += f"STDERR:\n{result.stderr}\n"
|
|
733
|
-
if result.stdout:
|
|
734
|
-
error_msg += f"STDOUT:\n{result.stdout}\n"
|
|
735
|
-
self.errors.append(error_msg)
|
|
736
|
-
debug_log(f"Standard strategy tests failed: {error_msg}", "error")
|
|
737
|
-
return False
|
|
738
|
-
|
|
739
|
-
debug_log("All tests passed successfully", "info")
|
|
740
|
-
return True
|
|
741
|
-
|
|
742
|
-
except subprocess.TimeoutExpired:
|
|
743
|
-
self.errors.append("Tests timed out after 5 minutes")
|
|
744
|
-
return False
|
|
745
|
-
except subprocess.CalledProcessError as e:
|
|
746
|
-
self.errors.append(f"Error running tests: {e}")
|
|
747
|
-
return False
|
|
748
|
-
except Exception as e:
|
|
749
|
-
self.errors.append(f"Unexpected error during testing: {e}")
|
|
750
|
-
return False
|
|
751
|
-
|
|
752
|
-
def _fix_script_line_endings(self, script_path: str) -> None:
|
|
753
|
-
"""Fix line endings in script file (convert Windows \r\n to Unix \n)."""
|
|
754
|
-
try:
|
|
755
|
-
with open(script_path, "rb") as f:
|
|
756
|
-
content = f.read()
|
|
757
|
-
|
|
758
|
-
# Convert Windows line endings to Unix
|
|
759
|
-
content = content.replace(b"\r\n", b"\n")
|
|
760
|
-
content = content.replace(b"\r", b"\n")
|
|
761
|
-
|
|
762
|
-
with open(script_path, "wb") as f:
|
|
763
|
-
f.write(content)
|
|
764
|
-
|
|
765
|
-
debug_log(f"Fixed line endings in {script_path}", "debug")
|
|
766
|
-
except Exception as e:
|
|
767
|
-
debug_log(f"Failed to fix line endings in {script_path}: {e}", "warning")
|
|
768
|
-
|
|
769
|
-
def _fix_all_script_line_endings(self) -> None:
|
|
770
|
-
"""Fix line endings in all shell scripts in the temp directory."""
|
|
771
|
-
script_patterns = ["*.sh", "*.bash"]
|
|
772
|
-
|
|
773
|
-
for pattern in script_patterns:
|
|
774
|
-
for root, dirs, files in os.walk(self.temp_dir):
|
|
775
|
-
for file in files:
|
|
776
|
-
if file.endswith((".sh", ".bash")):
|
|
777
|
-
script_path = os.path.join(root, file)
|
|
778
|
-
self._fix_script_line_endings(script_path)
|
|
779
|
-
|
|
780
|
-
debug_log("Fixed line endings for all shell scripts", "info")
|
|
781
|
-
|
|
782
|
-
def _run_test_script(
|
|
783
|
-
self, test_script_path: str, venv_path: str
|
|
784
|
-
) -> subprocess.CompletedProcess[str]:
|
|
785
|
-
"""Run the template's test script."""
|
|
786
|
-
# Fix line endings (convert Windows \r\n to Unix \n)
|
|
787
|
-
self._fix_script_line_endings(test_script_path)
|
|
788
|
-
|
|
789
|
-
# Make script executable
|
|
790
|
-
os.chmod(test_script_path, 0o755)
|
|
791
|
-
|
|
792
|
-
# Set up environment to use virtual environment
|
|
793
|
-
env = os.environ.copy()
|
|
794
|
-
if os.name == "nt": # Windows
|
|
795
|
-
env["PATH"] = f"{os.path.join(venv_path, 'Scripts')};{env.get('PATH', '')}"
|
|
796
|
-
else: # Unix-based
|
|
797
|
-
env["PATH"] = f"{os.path.join(venv_path, 'bin')}:{env.get('PATH', '')}"
|
|
798
|
-
|
|
799
|
-
# Run the test script
|
|
800
|
-
return subprocess.run(
|
|
801
|
-
[test_script_path],
|
|
802
|
-
cwd=self.temp_dir,
|
|
803
|
-
capture_output=True,
|
|
804
|
-
text=True,
|
|
805
|
-
env=env,
|
|
806
|
-
timeout=300, # 5 minutes timeout
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
def _run_pytest_directly(self, venv_path: str) -> subprocess.CompletedProcess[str]:
|
|
810
|
-
"""Run pytest directly using virtual environment."""
|
|
811
|
-
if os.name == "nt": # Windows
|
|
812
|
-
python_executable = os.path.join(venv_path, "Scripts", "python")
|
|
813
|
-
else: # Unix-based
|
|
814
|
-
python_executable = os.path.join(venv_path, "bin", "python")
|
|
815
|
-
|
|
816
|
-
test_dir = os.path.join(self.temp_dir, "tests")
|
|
817
|
-
return subprocess.run(
|
|
818
|
-
[python_executable, "-m", "pytest", test_dir, "-v"],
|
|
819
|
-
cwd=self.temp_dir,
|
|
820
|
-
capture_output=True,
|
|
821
|
-
text=True,
|
|
822
|
-
timeout=300, # 5 minutes timeout
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
def _run_test_script_with_env(
|
|
826
|
-
self, test_script_path: str, venv_path: str, env: Dict[str, str]
|
|
827
|
-
) -> subprocess.CompletedProcess[str]:
|
|
828
|
-
"""Run the template's test script with custom environment variables."""
|
|
829
|
-
# Fix line endings (convert Windows \r\n to Unix \n)
|
|
830
|
-
self._fix_script_line_endings(test_script_path)
|
|
831
|
-
|
|
832
|
-
# Make script executable
|
|
833
|
-
os.chmod(test_script_path, 0o755)
|
|
834
|
-
|
|
835
|
-
# Set up environment to use virtual environment
|
|
836
|
-
env = env.copy()
|
|
837
|
-
if os.name == "nt": # Windows
|
|
838
|
-
env["PATH"] = f"{os.path.join(venv_path, 'Scripts')};{env.get('PATH', '')}"
|
|
839
|
-
else: # Unix-based
|
|
840
|
-
env["PATH"] = f"{os.path.join(venv_path, 'bin')}:{env.get('PATH', '')}"
|
|
841
|
-
|
|
842
|
-
# Run the test script
|
|
843
|
-
return subprocess.run(
|
|
844
|
-
[test_script_path],
|
|
845
|
-
cwd=self.temp_dir,
|
|
846
|
-
capture_output=True,
|
|
847
|
-
text=True,
|
|
848
|
-
env=env,
|
|
849
|
-
timeout=300, # 5 minutes timeout
|
|
850
|
-
)
|
|
851
|
-
|
|
852
|
-
def _run_pytest_with_env(
|
|
853
|
-
self, venv_path: str, env: Dict[str, str], fallback_config: Dict[str, Any]
|
|
854
|
-
) -> subprocess.CompletedProcess[str]:
|
|
855
|
-
"""Run pytest directly with custom environment variables."""
|
|
856
|
-
if os.name == "nt": # Windows
|
|
857
|
-
python_executable = os.path.join(venv_path, "Scripts", "python")
|
|
858
|
-
else: # Unix-based
|
|
859
|
-
python_executable = os.path.join(venv_path, "bin", "python")
|
|
860
|
-
|
|
861
|
-
test_command = fallback_config.get("test_command", "pytest tests/ -v").split()
|
|
862
|
-
return subprocess.run(
|
|
863
|
-
[python_executable, "-m"] + test_command,
|
|
864
|
-
cwd=self.temp_dir,
|
|
865
|
-
capture_output=True,
|
|
866
|
-
text=True,
|
|
867
|
-
env=env,
|
|
868
|
-
timeout=300,
|
|
869
|
-
)
|
|
870
|
-
|
|
871
|
-
def _test_with_fallback_strategy(self) -> bool:
|
|
872
|
-
"""Run tests using fallback strategy (e.g., SQLite instead of PostgreSQL)."""
|
|
873
|
-
if not self.template_config or "fallback_testing" not in self.template_config:
|
|
874
|
-
debug_log(
|
|
875
|
-
"No fallback strategy configured, using standard strategy", "info"
|
|
876
|
-
)
|
|
877
|
-
return self._test_with_standard_strategy()
|
|
878
|
-
|
|
879
|
-
try:
|
|
880
|
-
# Create virtual environment for testing
|
|
881
|
-
venv_path = create_venv(self.temp_dir)
|
|
882
|
-
|
|
883
|
-
# Detect and use appropriate package manager
|
|
884
|
-
package_manager = self._detect_package_manager()
|
|
885
|
-
debug_log(f"Using package manager: {package_manager}", "info")
|
|
886
|
-
|
|
887
|
-
try:
|
|
888
|
-
install_dependencies_with_manager(
|
|
889
|
-
self.temp_dir, venv_path, package_manager
|
|
890
|
-
)
|
|
891
|
-
except Exception as dep_error:
|
|
892
|
-
# Capture detailed dependency installation error
|
|
893
|
-
error_msg = f"Failed to install dependencies: {str(dep_error)}"
|
|
894
|
-
debug_log(f"Dependency installation failed: {dep_error}", "error")
|
|
895
|
-
self.errors.append(error_msg)
|
|
896
|
-
return False
|
|
897
|
-
|
|
898
|
-
# Set up fallback environment (e.g., SQLite database)
|
|
899
|
-
fallback_config = self.template_config["fallback_testing"]
|
|
900
|
-
database_url = fallback_config.get("database_url", "sqlite:///:memory:")
|
|
901
|
-
|
|
902
|
-
# Set environment variable for fallback database
|
|
903
|
-
env = os.environ.copy()
|
|
904
|
-
env["DATABASE_URL"] = database_url
|
|
905
|
-
env["SQLALCHEMY_DATABASE_URI"] = database_url
|
|
906
|
-
|
|
907
|
-
# Check if scripts/test.sh exists
|
|
908
|
-
test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
|
|
909
|
-
if os.path.exists(test_script_path):
|
|
910
|
-
debug_log(
|
|
911
|
-
"Found scripts/test.sh, using template test script with fallback environment",
|
|
912
|
-
"info",
|
|
913
|
-
)
|
|
914
|
-
result = self._run_test_script_with_env(
|
|
915
|
-
test_script_path, venv_path, env
|
|
916
|
-
)
|
|
917
|
-
else:
|
|
918
|
-
debug_log(
|
|
919
|
-
"No scripts/test.sh found, running pytest directly with fallback environment",
|
|
920
|
-
"info",
|
|
921
|
-
)
|
|
922
|
-
result = self._run_pytest_with_env(venv_path, env, fallback_config)
|
|
923
|
-
|
|
924
|
-
if result.returncode != 0:
|
|
925
|
-
error_msg = (
|
|
926
|
-
f"Fallback tests failed with return code {result.returncode}\n"
|
|
927
|
-
)
|
|
928
|
-
if result.stderr:
|
|
929
|
-
error_msg += f"STDERR:\n{result.stderr}\n"
|
|
930
|
-
if result.stdout:
|
|
931
|
-
error_msg += f"STDOUT:\n{result.stdout}\n"
|
|
932
|
-
self.errors.append(error_msg)
|
|
933
|
-
debug_log(f"Fallback strategy tests failed: {error_msg}", "error")
|
|
934
|
-
return False
|
|
935
|
-
|
|
936
|
-
debug_log("Fallback tests passed successfully", "info")
|
|
937
|
-
warning_msg = (
|
|
938
|
-
"Tests passed using fallback strategy (SQLite instead of PostgreSQL)"
|
|
939
|
-
)
|
|
940
|
-
self.warnings.append(warning_msg)
|
|
941
|
-
debug_log(f"Template warning: {warning_msg}", "warning")
|
|
942
|
-
return True
|
|
943
|
-
|
|
944
|
-
except Exception as e:
|
|
945
|
-
self.errors.append(f"Unexpected error during fallback testing: {e}")
|
|
946
|
-
return False
|
|
947
|
-
|
|
948
|
-
def _setup_test_environment(self) -> None:
|
|
949
|
-
"""Set up environment variables for testing."""
|
|
950
|
-
if not self.template_config:
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
env_defaults = self.template_config.get("test_env_defaults", {})
|
|
954
|
-
env_file_path = os.path.join(self.temp_dir, ".env")
|
|
955
|
-
|
|
956
|
-
# Check if .env file already exists
|
|
957
|
-
if os.path.exists(env_file_path):
|
|
958
|
-
debug_log(f".env file already exists at {env_file_path}", "info")
|
|
959
|
-
# Read existing content and merge with defaults
|
|
960
|
-
existing_vars = {}
|
|
961
|
-
try:
|
|
962
|
-
with open(env_file_path, "r", encoding="utf-8") as f:
|
|
963
|
-
for line in f:
|
|
964
|
-
line = line.strip()
|
|
965
|
-
if line and "=" in line and not line.startswith("#"):
|
|
966
|
-
key, value = line.split("=", 1)
|
|
967
|
-
existing_vars[key] = value
|
|
968
|
-
except Exception as e:
|
|
969
|
-
debug_log(f"Error reading existing .env file: {e}", "warning")
|
|
970
|
-
else:
|
|
971
|
-
existing_vars = {}
|
|
972
|
-
|
|
973
|
-
# Merge defaults with existing variables (defaults take precedence for missing vars)
|
|
974
|
-
final_vars = {**env_defaults, **existing_vars}
|
|
975
|
-
|
|
976
|
-
# Create .env file with merged variables
|
|
977
|
-
with open(env_file_path, "w", encoding="utf-8") as f:
|
|
978
|
-
for key, value in final_vars.items():
|
|
979
|
-
f.write(f"{key}={value}\n")
|
|
980
|
-
|
|
981
|
-
debug_log(
|
|
982
|
-
f"Set up environment file: {env_file_path} with variables: {list(final_vars.keys())}",
|
|
983
|
-
"info",
|
|
984
|
-
)
|
|
985
|
-
|
|
986
|
-
# Fix line endings in all shell scripts
|
|
987
|
-
self._fix_all_script_line_endings()
|
|
988
|
-
|
|
989
|
-
def _wait_for_services_healthy(self, compose_file: str, timeout: int) -> None:
|
|
990
|
-
"""Wait for Docker Compose services to be healthy."""
|
|
991
|
-
import time
|
|
992
|
-
|
|
993
|
-
debug_log("Waiting for services to be healthy...", "info")
|
|
994
|
-
start_time = time.time()
|
|
995
|
-
|
|
996
|
-
while time.time() - start_time < timeout:
|
|
997
|
-
try:
|
|
998
|
-
# Check if all services are running and healthy
|
|
999
|
-
result = subprocess.run(
|
|
1000
|
-
["docker-compose", "-f", compose_file, "ps", "--format", "json"],
|
|
1001
|
-
cwd=self.temp_dir,
|
|
1002
|
-
capture_output=True,
|
|
1003
|
-
text=True,
|
|
1004
|
-
timeout=10,
|
|
1005
|
-
)
|
|
1006
|
-
|
|
1007
|
-
if result.returncode == 0:
|
|
1008
|
-
import json
|
|
1009
|
-
|
|
1010
|
-
services = []
|
|
1011
|
-
for line in result.stdout.strip().split("\n"):
|
|
1012
|
-
if line.strip():
|
|
1013
|
-
try:
|
|
1014
|
-
service_info = json.loads(line)
|
|
1015
|
-
services.append(service_info)
|
|
1016
|
-
except json.JSONDecodeError:
|
|
1017
|
-
continue
|
|
1018
|
-
|
|
1019
|
-
# Check if all services are running
|
|
1020
|
-
all_running = True
|
|
1021
|
-
for service in services:
|
|
1022
|
-
# Ensure service is a dictionary before calling .get()
|
|
1023
|
-
if not isinstance(service, dict):
|
|
1024
|
-
debug_log(
|
|
1025
|
-
f"Service info is not a dictionary: {service}",
|
|
1026
|
-
"warning",
|
|
1027
|
-
)
|
|
1028
|
-
continue
|
|
1029
|
-
|
|
1030
|
-
if service.get("State") != "running":
|
|
1031
|
-
all_running = False
|
|
1032
|
-
debug_log(
|
|
1033
|
-
f"Service {service.get('Name')} is {service.get('State')}",
|
|
1034
|
-
"info",
|
|
1035
|
-
)
|
|
1036
|
-
break
|
|
1037
|
-
|
|
1038
|
-
if all_running and len(services) >= 2: # db and app services
|
|
1039
|
-
debug_log("All services are running", "info")
|
|
1040
|
-
# Additional wait for app to be fully ready
|
|
1041
|
-
time.sleep(10)
|
|
1042
|
-
return
|
|
1043
|
-
|
|
1044
|
-
except Exception as e:
|
|
1045
|
-
debug_log(f"Error checking service health: {e}", "warning")
|
|
1046
|
-
|
|
1047
|
-
debug_log("Services not ready yet, waiting...", "info")
|
|
1048
|
-
time.sleep(5)
|
|
1049
|
-
|
|
1050
|
-
debug_log(
|
|
1051
|
-
f"Services did not become healthy within {timeout} seconds", "warning"
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
def _verify_services_running(self, compose_file: str) -> bool:
|
|
1055
|
-
"""Verify that all required services are running."""
|
|
1056
|
-
try:
|
|
1057
|
-
result = subprocess.run(
|
|
1058
|
-
["docker-compose", "-f", compose_file, "ps", "--format", "json"],
|
|
1059
|
-
cwd=self.temp_dir,
|
|
1060
|
-
capture_output=True,
|
|
1061
|
-
text=True,
|
|
1062
|
-
timeout=30,
|
|
1063
|
-
)
|
|
1064
|
-
|
|
1065
|
-
if result.returncode != 0:
|
|
1066
|
-
debug_log(f"Failed to check service status: {result.stderr}", "error")
|
|
1067
|
-
return False
|
|
1068
|
-
|
|
1069
|
-
import json
|
|
1070
|
-
|
|
1071
|
-
services = []
|
|
1072
|
-
for line in result.stdout.strip().split("\n"):
|
|
1073
|
-
if line.strip():
|
|
1074
|
-
try:
|
|
1075
|
-
service_info = json.loads(line)
|
|
1076
|
-
services.append(service_info)
|
|
1077
|
-
except json.JSONDecodeError:
|
|
1078
|
-
continue
|
|
1079
|
-
|
|
1080
|
-
# Check for required services (db and app)
|
|
1081
|
-
db_running = False
|
|
1082
|
-
app_running = False
|
|
1083
|
-
|
|
1084
|
-
for service in services:
|
|
1085
|
-
# Ensure service is a dictionary before calling .get()
|
|
1086
|
-
if not isinstance(service, dict):
|
|
1087
|
-
debug_log(f"Service info is not a dictionary: {service}", "warning")
|
|
1088
|
-
continue
|
|
1089
|
-
|
|
1090
|
-
service_name = service.get("Name", "")
|
|
1091
|
-
service_state = service.get("State", "")
|
|
1092
|
-
|
|
1093
|
-
debug_log(f"Service {service_name}: {service_state}", "info")
|
|
1094
|
-
|
|
1095
|
-
if "db" in service_name and service_state == "running":
|
|
1096
|
-
db_running = True
|
|
1097
|
-
elif "app" in service_name and service_state == "running":
|
|
1098
|
-
app_running = True
|
|
1099
|
-
|
|
1100
|
-
if not db_running:
|
|
1101
|
-
self.errors.append("Database service is not running")
|
|
1102
|
-
return False
|
|
1103
|
-
|
|
1104
|
-
if not app_running:
|
|
1105
|
-
# Get logs from failed app service for debugging
|
|
1106
|
-
try:
|
|
1107
|
-
logs_result = subprocess.run(
|
|
1108
|
-
[
|
|
1109
|
-
"docker-compose",
|
|
1110
|
-
"-f",
|
|
1111
|
-
compose_file,
|
|
1112
|
-
"logs",
|
|
1113
|
-
"--tail",
|
|
1114
|
-
"50",
|
|
1115
|
-
"app",
|
|
1116
|
-
],
|
|
1117
|
-
cwd=self.temp_dir,
|
|
1118
|
-
capture_output=True,
|
|
1119
|
-
text=True,
|
|
1120
|
-
timeout=30,
|
|
1121
|
-
)
|
|
1122
|
-
if logs_result.returncode == 0:
|
|
1123
|
-
debug_log(f"App service logs: {logs_result.stdout}", "error")
|
|
1124
|
-
self.errors.append(
|
|
1125
|
-
f"Application service is not running. Logs: {logs_result.stdout[-500:]}"
|
|
1126
|
-
)
|
|
1127
|
-
else:
|
|
1128
|
-
self.errors.append("Application service is not running")
|
|
1129
|
-
except Exception:
|
|
1130
|
-
self.errors.append("Application service is not running")
|
|
1131
|
-
return False
|
|
1132
|
-
|
|
1133
|
-
debug_log("All required services are running", "info")
|
|
1134
|
-
return True
|
|
1135
|
-
|
|
1136
|
-
except Exception as e:
|
|
1137
|
-
debug_log(f"Error verifying services: {e}", "error")
|
|
1138
|
-
self.errors.append(f"Failed to verify service status: {e}")
|
|
1139
|
-
return False
|
|
1140
|
-
|
|
1141
|
-
def _run_docker_tests(self, compose_file: str) -> bool:
|
|
1142
|
-
"""Run tests in Docker environment."""
|
|
1143
|
-
try:
|
|
1144
|
-
# Check if scripts/test.sh exists in temp directory
|
|
1145
|
-
test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
|
|
1146
|
-
if os.path.exists(test_script_path):
|
|
1147
|
-
debug_log(
|
|
1148
|
-
"Found scripts/test.sh, using template test script in Docker",
|
|
1149
|
-
"info",
|
|
1150
|
-
)
|
|
1151
|
-
# Fix line endings before running in Docker
|
|
1152
|
-
self._fix_script_line_endings(test_script_path)
|
|
1153
|
-
# Run the test script inside Docker container
|
|
1154
|
-
result = subprocess.run(
|
|
1155
|
-
[
|
|
1156
|
-
"docker-compose",
|
|
1157
|
-
"-f",
|
|
1158
|
-
compose_file,
|
|
1159
|
-
"exec",
|
|
1160
|
-
"-T",
|
|
1161
|
-
"app",
|
|
1162
|
-
"bash",
|
|
1163
|
-
"-c",
|
|
1164
|
-
"chmod +x ./scripts/test.sh && ./scripts/test.sh",
|
|
1165
|
-
],
|
|
1166
|
-
cwd=self.temp_dir,
|
|
1167
|
-
capture_output=True,
|
|
1168
|
-
text=True,
|
|
1169
|
-
timeout=300,
|
|
1170
|
-
)
|
|
1171
|
-
else:
|
|
1172
|
-
debug_log(
|
|
1173
|
-
"No scripts/test.sh found, running pytest directly in Docker",
|
|
1174
|
-
"info",
|
|
1175
|
-
)
|
|
1176
|
-
# Run pytest directly using docker-compose exec
|
|
1177
|
-
result = subprocess.run(
|
|
1178
|
-
[
|
|
1179
|
-
"docker-compose",
|
|
1180
|
-
"-f",
|
|
1181
|
-
compose_file,
|
|
1182
|
-
"exec",
|
|
1183
|
-
"-T",
|
|
1184
|
-
"app",
|
|
1185
|
-
"python",
|
|
1186
|
-
"-m",
|
|
1187
|
-
"pytest",
|
|
1188
|
-
"tests/",
|
|
1189
|
-
"-v",
|
|
1190
|
-
],
|
|
1191
|
-
cwd=self.temp_dir,
|
|
1192
|
-
capture_output=True,
|
|
1193
|
-
text=True,
|
|
1194
|
-
timeout=300,
|
|
1195
|
-
)
|
|
1196
|
-
|
|
1197
|
-
if result.returncode != 0:
|
|
1198
|
-
error_msg = (
|
|
1199
|
-
f"Docker tests failed with return code {result.returncode}\n"
|
|
1200
|
-
)
|
|
1201
|
-
if result.stderr:
|
|
1202
|
-
error_msg += f"STDERR:\n{result.stderr}\n"
|
|
1203
|
-
if result.stdout:
|
|
1204
|
-
error_msg += f"STDOUT:\n{result.stdout}\n"
|
|
1205
|
-
self.errors.append(error_msg)
|
|
1206
|
-
debug_log(f"Docker strategy tests failed: {error_msg}", "error")
|
|
1207
|
-
return False
|
|
1208
|
-
|
|
1209
|
-
debug_log("Docker tests passed successfully", "info")
|
|
1210
|
-
return True
|
|
1211
|
-
|
|
1212
|
-
except subprocess.TimeoutExpired:
|
|
1213
|
-
self.errors.append("Docker tests timed out")
|
|
1214
|
-
return False
|
|
1215
|
-
except Exception as e:
|
|
1216
|
-
self.errors.append(f"Error running Docker tests: {e}")
|
|
1217
|
-
return False
|
|
1218
|
-
|
|
1219
|
-
def _run_docker_exec_tests(self, compose_file: str) -> bool:
|
|
1220
|
-
"""Run tests using docker-compose exec command."""
|
|
1221
|
-
try:
|
|
1222
|
-
# Check if scripts/test.sh exists in temp directory
|
|
1223
|
-
test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
|
|
1224
|
-
if os.path.exists(test_script_path):
|
|
1225
|
-
debug_log(
|
|
1226
|
-
"Found scripts/test.sh, using template test script with docker-compose exec",
|
|
1227
|
-
"info",
|
|
1228
|
-
)
|
|
1229
|
-
# Fix line endings before running in Docker
|
|
1230
|
-
self._fix_script_line_endings(test_script_path)
|
|
1231
|
-
# Run the test script inside Docker container using docker-compose exec
|
|
1232
|
-
result = subprocess.run(
|
|
1233
|
-
[
|
|
1234
|
-
"docker-compose",
|
|
1235
|
-
"-f",
|
|
1236
|
-
compose_file,
|
|
1237
|
-
"exec",
|
|
1238
|
-
"-T",
|
|
1239
|
-
"app",
|
|
1240
|
-
"bash",
|
|
1241
|
-
"scripts/test.sh",
|
|
1242
|
-
],
|
|
1243
|
-
cwd=self.temp_dir,
|
|
1244
|
-
capture_output=True,
|
|
1245
|
-
text=True,
|
|
1246
|
-
timeout=300,
|
|
1247
|
-
)
|
|
1248
|
-
else:
|
|
1249
|
-
debug_log(
|
|
1250
|
-
"No scripts/test.sh found, running pytest directly with docker-compose exec",
|
|
1251
|
-
"info",
|
|
1252
|
-
)
|
|
1253
|
-
# Run pytest directly using docker-compose exec
|
|
1254
|
-
result = subprocess.run(
|
|
1255
|
-
[
|
|
1256
|
-
"docker-compose",
|
|
1257
|
-
"-f",
|
|
1258
|
-
compose_file,
|
|
1259
|
-
"exec",
|
|
1260
|
-
"-T",
|
|
1261
|
-
"app",
|
|
1262
|
-
"python",
|
|
1263
|
-
"-m",
|
|
1264
|
-
"pytest",
|
|
1265
|
-
"tests/",
|
|
1266
|
-
"-v",
|
|
1267
|
-
],
|
|
1268
|
-
cwd=self.temp_dir,
|
|
1269
|
-
capture_output=True,
|
|
1270
|
-
text=True,
|
|
1271
|
-
timeout=300,
|
|
1272
|
-
)
|
|
1273
|
-
|
|
1274
|
-
if result.returncode != 0:
|
|
1275
|
-
error_msg = (
|
|
1276
|
-
f"Docker exec tests failed with return code {result.returncode}\n"
|
|
1277
|
-
)
|
|
1278
|
-
if result.stderr:
|
|
1279
|
-
error_msg += f"STDERR:\n{result.stderr}\n"
|
|
1280
|
-
if result.stdout:
|
|
1281
|
-
error_msg += f"STDOUT:\n{result.stdout}\n"
|
|
1282
|
-
self.errors.append(error_msg)
|
|
1283
|
-
debug_log(f"Docker exec strategy tests failed: {error_msg}", "error")
|
|
1284
|
-
debug_log(f"Docker exec test stderr: {result.stderr}", "error")
|
|
1285
|
-
debug_log(f"Docker exec test stdout: {result.stdout}", "info")
|
|
1286
|
-
return False
|
|
1287
|
-
|
|
1288
|
-
debug_log("Docker exec tests passed successfully", "info")
|
|
1289
|
-
debug_log(f"Docker exec test stdout: {result.stdout}", "info")
|
|
1290
|
-
return True
|
|
1291
|
-
|
|
1292
|
-
except subprocess.TimeoutExpired:
|
|
1293
|
-
self.errors.append("Docker exec tests timed out")
|
|
1294
|
-
return False
|
|
1295
|
-
except Exception as e:
|
|
1296
|
-
self.errors.append(f"Error running Docker exec tests: {e}")
|
|
1297
|
-
return False
|
|
1298
|
-
|
|
1299
|
-
def _cleanup_docker_services(self) -> None:
|
|
1300
|
-
"""Clean up Docker Compose services."""
|
|
1301
|
-
try:
|
|
1302
|
-
debug_log("Cleaning up Docker services", "info")
|
|
1303
|
-
subprocess.run(
|
|
1304
|
-
["docker-compose", "down", "-v", "--remove-orphans"],
|
|
1305
|
-
cwd=self.temp_dir,
|
|
1306
|
-
capture_output=True,
|
|
1307
|
-
text=True,
|
|
1308
|
-
timeout=60,
|
|
1309
|
-
)
|
|
1310
|
-
|
|
1311
|
-
try:
|
|
1312
|
-
subprocess.run(
|
|
1313
|
-
["docker", "system", "prune", "-f"],
|
|
1314
|
-
capture_output=True,
|
|
1315
|
-
text=True,
|
|
1316
|
-
timeout=30,
|
|
1317
|
-
)
|
|
1318
|
-
except Exception:
|
|
1319
|
-
pass
|
|
1320
|
-
|
|
1321
|
-
except Exception as e:
|
|
1322
|
-
debug_log(f"Failed to cleanup Docker services: {e}", "warning")
|
|
1323
|
-
|
|
1324
|
-
def get_report(self) -> Dict[str, Any]:
|
|
1325
|
-
"""
|
|
1326
|
-
Get inspection report with errors and warnings.
|
|
1327
|
-
|
|
1328
|
-
:return: Dictionary containing inspection results
|
|
1329
|
-
"""
|
|
1330
|
-
is_valid = len(self.errors) == 0
|
|
1331
|
-
template_name = self.template_path.name
|
|
1332
|
-
|
|
1333
|
-
# Log final inspection results
|
|
1334
|
-
if is_valid:
|
|
1335
|
-
debug_log(
|
|
1336
|
-
f"Template inspection completed successfully for {template_name}",
|
|
1337
|
-
"info",
|
|
1338
|
-
)
|
|
1339
|
-
if self.warnings:
|
|
1340
|
-
debug_log(
|
|
1341
|
-
f"Template {template_name} has {len(self.warnings)} warnings: {self.warnings}",
|
|
1342
|
-
"warning",
|
|
1343
|
-
)
|
|
1344
|
-
else:
|
|
1345
|
-
debug_log(
|
|
1346
|
-
f"Template inspection failed for {template_name} with {len(self.errors)} errors",
|
|
1347
|
-
"error",
|
|
1348
|
-
)
|
|
1349
|
-
for i, error in enumerate(self.errors, 1):
|
|
1350
|
-
debug_log(f"Error {i}: {error}", "error")
|
|
1351
|
-
if self.warnings:
|
|
1352
|
-
debug_log(
|
|
1353
|
-
f"Template {template_name} also has {len(self.warnings)} warnings: {self.warnings}",
|
|
1354
|
-
"warning",
|
|
1355
|
-
)
|
|
1356
|
-
|
|
1357
|
-
return {
|
|
1358
|
-
"template_path": str(self.template_path),
|
|
1359
|
-
"errors": self.errors,
|
|
1360
|
-
"warnings": self.warnings,
|
|
1361
|
-
"is_valid": is_valid,
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
def inspect_fastapi_template(
|
|
1366
|
-
template_path: str, temp_base_dir: Optional[str] = None
|
|
1367
|
-
) -> Dict[str, Any]:
|
|
1368
|
-
"""
|
|
1369
|
-
Convenience function to inspect a FastAPI template.
|
|
1370
|
-
|
|
1371
|
-
:param template_path: Path to the template to inspect
|
|
1372
|
-
:param temp_base_dir: Base directory for temporary files (defaults to backend directory)
|
|
1373
|
-
:return: Inspection report dictionary
|
|
1374
|
-
"""
|
|
1375
|
-
template_name = Path(template_path).name
|
|
1376
|
-
debug_log(
|
|
1377
|
-
f"Starting template inspection for {template_name} at {template_path}", "info"
|
|
1378
|
-
)
|
|
1379
|
-
|
|
1380
|
-
with TemplateInspector(template_path, temp_base_dir) as inspector:
|
|
1381
|
-
is_valid = inspector.inspect_template()
|
|
1382
|
-
report = inspector.get_report()
|
|
1383
|
-
|
|
1384
|
-
if is_valid:
|
|
1385
|
-
print_success(f"Template {template_path} is valid!")
|
|
1386
|
-
debug_log(
|
|
1387
|
-
f"Template inspection completed successfully for {template_name}",
|
|
1388
|
-
"info",
|
|
1389
|
-
)
|
|
1390
|
-
else:
|
|
1391
|
-
print_error(f"Template {template_path} validation failed")
|
|
1392
|
-
debug_log(f"Template inspection failed for {template_name}", "error")
|
|
1393
|
-
for error in inspector.errors:
|
|
1394
|
-
print_error(f" - {error}")
|
|
1395
|
-
|
|
1396
|
-
if inspector.warnings:
|
|
1397
|
-
debug_log(
|
|
1398
|
-
f"Template inspection for {template_name} has warnings", "warning"
|
|
1399
|
-
)
|
|
1400
|
-
for warning in inspector.warnings:
|
|
1401
|
-
print_warning(f" - {warning}")
|
|
19
|
+
from fastapi_fastkit.utils.main import print_error
|
|
1402
20
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
21
|
+
__all__ = [
|
|
22
|
+
"InspectionContext",
|
|
23
|
+
"InspectionOptions",
|
|
24
|
+
"TemplateInspector",
|
|
25
|
+
"inspect_fastapi_template",
|
|
26
|
+
]
|
|
1408
27
|
|
|
1409
28
|
|
|
1410
29
|
if __name__ == "__main__":
|
|
@@ -1412,5 +31,4 @@ if __name__ == "__main__":
|
|
|
1412
31
|
print_error("Usage: python inspector.py <template_dir>")
|
|
1413
32
|
sys.exit(1)
|
|
1414
33
|
|
|
1415
|
-
|
|
1416
|
-
inspect_fastapi_template(template_dir)
|
|
34
|
+
inspect_fastapi_template(sys.argv[1])
|