FastAPI-fastkit 1.2.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_fastkit/__init__.py +1 -1
- fastapi_fastkit/backend/inspection/__init__.py +26 -0
- fastapi_fastkit/backend/inspection/checks.py +349 -0
- fastapi_fastkit/backend/inspection/consistency.py +125 -0
- fastapi_fastkit/backend/inspection/context.py +67 -0
- fastapi_fastkit/backend/inspection/core.py +347 -0
- fastapi_fastkit/backend/inspection/docker.py +185 -0
- fastapi_fastkit/backend/inspection/freshness.py +145 -0
- fastapi_fastkit/backend/inspection/fsutils.py +139 -0
- fastapi_fastkit/backend/inspection/lint.py +89 -0
- fastapi_fastkit/backend/inspection/report.py +58 -0
- fastapi_fastkit/backend/inspection/smoke.py +327 -0
- fastapi_fastkit/backend/inspection/strategies.py +336 -0
- fastapi_fastkit/backend/inspector.py +18 -1279
- fastapi_fastkit/backend/interactive/__init__.py +8 -0
- fastapi_fastkit/backend/interactive/config_builder.py +66 -65
- fastapi_fastkit/backend/interactive/prompts.py +205 -5
- fastapi_fastkit/backend/interactive/selectors.py +28 -2
- fastapi_fastkit/backend/main.py +956 -95
- fastapi_fastkit/backend/package_managers/base.py +172 -0
- fastapi_fastkit/backend/package_managers/factory.py +6 -5
- fastapi_fastkit/backend/package_managers/pdm_manager.py +32 -85
- fastapi_fastkit/backend/package_managers/pip_manager.py +31 -85
- fastapi_fastkit/backend/package_managers/poetry_manager.py +73 -113
- fastapi_fastkit/backend/package_managers/uv_manager.py +46 -115
- fastapi_fastkit/backend/project_builder/__init__.py +10 -0
- fastapi_fastkit/backend/project_builder/config_generator.py +813 -450
- fastapi_fastkit/backend/project_builder/config_schema.py +384 -0
- fastapi_fastkit/backend/project_builder/dependency_collector.py +105 -66
- fastapi_fastkit/backend/project_builder/preset_layout.py +429 -0
- fastapi_fastkit/backend/scaffolder.py +578 -0
- fastapi_fastkit/backend/transducer.py +54 -22
- fastapi_fastkit/cli.py +452 -284
- fastapi_fastkit/core/settings.py +248 -43
- fastapi_fastkit/fastapi_project_template/README.md +88 -23
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +25 -23
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +39 -36
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +9 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/.env-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/.gitignore-tpl +33 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/Dockerfile-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl +210 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/alembic.ini-tpl +119 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/pyproject.toml-tpl +86 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/requirements.txt-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/README-tpl +1 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/env.py-tpl +51 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/script.py.mako-tpl +26 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/versions/0001_initial_auth_schema.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/deps.py-tpl +91 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/router.py-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/config.py-tpl +64 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/security.py-tpl +100 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/db/session.py-tpl +36 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/models.py-tpl +25 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/repository.py-tpl +41 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/router.py-tpl +82 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/schemas.py-tpl +14 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/service.py-tpl +65 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/models.py-tpl +35 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/repository.py-tpl +33 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/router.py-tpl +71 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/schemas.py-tpl +29 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/service.py-tpl +55 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/main.py-tpl +69 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/template-config.yml-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/conftest.py-tpl +73 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_auth.py-tpl +201 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_health.py-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_users.py-tpl +77 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +25 -23
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +39 -36
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +3 -3
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +22 -20
- fastapi_fastkit/fastapi_project_template/fastapi-default/requirements.txt-tpl +37 -34
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/api/routes/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/main.py-tpl +9 -1
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +2 -3
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +0 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +25 -23
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +41 -36
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +9 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +2 -3
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +0 -2
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.env-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.gitignore-tpl +31 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl +128 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/pyproject.toml-tpl +69 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +37 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/__init__.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/models.py-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +47 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +58 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/schemas.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +51 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +28 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_items.py-tpl +77 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +2 -2
- fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +18 -16
- fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +36 -10
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +3 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.env-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.gitignore-tpl +31 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/Dockerfile-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl +193 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/pyproject.toml-tpl +76 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/requirements.txt-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/chat.sh-tpl +10 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/chat.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/deps.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/router.py-tpl +13 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/config.py-tpl +67 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/agent.py-tpl +136 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/client.py-tpl +30 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/tools.py-tpl +131 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/main.py-tpl +53 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/base.py-tpl +27 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/in_memory.py-tpl +32 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/chat.py-tpl +46 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/template-config.yml-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/conftest.py-tpl +158 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_chat.py-tpl +160 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_health.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_memory.py-tpl +38 -0
- fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_tools.py-tpl +37 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +29 -26
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +68 -39
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +3 -4
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/router.py-tpl +3 -5
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +6 -7
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +26 -24
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +44 -41
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +1 -3
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +5 -6
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +2 -4
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +1 -2
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +4 -6
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +4 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +1 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +0 -2
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/.env-tpl +1 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/.gitignore-tpl +30 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +7 -5
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/requirements.txt-tpl +26 -6
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/src/main.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/conftest.py-tpl +0 -1
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.env-tpl +7 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.gitignore-tpl +35 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/Dockerfile-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl +166 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/alembic.ini-tpl +44 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/pyproject.toml-tpl +82 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/requirements.txt-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/migrate.sh-tpl +7 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/README-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/env.py-tpl +72 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/script.py.mako-tpl +27 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/versions/0001_create_items_table.py-tpl +38 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/router.py-tpl +15 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/config.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/base.py-tpl +62 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/pagination.py-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/base.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/session.py-tpl +50 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/crud.py-tpl +26 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/models.py-tpl +70 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/router.py-tpl +69 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/main.py-tpl +43 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/template-config.yml-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/conftest.py-tpl +60 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_crud_base.py-tpl +43 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_health.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_items.py-tpl +110 -0
- fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +2 -2
- fastapi_fastkit/fragments/README.md +90 -0
- fastapi_fastkit/fragments/auth/fastapi_users.py.j2 +8 -0
- fastapi_fastkit/fragments/auth/jwt.py.j2 +44 -0
- fastapi_fastkit/fragments/auth/oauth2.py.j2 +105 -0
- fastapi_fastkit/fragments/auth/session.py.j2 +67 -0
- fastapi_fastkit/fragments/db/mongodb.py.j2 +19 -0
- fastapi_fastkit/fragments/db/redis.py.j2 +26 -0
- fastapi_fastkit/fragments/db/sqlalchemy.py.j2 +45 -0
- fastapi_fastkit/fragments/docker/docker_compose.j2 +84 -0
- fastapi_fastkit/fragments/docker/dockerfile.j2 +19 -0
- fastapi_fastkit/fragments/features/cache.py.j2 +26 -0
- fastapi_fastkit/fragments/features/pagination.py.j2 +31 -0
- fastapi_fastkit/fragments/features/tasks_celery.py.j2 +50 -0
- fastapi_fastkit/fragments/features/tasks_dramatiq.py.j2 +59 -0
- fastapi_fastkit/fragments/features/websocket.py.j2 +66 -0
- fastapi_fastkit/fragments/header.j2 +4 -0
- fastapi_fastkit/fragments/logging/structured.py.j2 +88 -0
- fastapi_fastkit/fragments/main/imports/async_tasks.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/caching.j2 +5 -0
- fastapi_fastkit/fragments/main/imports/fastapi_users.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/jwt.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/loguru.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/mongodb.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/oauth2.j2 +4 -0
- fastapi_fastkit/fragments/main/imports/opentelemetry.j2 +6 -0
- fastapi_fastkit/fragments/main/imports/pagination.j2 +3 -0
- fastapi_fastkit/fragments/main/imports/prometheus.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/rate_limiting.j2 +3 -0
- fastapi_fastkit/fragments/main/imports/session_auth.j2 +4 -0
- fastapi_fastkit/fragments/main/imports/sqlalchemy.j2 +2 -0
- fastapi_fastkit/fragments/main/imports/structured_logging.j2 +1 -0
- fastapi_fastkit/fragments/main/imports/websocket.j2 +1 -0
- fastapi_fastkit/fragments/main/lifespan/caching.j2 +7 -0
- fastapi_fastkit/fragments/main/lifespan/mongodb.j2 +2 -0
- fastapi_fastkit/fragments/main/lifespan/prometheus.j2 +2 -0
- fastapi_fastkit/fragments/main/lifespan/sqlalchemy.j2 +2 -0
- fastapi_fastkit/fragments/main/setup/async_tasks.j2 +1 -0
- fastapi_fastkit/fragments/main/setup/caching.j2 +1 -0
- fastapi_fastkit/fragments/main/setup/cors.j2 +8 -0
- fastapi_fastkit/fragments/main/setup/oauth2.j2 +3 -0
- fastapi_fastkit/fragments/main/setup/opentelemetry.j2 +19 -0
- fastapi_fastkit/fragments/main/setup/pagination.j2 +2 -0
- fastapi_fastkit/fragments/main/setup/rate_limiting.j2 +3 -0
- fastapi_fastkit/fragments/main/setup/session_auth.j2 +6 -0
- fastapi_fastkit/fragments/main/setup/structured_logging.j2 +2 -0
- fastapi_fastkit/fragments/main/setup/websocket.j2 +1 -0
- fastapi_fastkit/fragments/main.py.j2 +57 -0
- fastapi_fastkit/fragments/migrations/alembic_ini.j2 +42 -0
- fastapi_fastkit/fragments/migrations/env.py.j2 +76 -0
- fastapi_fastkit/fragments/migrations/initial_revision.py.j2 +27 -0
- fastapi_fastkit/fragments/migrations/migrate_sh.j2 +34 -0
- fastapi_fastkit/fragments/migrations/script.py.mako.j2 +26 -0
- fastapi_fastkit/fragments/tasks/celery_worker.py.j2 +49 -0
- fastapi_fastkit/fragments/tasks/dramatiq_worker.py.j2 +41 -0
- fastapi_fastkit/fragments/test/factories.py.j2 +68 -0
- fastapi_fastkit/fragments/test/pytest_ini.j2 +16 -0
- fastapi_fastkit/fragments/test/test_factories.py.j2 +27 -0
- fastapi_fastkit/fragments/test/test_main.py.j2 +46 -0
- fastapi_fastkit/fragments/tooling/devcontainer.j2 +26 -0
- fastapi_fastkit/fragments/tooling/github_actions.j2 +37 -0
- fastapi_fastkit/fragments/tooling/makefile.j2 +40 -0
- fastapi_fastkit/fragments/tooling/pre_commit.j2 +36 -0
- fastapi_fastkit/fragments/tooling/ruff_toml.j2 +16 -0
- fastapi_fastkit/utils/config_file.py +263 -0
- fastapi_fastkit/utils/main.py +111 -6
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/METADATA +33 -6
- fastapi_fastkit-1.4.0.dist-info/RECORD +484 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/WHEEL +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +0 -25
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +0 -27
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +0 -22
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +0 -51
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +0 -25
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/setup.py-tpl +0 -17
- fastapi_fastkit-1.2.1.dist-info/RECORD +0 -256
- /fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/__init__.py-tpl +0 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/entry_points.txt +0 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/licenses/LICENSE +0 -0
fastapi_fastkit/backend/main.py
CHANGED
|
@@ -3,28 +3,45 @@
|
|
|
3
3
|
#
|
|
4
4
|
# @author bnbong bbbong9@gmail.com
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
|
+
import ast
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
6
9
|
import os
|
|
7
10
|
import re
|
|
8
|
-
|
|
11
|
+
import tokenize
|
|
12
|
+
import tomllib
|
|
13
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
9
14
|
|
|
10
15
|
import click
|
|
11
16
|
|
|
12
17
|
from fastapi_fastkit.backend.package_managers import PackageManagerFactory
|
|
18
|
+
from fastapi_fastkit.backend.project_builder.preset_layout import (
|
|
19
|
+
app_module_from_main_path,
|
|
20
|
+
)
|
|
13
21
|
from fastapi_fastkit.backend.transducer import (
|
|
14
22
|
copy_and_convert_template,
|
|
15
23
|
copy_and_convert_template_file,
|
|
16
24
|
)
|
|
17
25
|
from fastapi_fastkit.core.exceptions import BackendExceptions
|
|
18
26
|
from fastapi_fastkit.core.settings import settings
|
|
27
|
+
from fastapi_fastkit.utils.config_file import format_toml_value
|
|
19
28
|
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
20
29
|
from fastapi_fastkit.utils.main import (
|
|
30
|
+
FASTKIT_DESCRIPTION_MARKER,
|
|
31
|
+
FASTKIT_TOOL_SECTION,
|
|
21
32
|
handle_exception,
|
|
22
|
-
print_error,
|
|
23
33
|
print_info,
|
|
24
34
|
print_success,
|
|
25
35
|
print_warning,
|
|
36
|
+
read_fastkit_metadata,
|
|
26
37
|
)
|
|
27
38
|
|
|
39
|
+
# Explicit insertion points templates may declare so ``addroute`` never has
|
|
40
|
+
# to guess where generated code belongs. See the route anchor contract in
|
|
41
|
+
# the project docs.
|
|
42
|
+
IMPORT_ANCHOR = "# fastkit:imports"
|
|
43
|
+
ROUTES_ANCHOR = "# fastkit:routes"
|
|
44
|
+
|
|
28
45
|
logger = get_logger(__name__)
|
|
29
46
|
|
|
30
47
|
|
|
@@ -82,12 +99,19 @@ def find_template_core_modules(project_dir: str) -> Dict[str, str]:
|
|
|
82
99
|
|
|
83
100
|
def read_template_stack(template_path: str) -> List[str]:
|
|
84
101
|
"""
|
|
85
|
-
Read dependencies from template
|
|
102
|
+
Read dependencies from a template's metadata files.
|
|
103
|
+
|
|
104
|
+
Lookup order preserves legacy behaviour for templates that still ship
|
|
105
|
+
``requirements.txt-tpl`` while allowing modern pyproject-first templates
|
|
106
|
+
to resolve their stack from ``pyproject.toml-tpl``:
|
|
107
|
+
|
|
108
|
+
1. ``requirements.txt-tpl`` (legacy, highest precedence)
|
|
109
|
+
2. ``pyproject.toml-tpl`` ``[project].dependencies``
|
|
110
|
+
3. ``setup.py-tpl`` ``install_requires``
|
|
86
111
|
|
|
87
112
|
:param template_path: Path to the template directory
|
|
88
|
-
:return: List of
|
|
113
|
+
:return: List of PEP 508 requirement strings
|
|
89
114
|
"""
|
|
90
|
-
# First try requirements.txt-tpl
|
|
91
115
|
req_file = os.path.join(template_path, "requirements.txt-tpl")
|
|
92
116
|
if os.path.exists(req_file):
|
|
93
117
|
try:
|
|
@@ -100,7 +124,12 @@ def read_template_stack(template_path: str) -> List[str]:
|
|
|
100
124
|
)
|
|
101
125
|
return []
|
|
102
126
|
|
|
103
|
-
|
|
127
|
+
pyproject_file = os.path.join(template_path, "pyproject.toml-tpl")
|
|
128
|
+
if os.path.exists(pyproject_file):
|
|
129
|
+
deps = _parse_pyproject_dependencies(pyproject_file)
|
|
130
|
+
if deps:
|
|
131
|
+
return deps
|
|
132
|
+
|
|
104
133
|
setup_file = os.path.join(template_path, "setup.py-tpl")
|
|
105
134
|
if os.path.exists(setup_file):
|
|
106
135
|
try:
|
|
@@ -153,20 +182,285 @@ def _parse_setup_dependencies(content: str) -> List[str]:
|
|
|
153
182
|
return []
|
|
154
183
|
|
|
155
184
|
|
|
185
|
+
def _parse_pyproject_dependencies(pyproject_path: str) -> List[str]:
|
|
186
|
+
"""
|
|
187
|
+
Parse dependencies from a ``pyproject.toml-tpl`` file.
|
|
188
|
+
|
|
189
|
+
Returns the list of PEP 508 requirement strings from
|
|
190
|
+
``[project].dependencies``. Returns an empty list on parse errors so
|
|
191
|
+
callers can fall through to the next metadata source.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
with open(pyproject_path, "rb") as f:
|
|
195
|
+
data = tomllib.load(f)
|
|
196
|
+
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
|
|
197
|
+
debug_log(f"Error reading pyproject.toml template: {e}", "error")
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
project = data.get("project", {})
|
|
201
|
+
raw_deps = project.get("dependencies", []) or []
|
|
202
|
+
if not isinstance(raw_deps, list):
|
|
203
|
+
return []
|
|
204
|
+
return [dep.strip() for dep in raw_deps if isinstance(dep, str) and dep.strip()]
|
|
205
|
+
|
|
206
|
+
|
|
156
207
|
# ------------------------------------------------------------
|
|
157
208
|
# Project Setup Functions
|
|
158
209
|
# ------------------------------------------------------------
|
|
159
210
|
|
|
160
211
|
|
|
212
|
+
# Placeholder tokens a template may declare anywhere in its files. Every one
|
|
213
|
+
# of them is substituted project-wide during generation, so a template author
|
|
214
|
+
# can use them in a README, a .env, a conftest.py - not just in the handful of
|
|
215
|
+
# "core" modules the injector used to know about.
|
|
216
|
+
PROJECT_PLACEHOLDERS = (
|
|
217
|
+
"<project_name>",
|
|
218
|
+
"<author>",
|
|
219
|
+
"<author_email>",
|
|
220
|
+
"<description>",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Suffixes that are safe to rewrite as UTF-8 text. Anything else (images,
|
|
224
|
+
# fonts, compiled artifacts) is skipped without being read. Extension-less
|
|
225
|
+
# files (Dockerfile, Makefile, .env, .gitignore) are read too - they are the
|
|
226
|
+
# ones templates most often put placeholders in.
|
|
227
|
+
_TEXT_FILE_SUFFIXES = frozenset(
|
|
228
|
+
{
|
|
229
|
+
"",
|
|
230
|
+
".cfg",
|
|
231
|
+
".conf",
|
|
232
|
+
".env",
|
|
233
|
+
".example",
|
|
234
|
+
".html",
|
|
235
|
+
".in",
|
|
236
|
+
".ini",
|
|
237
|
+
".j2",
|
|
238
|
+
".json",
|
|
239
|
+
".mako",
|
|
240
|
+
".md",
|
|
241
|
+
".po",
|
|
242
|
+
".py",
|
|
243
|
+
".rst",
|
|
244
|
+
".sh",
|
|
245
|
+
".sql",
|
|
246
|
+
".toml",
|
|
247
|
+
".txt",
|
|
248
|
+
".yaml",
|
|
249
|
+
".yml",
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Directories never worth walking: virtualenvs and caches are not part of the
|
|
254
|
+
# template output, and rewriting them would be both slow and wrong.
|
|
255
|
+
_SKIPPED_SCAN_DIRS = frozenset(
|
|
256
|
+
{
|
|
257
|
+
".git",
|
|
258
|
+
".mypy_cache",
|
|
259
|
+
".pytest_cache",
|
|
260
|
+
".ruff_cache",
|
|
261
|
+
".venv",
|
|
262
|
+
"__pycache__",
|
|
263
|
+
"venv",
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Guard against rewriting an accidentally huge file (a checked-in fixture,
|
|
268
|
+
# a lockfile dump). 2 MiB is far above any real template source file.
|
|
269
|
+
_MAX_SUBSTITUTION_FILE_BYTES = 2 * 1024 * 1024
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
#: File suffixes whose placeholder values must be escaped as TOML basic
|
|
273
|
+
#: string bodies, and those needing Python/JSON string escaping. A raw
|
|
274
|
+
#: substitution of a value containing a quote or a backslash would otherwise
|
|
275
|
+
#: produce a pyproject.toml or a config.py that no longer parses.
|
|
276
|
+
_TOML_SUFFIXES = frozenset({".toml"})
|
|
277
|
+
_PY_STRING_SUFFIXES = frozenset({".py", ".json"})
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def escape_placeholder_value(value: str, file_path: str) -> str:
|
|
281
|
+
"""
|
|
282
|
+
Escape a metadata value for safe substitution inside ``file_path``.
|
|
283
|
+
|
|
284
|
+
Placeholder tokens sit *inside* a quoted literal in the template
|
|
285
|
+
(``name = "<project_name>"``), so what is needed is the escaped *body*
|
|
286
|
+
of a string, not a fully quoted literal.
|
|
287
|
+
|
|
288
|
+
:param value: Raw metadata value entered by the user
|
|
289
|
+
:param file_path: Destination file the value is substituted into
|
|
290
|
+
:return: The value escaped for that file type (unchanged for plain text)
|
|
291
|
+
"""
|
|
292
|
+
suffix = os.path.splitext(file_path)[1].lower()
|
|
293
|
+
if suffix in _TOML_SUFFIXES:
|
|
294
|
+
return format_toml_value(value)[1:-1]
|
|
295
|
+
if suffix in _PY_STRING_SUFFIXES:
|
|
296
|
+
return json.dumps(value, ensure_ascii=False)[1:-1]
|
|
297
|
+
return value
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _escaped_replacements(
|
|
301
|
+
replacements: Dict[str, str], file_path: str
|
|
302
|
+
) -> Dict[str, str]:
|
|
303
|
+
"""Escape every replacement value for the target file's syntax."""
|
|
304
|
+
return {
|
|
305
|
+
placeholder: escape_placeholder_value(value, file_path)
|
|
306
|
+
for placeholder, value in replacements.items()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _apply_escaped_replacements(
|
|
311
|
+
content: str, replacements: Dict[str, str], file_path: str
|
|
312
|
+
) -> str:
|
|
313
|
+
"""Substitute ``replacements`` into ``content``, escaping per file type."""
|
|
314
|
+
for placeholder, value in _escaped_replacements(replacements, file_path).items():
|
|
315
|
+
content = content.replace(placeholder, value)
|
|
316
|
+
return content
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _is_substitutable_file(file_path: str) -> bool:
|
|
320
|
+
"""Return True when a file should be scanned for placeholder tokens.
|
|
321
|
+
|
|
322
|
+
:param file_path: Absolute path to the candidate file
|
|
323
|
+
:return: True for reasonably sized text files
|
|
324
|
+
"""
|
|
325
|
+
if os.path.islink(file_path):
|
|
326
|
+
return False
|
|
327
|
+
if os.path.splitext(file_path)[1].lower() not in _TEXT_FILE_SUFFIXES:
|
|
328
|
+
return False
|
|
329
|
+
try:
|
|
330
|
+
return os.path.getsize(file_path) <= _MAX_SUBSTITUTION_FILE_BYTES
|
|
331
|
+
except OSError:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _iter_project_text_files(project_dir: str) -> List[str]:
|
|
336
|
+
"""List every text file in a generated project, skipping caches and venvs.
|
|
337
|
+
|
|
338
|
+
:param project_dir: Path to the generated project
|
|
339
|
+
:return: Absolute paths of candidate text files
|
|
340
|
+
"""
|
|
341
|
+
candidates: List[str] = []
|
|
342
|
+
for root, dirs, files in os.walk(project_dir):
|
|
343
|
+
dirs[:] = [d for d in dirs if d not in _SKIPPED_SCAN_DIRS]
|
|
344
|
+
for file_name in files:
|
|
345
|
+
file_path = os.path.join(root, file_name)
|
|
346
|
+
if _is_substitutable_file(file_path):
|
|
347
|
+
candidates.append(file_path)
|
|
348
|
+
return candidates
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _substitution_targets(
|
|
352
|
+
project_dir: str, files: Optional[Iterable[str]] = None
|
|
353
|
+
) -> List[str]:
|
|
354
|
+
"""Resolve which files a rewriting pass may touch.
|
|
355
|
+
|
|
356
|
+
``files`` is the list of paths the template deployment actually wrote.
|
|
357
|
+
Scoping to it is what keeps an in-place deployment (``project_dir`` is
|
|
358
|
+
the user's own workspace) from rewriting unrelated files that happened
|
|
359
|
+
to be sitting there. Only when a caller supplies nothing do we fall back
|
|
360
|
+
to walking the whole project.
|
|
361
|
+
"""
|
|
362
|
+
if files is None:
|
|
363
|
+
return _iter_project_text_files(project_dir)
|
|
364
|
+
return [path for path in dict.fromkeys(files) if _is_substitutable_file(path)]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def replace_placeholders_in_project(
|
|
368
|
+
project_dir: str,
|
|
369
|
+
replacements: Dict[str, str],
|
|
370
|
+
files: Optional[Iterable[str]] = None,
|
|
371
|
+
) -> List[str]:
|
|
372
|
+
"""
|
|
373
|
+
Substitute placeholder tokens across every text file of a project.
|
|
374
|
+
|
|
375
|
+
Files that cannot be decoded as UTF-8 are left untouched rather than
|
|
376
|
+
corrupted, and a file is only rewritten when it actually changed.
|
|
377
|
+
|
|
378
|
+
:param project_dir: Path to the generated project
|
|
379
|
+
:param replacements: Mapping of placeholder token to replacement value
|
|
380
|
+
:param files: Absolute paths written by the template deployment. When
|
|
381
|
+
given, only these files are considered; otherwise the whole project
|
|
382
|
+
tree is walked.
|
|
383
|
+
:return: Project-relative paths of the files that were rewritten
|
|
384
|
+
"""
|
|
385
|
+
rewritten: List[str] = []
|
|
386
|
+
|
|
387
|
+
for file_path in _substitution_targets(project_dir, files):
|
|
388
|
+
try:
|
|
389
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
390
|
+
content = f.read()
|
|
391
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
392
|
+
debug_log(f"Skipping {file_path} during substitution: {e}", "debug")
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
updated = _apply_escaped_replacements(content, replacements, file_path)
|
|
396
|
+
|
|
397
|
+
if updated == content:
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
402
|
+
f.write(updated)
|
|
403
|
+
except OSError as e:
|
|
404
|
+
debug_log(f"Failed to write substituted file {file_path}: {e}", "error")
|
|
405
|
+
raise BackendExceptions(f"Failed to substitute placeholders: {e}")
|
|
406
|
+
|
|
407
|
+
rewritten.append(os.path.relpath(file_path, project_dir))
|
|
408
|
+
|
|
409
|
+
return rewritten
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def find_unsubstituted_placeholders(
|
|
413
|
+
project_dir: str, files: Optional[Iterable[str]] = None
|
|
414
|
+
) -> Dict[str, List[str]]:
|
|
415
|
+
"""
|
|
416
|
+
Report placeholder tokens that survived project generation.
|
|
417
|
+
|
|
418
|
+
A surviving token means the template declares a variable the generator
|
|
419
|
+
does not know how to fill - a bug in the template, and a broken file in
|
|
420
|
+
the user's brand new project. Callers surface it instead of failing hard
|
|
421
|
+
so the project is still usable.
|
|
422
|
+
|
|
423
|
+
:param project_dir: Path to the generated project
|
|
424
|
+
:param files: Absolute paths written by the template deployment; when
|
|
425
|
+
given the scan is limited to them
|
|
426
|
+
:return: Mapping of project-relative file path to the tokens found in it
|
|
427
|
+
"""
|
|
428
|
+
residue: Dict[str, List[str]] = {}
|
|
429
|
+
|
|
430
|
+
for file_path in _substitution_targets(project_dir, files):
|
|
431
|
+
try:
|
|
432
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
433
|
+
content = f.read()
|
|
434
|
+
except (OSError, UnicodeDecodeError):
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
hits = [token for token in PROJECT_PLACEHOLDERS if token in content]
|
|
438
|
+
if hits:
|
|
439
|
+
residue[os.path.relpath(file_path, project_dir)] = hits
|
|
440
|
+
|
|
441
|
+
return residue
|
|
442
|
+
|
|
443
|
+
|
|
161
444
|
def inject_project_metadata(
|
|
162
445
|
target_dir: str,
|
|
163
446
|
project_name: str,
|
|
164
447
|
author: str,
|
|
165
448
|
author_email: str,
|
|
166
449
|
description: str,
|
|
450
|
+
files: Optional[Iterable[str]] = None,
|
|
167
451
|
) -> None:
|
|
168
452
|
"""
|
|
169
453
|
Inject project metadata into template files after template conversion.
|
|
454
|
+
|
|
455
|
+
The core metadata files (setup.py, pyproject.toml, the settings module and
|
|
456
|
+
main.py) are handled first because they need more than a text replacement -
|
|
457
|
+
pyproject also gets the ``[tool.fastapi-fastkit]`` marker block. Every
|
|
458
|
+
remaining text file in the project is then swept for the same placeholder
|
|
459
|
+
tokens, so README.md, .env, tests/conftest.py and friends are covered too.
|
|
460
|
+
|
|
461
|
+
``files`` scopes that sweep to the paths the template deployment wrote.
|
|
462
|
+
An in-place deployment targets the user's own workspace, and walking the
|
|
463
|
+
whole tree there would rewrite unrelated files.
|
|
170
464
|
"""
|
|
171
465
|
try:
|
|
172
466
|
core_modules = find_template_core_modules(target_dir)
|
|
@@ -187,8 +481,31 @@ def inject_project_metadata(
|
|
|
187
481
|
_process_config_file(core_modules.get("config", ""), project_name)
|
|
188
482
|
_process_main_file(core_modules.get("main", ""), project_name)
|
|
189
483
|
|
|
484
|
+
replacements = dict(
|
|
485
|
+
zip(
|
|
486
|
+
PROJECT_PLACEHOLDERS,
|
|
487
|
+
(project_name, author, author_email, description),
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
rewritten = replace_placeholders_in_project(target_dir, replacements, files)
|
|
491
|
+
if rewritten:
|
|
492
|
+
debug_log(
|
|
493
|
+
f"Substituted placeholders in {len(rewritten)} additional files",
|
|
494
|
+
"info",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
residue = find_unsubstituted_placeholders(target_dir, files)
|
|
498
|
+
if residue:
|
|
499
|
+
for relative_path, tokens in sorted(residue.items()):
|
|
500
|
+
print_warning(
|
|
501
|
+
f"{relative_path}: {', '.join(tokens)}",
|
|
502
|
+
title="Unsubstituted placeholder",
|
|
503
|
+
)
|
|
504
|
+
|
|
190
505
|
print_success("Project metadata injected successfully")
|
|
191
506
|
|
|
507
|
+
except BackendExceptions:
|
|
508
|
+
raise
|
|
192
509
|
except Exception as e:
|
|
193
510
|
debug_log(f"Failed to inject project metadata: {e}", "error")
|
|
194
511
|
handle_exception(e, "Failed to inject project metadata")
|
|
@@ -222,8 +539,7 @@ def _process_setup_file(
|
|
|
222
539
|
"<description>": description,
|
|
223
540
|
}
|
|
224
541
|
|
|
225
|
-
|
|
226
|
-
content = content.replace(placeholder, value)
|
|
542
|
+
content = _apply_escaped_replacements(content, replacements, setup_py)
|
|
227
543
|
|
|
228
544
|
with open(setup_py, "w", encoding="utf-8") as f:
|
|
229
545
|
f.write(content)
|
|
@@ -257,7 +573,9 @@ def _process_main_file(main_py: str, project_name: str) -> None:
|
|
|
257
573
|
if "<project_name>" not in content:
|
|
258
574
|
return
|
|
259
575
|
|
|
260
|
-
content = content.replace(
|
|
576
|
+
content = content.replace(
|
|
577
|
+
"<project_name>", escape_placeholder_value(project_name, main_py)
|
|
578
|
+
)
|
|
261
579
|
|
|
262
580
|
with open(main_py, "w", encoding="utf-8") as f:
|
|
263
581
|
f.write(content)
|
|
@@ -285,7 +603,9 @@ def _process_config_file(config_py: str, project_name: str) -> None:
|
|
|
285
603
|
content = f.read()
|
|
286
604
|
|
|
287
605
|
# Replace project name placeholder
|
|
288
|
-
content = content.replace(
|
|
606
|
+
content = content.replace(
|
|
607
|
+
"<project_name>", escape_placeholder_value(project_name, config_py)
|
|
608
|
+
)
|
|
289
609
|
|
|
290
610
|
with open(config_py, "w", encoding="utf-8") as f:
|
|
291
611
|
f.write(content)
|
|
@@ -329,8 +649,9 @@ def _process_pyproject_file(
|
|
|
329
649
|
"<description>": description,
|
|
330
650
|
}
|
|
331
651
|
|
|
332
|
-
|
|
333
|
-
|
|
652
|
+
content = _apply_escaped_replacements(content, replacements, pyproject_toml)
|
|
653
|
+
|
|
654
|
+
content = _ensure_pyproject_fastkit_markers(content)
|
|
334
655
|
|
|
335
656
|
with open(pyproject_toml, "w", encoding="utf-8") as f:
|
|
336
657
|
f.write(content)
|
|
@@ -343,20 +664,288 @@ def _process_pyproject_file(
|
|
|
343
664
|
raise BackendExceptions(f"Failed to process pyproject.toml: {e}")
|
|
344
665
|
|
|
345
666
|
|
|
667
|
+
_DESCRIPTION_LINE_PATTERN = re.compile(
|
|
668
|
+
r'^(?P<prefix>description\s*=\s*)"(?P<value>.*?)"(?P<suffix>\s*)$',
|
|
669
|
+
re.MULTILINE,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
# The ``[tool.fastapi-fastkit]`` table body cannot be matched with a regex: a
|
|
674
|
+
# ``features = [...]`` array contains ``[`` characters, so an "everything up to
|
|
675
|
+
# the next bracket" pattern stops in the middle of the table and leaves half of
|
|
676
|
+
# it behind. The extent of the table is found line by line instead, using the
|
|
677
|
+
# two patterns below (see ``_replace_fastkit_section``).
|
|
678
|
+
_FASTKIT_SECTION_HEADER_PATTERN = re.compile(
|
|
679
|
+
r"^\[tool\." + re.escape(FASTKIT_TOOL_SECTION) + r"\]\s*(?:#.*)?$"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
#: A TOML table / array-of-tables header line, used as the section terminator.
|
|
683
|
+
_TOML_TABLE_HEADER_PATTERN = re.compile(r"^\[\[?[^\[\]]+\]\]?\s*(?:#.*)?$")
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _replace_fastkit_section(content: str, block: str) -> str:
|
|
687
|
+
"""
|
|
688
|
+
Replace an existing ``[tool.fastapi-fastkit]`` table with ``block``.
|
|
689
|
+
|
|
690
|
+
The table runs from its header up to (but excluding) the next line that
|
|
691
|
+
starts a new TOML table, so a multi-line ``features`` array inside it is
|
|
692
|
+
replaced together with the rest of the table instead of being orphaned.
|
|
693
|
+
|
|
694
|
+
:param content: Full pyproject.toml text
|
|
695
|
+
:param block: Rendered replacement table, newline-terminated
|
|
696
|
+
:return: Updated content (unchanged when no section is present)
|
|
697
|
+
"""
|
|
698
|
+
lines = content.splitlines(keepends=True)
|
|
699
|
+
|
|
700
|
+
start = next(
|
|
701
|
+
(
|
|
702
|
+
index
|
|
703
|
+
for index, line in enumerate(lines)
|
|
704
|
+
if _FASTKIT_SECTION_HEADER_PATTERN.match(line.strip())
|
|
705
|
+
),
|
|
706
|
+
None,
|
|
707
|
+
)
|
|
708
|
+
if start is None:
|
|
709
|
+
return content
|
|
710
|
+
|
|
711
|
+
end = len(lines)
|
|
712
|
+
for index in range(start + 1, len(lines)):
|
|
713
|
+
if _TOML_TABLE_HEADER_PATTERN.match(lines[index].strip()):
|
|
714
|
+
end = index
|
|
715
|
+
break
|
|
716
|
+
|
|
717
|
+
# Blank lines between this table and the next one belong to the file's
|
|
718
|
+
# formatting, not to the table, so leave them in place.
|
|
719
|
+
while end - 1 > start and not lines[end - 1].strip():
|
|
720
|
+
end -= 1
|
|
721
|
+
|
|
722
|
+
return "".join(lines[:start]) + block + "".join(lines[end:])
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# Order the ``[tool.fastapi-fastkit]`` keys are emitted in. Stable ordering
|
|
726
|
+
# keeps generated pyproject diffs readable and matches the documented
|
|
727
|
+
# metadata contract.
|
|
728
|
+
_FASTKIT_METADATA_KEY_ORDER = (
|
|
729
|
+
"managed",
|
|
730
|
+
"version",
|
|
731
|
+
"template",
|
|
732
|
+
"preset",
|
|
733
|
+
"package_manager",
|
|
734
|
+
"app_module",
|
|
735
|
+
"features",
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def build_fastkit_metadata_block(metadata: Dict[str, Any]) -> str:
|
|
740
|
+
"""
|
|
741
|
+
Render a ``[tool.fastapi-fastkit]`` TOML table from a metadata mapping.
|
|
742
|
+
|
|
743
|
+
``managed = true`` is always emitted so project detection keeps working
|
|
744
|
+
even when a caller passes a partial mapping. Keys with a ``None`` value
|
|
745
|
+
are omitted (that is how optional entries such as ``preset`` disappear
|
|
746
|
+
for non-preset templates).
|
|
747
|
+
|
|
748
|
+
:param metadata: Metadata mapping (see the project metadata contract)
|
|
749
|
+
:return: TOML table text, newline-terminated
|
|
750
|
+
"""
|
|
751
|
+
values: Dict[str, Any] = {"managed": True}
|
|
752
|
+
values.update({k: v for k, v in metadata.items() if v is not None})
|
|
753
|
+
|
|
754
|
+
ordered_keys = [k for k in _FASTKIT_METADATA_KEY_ORDER if k in values]
|
|
755
|
+
ordered_keys += [k for k in values if k not in _FASTKIT_METADATA_KEY_ORDER]
|
|
756
|
+
|
|
757
|
+
lines = [f"[tool.{FASTKIT_TOOL_SECTION}]"]
|
|
758
|
+
lines += [f"{key} = {format_toml_value(values[key])}" for key in ordered_keys]
|
|
759
|
+
return "\n".join(lines) + "\n"
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _ensure_pyproject_fastkit_markers(
|
|
763
|
+
content: str, metadata: Optional[Dict[str, Any]] = None
|
|
764
|
+
) -> str:
|
|
765
|
+
"""Idempotently ensure pyproject content carries FastAPI-fastkit markers.
|
|
766
|
+
|
|
767
|
+
Adds the canonical description marker if the ``description`` field does
|
|
768
|
+
not already include it (case-insensitive check), and writes the
|
|
769
|
+
``[tool.fastapi-fastkit]`` section. Without ``metadata`` the section is
|
|
770
|
+
only created when missing (``managed = true``); with ``metadata`` any
|
|
771
|
+
existing section is replaced so re-running generation refreshes the
|
|
772
|
+
recorded template / preset / app module rather than duplicating it.
|
|
773
|
+
"""
|
|
774
|
+
marker = FASTKIT_DESCRIPTION_MARKER
|
|
775
|
+
match = _DESCRIPTION_LINE_PATTERN.search(content)
|
|
776
|
+
if match and marker.lower() not in match.group("value").lower():
|
|
777
|
+
existing = match.group("value")
|
|
778
|
+
new_value = f"{marker} {existing}".strip() if existing else marker
|
|
779
|
+
content = (
|
|
780
|
+
content[: match.start()]
|
|
781
|
+
+ f'{match.group("prefix")}"{new_value}"{match.group("suffix")}'
|
|
782
|
+
+ content[match.end() :]
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
tool_header = f"[tool.{FASTKIT_TOOL_SECTION}]"
|
|
786
|
+
has_section = tool_header in content
|
|
787
|
+
|
|
788
|
+
if metadata is None:
|
|
789
|
+
if not has_section:
|
|
790
|
+
if not content.endswith("\n"):
|
|
791
|
+
content += "\n"
|
|
792
|
+
content += f"\n{tool_header}\nmanaged = true\n"
|
|
793
|
+
return content
|
|
794
|
+
|
|
795
|
+
block = build_fastkit_metadata_block(metadata)
|
|
796
|
+
|
|
797
|
+
if has_section:
|
|
798
|
+
return _replace_fastkit_section(content, block)
|
|
799
|
+
|
|
800
|
+
if not content.endswith("\n"):
|
|
801
|
+
content += "\n"
|
|
802
|
+
return content + "\n" + block
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def write_fastkit_metadata(project_dir: str, metadata: Dict[str, Any]) -> None:
|
|
806
|
+
"""
|
|
807
|
+
Write the ``[tool.fastapi-fastkit]`` block into a project's pyproject.toml.
|
|
808
|
+
|
|
809
|
+
Called at the very end of scaffolding: package managers regenerate
|
|
810
|
+
``pyproject.toml`` while resolving dependencies, so the metadata block
|
|
811
|
+
has to be (re)stamped after that step rather than during the initial
|
|
812
|
+
placeholder substitution.
|
|
813
|
+
|
|
814
|
+
:param project_dir: Path to the generated project directory
|
|
815
|
+
:param metadata: Metadata mapping (see the project metadata contract)
|
|
816
|
+
"""
|
|
817
|
+
pyproject_path = find_template_core_modules(project_dir).get("pyproject", "")
|
|
818
|
+
if not pyproject_path:
|
|
819
|
+
pyproject_path = os.path.join(project_dir, "pyproject.toml")
|
|
820
|
+
|
|
821
|
+
if not os.path.exists(pyproject_path):
|
|
822
|
+
debug_log("No pyproject.toml found; skipping fastkit metadata write", "warning")
|
|
823
|
+
return
|
|
824
|
+
|
|
825
|
+
try:
|
|
826
|
+
with open(pyproject_path, "r", encoding="utf-8") as f:
|
|
827
|
+
content = f.read()
|
|
828
|
+
|
|
829
|
+
content = _ensure_pyproject_fastkit_markers(content, metadata)
|
|
830
|
+
|
|
831
|
+
with open(pyproject_path, "w", encoding="utf-8") as f:
|
|
832
|
+
f.write(content)
|
|
833
|
+
|
|
834
|
+
debug_log("Wrote FastAPI-fastkit metadata into pyproject.toml", "info")
|
|
835
|
+
|
|
836
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
837
|
+
debug_log(f"Error writing fastkit metadata: {e}", "error")
|
|
838
|
+
print_warning(f"Could not write fastkit metadata: {e}")
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _render_dependency_array(dependencies: List[str]) -> str:
|
|
842
|
+
"""Render a PEP 621 ``dependencies`` array as multi-line TOML."""
|
|
843
|
+
if not dependencies:
|
|
844
|
+
return "[]"
|
|
845
|
+
body = "\n".join(f' "{dep}",' for dep in dependencies)
|
|
846
|
+
return f"[\n{body}\n]"
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _update_pyproject_dependencies(
|
|
850
|
+
pyproject_path: str, dependencies: List[str]
|
|
851
|
+
) -> bool:
|
|
852
|
+
"""
|
|
853
|
+
Replace ``[project].dependencies`` in a pyproject.toml file.
|
|
854
|
+
|
|
855
|
+
The array is located by bracket matching rather than a lazy regex so
|
|
856
|
+
requirement strings carrying extras (``redis[hiredis]``) don't truncate
|
|
857
|
+
the match.
|
|
858
|
+
|
|
859
|
+
:param pyproject_path: Path to the project's pyproject.toml
|
|
860
|
+
:param dependencies: Dependency specifications to write
|
|
861
|
+
:return: True when the file was rewritten, False when no array was found
|
|
862
|
+
"""
|
|
863
|
+
try:
|
|
864
|
+
with open(pyproject_path, "r", encoding="utf-8") as f:
|
|
865
|
+
content = f.read()
|
|
866
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
867
|
+
debug_log(f"Error reading pyproject.toml dependencies: {e}", "error")
|
|
868
|
+
return False
|
|
869
|
+
|
|
870
|
+
project_match = re.search(r"^\[project\]\s*$", content, re.MULTILINE)
|
|
871
|
+
if not project_match:
|
|
872
|
+
return False
|
|
873
|
+
|
|
874
|
+
section_start = project_match.end()
|
|
875
|
+
next_table = re.search(r"^\[", content[section_start:], re.MULTILINE)
|
|
876
|
+
section_end = section_start + next_table.start() if next_table else len(content)
|
|
877
|
+
section = content[section_start:section_end]
|
|
878
|
+
|
|
879
|
+
deps_match = re.search(r"^dependencies\s*=\s*\[", section, re.MULTILINE)
|
|
880
|
+
if not deps_match:
|
|
881
|
+
return False
|
|
882
|
+
|
|
883
|
+
array_start = section.rindex("[", deps_match.start(), deps_match.end())
|
|
884
|
+
depth = 0
|
|
885
|
+
array_end = -1
|
|
886
|
+
for index in range(array_start, len(section)):
|
|
887
|
+
char = section[index]
|
|
888
|
+
if char == "[":
|
|
889
|
+
depth += 1
|
|
890
|
+
elif char == "]":
|
|
891
|
+
depth -= 1
|
|
892
|
+
if depth == 0:
|
|
893
|
+
array_end = index + 1
|
|
894
|
+
break
|
|
895
|
+
if array_end == -1:
|
|
896
|
+
return False
|
|
897
|
+
|
|
898
|
+
new_section = (
|
|
899
|
+
section[: deps_match.start()]
|
|
900
|
+
+ f"dependencies = {_render_dependency_array(dependencies)}"
|
|
901
|
+
+ section[array_end:]
|
|
902
|
+
)
|
|
903
|
+
new_content = content[:section_start] + new_section + content[section_end:]
|
|
904
|
+
|
|
905
|
+
try:
|
|
906
|
+
with open(pyproject_path, "w", encoding="utf-8") as f:
|
|
907
|
+
f.write(new_content)
|
|
908
|
+
except OSError as e:
|
|
909
|
+
debug_log(f"Error writing pyproject.toml dependencies: {e}", "error")
|
|
910
|
+
return False
|
|
911
|
+
|
|
912
|
+
return True
|
|
913
|
+
|
|
914
|
+
|
|
346
915
|
def update_setup_py_dependencies(project_dir: str, dependencies: List[str]) -> None:
|
|
347
916
|
"""
|
|
348
|
-
Update
|
|
917
|
+
Update the project's declared dependencies with the selected packages.
|
|
349
918
|
|
|
350
|
-
|
|
351
|
-
|
|
919
|
+
``pyproject.toml``'s ``[project].dependencies`` is the primary target —
|
|
920
|
+
every current template ships one, and it is what every supported package
|
|
921
|
+
manager installs from. ``setup.py`` is still updated when a template
|
|
922
|
+
happens to ship one, purely as a legacy path.
|
|
923
|
+
|
|
924
|
+
The function name is kept for backwards compatibility with existing
|
|
925
|
+
callers and tests.
|
|
352
926
|
|
|
353
927
|
:param project_dir: Path to the project directory
|
|
354
928
|
:param dependencies: List of dependency specifications
|
|
355
929
|
"""
|
|
356
|
-
|
|
930
|
+
updated_any = False
|
|
357
931
|
|
|
932
|
+
pyproject_path = find_template_core_modules(project_dir).get("pyproject", "")
|
|
933
|
+
if pyproject_path and os.path.exists(pyproject_path):
|
|
934
|
+
if _update_pyproject_dependencies(pyproject_path, dependencies):
|
|
935
|
+
updated_any = True
|
|
936
|
+
print_info(f"Updated pyproject.toml with {len(dependencies)} dependencies")
|
|
937
|
+
else:
|
|
938
|
+
debug_log(
|
|
939
|
+
"Could not find [project].dependencies in pyproject.toml", "warning"
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
setup_py_path = os.path.join(project_dir, "setup.py")
|
|
358
943
|
if not os.path.exists(setup_py_path):
|
|
359
|
-
|
|
944
|
+
if not updated_any:
|
|
945
|
+
debug_log(
|
|
946
|
+
"No pyproject.toml/setup.py dependency table found, skipping update",
|
|
947
|
+
"info",
|
|
948
|
+
)
|
|
360
949
|
return
|
|
361
950
|
|
|
362
951
|
try:
|
|
@@ -527,6 +1116,250 @@ def generate_dependency_file_with_manager(
|
|
|
527
1116
|
# ------------------------------------------------------------
|
|
528
1117
|
|
|
529
1118
|
|
|
1119
|
+
def resolve_project_layout(project_dir: str) -> Dict[str, str]:
|
|
1120
|
+
"""
|
|
1121
|
+
Resolve where a generated project keeps its entrypoint and API router.
|
|
1122
|
+
|
|
1123
|
+
Replaces the old ``src/main.py`` assumption. Resolution order:
|
|
1124
|
+
|
|
1125
|
+
1. ``[tool.fastapi-fastkit].app_module`` in ``pyproject.toml`` — written
|
|
1126
|
+
at generation time, so it is authoritative for every fastkit project.
|
|
1127
|
+
2. On-disk discovery via :func:`find_template_core_modules`.
|
|
1128
|
+
3. A ``src/`` fallback so hand-made projects behave as they used to.
|
|
1129
|
+
|
|
1130
|
+
:param project_dir: Path to the project directory
|
|
1131
|
+
:return: Mapping with ``main``, ``app_module``, ``package_dir``,
|
|
1132
|
+
``package_module``, ``api_dir``, ``api_router_file`` and
|
|
1133
|
+
``api_router_module`` entries (``main`` is empty when no entrypoint
|
|
1134
|
+
could be found).
|
|
1135
|
+
"""
|
|
1136
|
+
metadata = read_fastkit_metadata(project_dir)
|
|
1137
|
+
|
|
1138
|
+
main_path = ""
|
|
1139
|
+
recorded_module = metadata.get("app_module")
|
|
1140
|
+
if isinstance(recorded_module, str) and recorded_module:
|
|
1141
|
+
module_path = recorded_module.split(":")[0].replace(".", os.sep)
|
|
1142
|
+
candidate = os.path.join(project_dir, f"{module_path}.py")
|
|
1143
|
+
if os.path.exists(candidate):
|
|
1144
|
+
main_path = candidate
|
|
1145
|
+
|
|
1146
|
+
if not main_path:
|
|
1147
|
+
main_path = find_template_core_modules(project_dir).get("main", "")
|
|
1148
|
+
|
|
1149
|
+
if main_path:
|
|
1150
|
+
package_dir = os.path.dirname(main_path)
|
|
1151
|
+
app_module = app_module_from_main_path(project_dir, main_path)
|
|
1152
|
+
else:
|
|
1153
|
+
# Nothing discovered — keep the historical ``src/`` assumption so the
|
|
1154
|
+
# caller still gets a usable structure to create files in.
|
|
1155
|
+
package_dir = os.path.join(project_dir, "src")
|
|
1156
|
+
app_module = "src.main:app"
|
|
1157
|
+
|
|
1158
|
+
package_module = _dotted_module(project_dir, package_dir)
|
|
1159
|
+
api_dir = os.path.join(package_dir, "api")
|
|
1160
|
+
api_router_file = _find_api_router_file(api_dir)
|
|
1161
|
+
stem = os.path.splitext(os.path.basename(api_router_file))[0]
|
|
1162
|
+
api_router_module = (
|
|
1163
|
+
f"{package_module}.api"
|
|
1164
|
+
if stem == "__init__"
|
|
1165
|
+
else f"{package_module}.api.{stem}"
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
return {
|
|
1169
|
+
"main": main_path,
|
|
1170
|
+
"app_module": app_module,
|
|
1171
|
+
"package_dir": package_dir,
|
|
1172
|
+
"package_module": package_module,
|
|
1173
|
+
"api_dir": api_dir,
|
|
1174
|
+
"api_router_file": api_router_file,
|
|
1175
|
+
"api_router_module": api_router_module,
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _dotted_module(project_dir: str, path: str) -> str:
|
|
1180
|
+
"""Convert a path inside the project into a dotted module prefix."""
|
|
1181
|
+
relative = os.path.relpath(path, project_dir)
|
|
1182
|
+
if relative in (".", ""):
|
|
1183
|
+
return ""
|
|
1184
|
+
return relative.replace(os.sep, ".")
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
# Router module filenames templates use, in precedence order. ``api.py`` is
|
|
1188
|
+
# the classic-layered name, ``router.py`` the domain-starter one.
|
|
1189
|
+
_API_ROUTER_FILENAMES = ("api.py", "router.py", "__init__.py")
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _find_api_router_file(api_dir: str) -> str:
|
|
1193
|
+
"""
|
|
1194
|
+
Pick the file inside ``api/`` that declares ``api_router``.
|
|
1195
|
+
|
|
1196
|
+
Falls back to ``api.py`` (the file ``addroute`` creates from the module
|
|
1197
|
+
template) when the package has no router file yet.
|
|
1198
|
+
"""
|
|
1199
|
+
for filename in _API_ROUTER_FILENAMES:
|
|
1200
|
+
candidate = os.path.join(api_dir, filename)
|
|
1201
|
+
if not os.path.exists(candidate):
|
|
1202
|
+
continue
|
|
1203
|
+
try:
|
|
1204
|
+
with open(candidate, "r", encoding="utf-8") as f:
|
|
1205
|
+
content = f.read()
|
|
1206
|
+
except (OSError, UnicodeDecodeError):
|
|
1207
|
+
continue
|
|
1208
|
+
if "api_router" in content:
|
|
1209
|
+
return candidate
|
|
1210
|
+
|
|
1211
|
+
return os.path.join(api_dir, "api.py")
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def insert_import_line(content: str, import_line: str) -> str:
|
|
1215
|
+
"""
|
|
1216
|
+
Insert an import statement using the ``# fastkit:imports`` anchor.
|
|
1217
|
+
|
|
1218
|
+
Templates declare the anchor to say exactly where generated imports
|
|
1219
|
+
belong. Without one the insertion point is computed from the module's
|
|
1220
|
+
AST (right after the last top-level import), which is precise where the
|
|
1221
|
+
previous line-prefix scan was merely approximate.
|
|
1222
|
+
|
|
1223
|
+
:param content: Source of the module being edited
|
|
1224
|
+
:param import_line: Import statement to add
|
|
1225
|
+
:return: Updated source (unchanged if the import is already present)
|
|
1226
|
+
"""
|
|
1227
|
+
if import_line in content:
|
|
1228
|
+
return content
|
|
1229
|
+
|
|
1230
|
+
anchored = _insert_after_anchor(content, IMPORT_ANCHOR, import_line)
|
|
1231
|
+
if anchored is not None:
|
|
1232
|
+
return anchored
|
|
1233
|
+
|
|
1234
|
+
lines = content.split("\n")
|
|
1235
|
+
insert_at = _last_import_line(content)
|
|
1236
|
+
lines.insert(insert_at, import_line)
|
|
1237
|
+
return "\n".join(lines)
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def insert_statement_line(
|
|
1241
|
+
content: str, statement: str, anchor: str = ROUTES_ANCHOR
|
|
1242
|
+
) -> str:
|
|
1243
|
+
"""
|
|
1244
|
+
Insert a statement at the ``# fastkit:routes`` anchor, else append it.
|
|
1245
|
+
|
|
1246
|
+
:param content: Source of the module being edited
|
|
1247
|
+
:param statement: Statement to add
|
|
1248
|
+
:param anchor: Anchor comment to look for
|
|
1249
|
+
:return: Updated source (unchanged if the statement is already present)
|
|
1250
|
+
"""
|
|
1251
|
+
if statement in content:
|
|
1252
|
+
return content
|
|
1253
|
+
|
|
1254
|
+
anchored = _insert_after_anchor(content, anchor, statement)
|
|
1255
|
+
if anchored is not None:
|
|
1256
|
+
return anchored
|
|
1257
|
+
|
|
1258
|
+
if content and not content.endswith("\n"):
|
|
1259
|
+
content += "\n"
|
|
1260
|
+
return f"{content}{statement}\n"
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _anchor_line_numbers(content: str, anchor: str) -> List[int]:
|
|
1264
|
+
"""Return the 1-based line numbers carrying ``anchor`` as a real comment.
|
|
1265
|
+
|
|
1266
|
+
Tokenising is what separates an anchor from a string that merely *looks*
|
|
1267
|
+
like one: a docstring or a ``"# fastkit:routes"`` literal must not be
|
|
1268
|
+
treated as an insertion point, because inserting a router registration
|
|
1269
|
+
inside a string silently produces a module that no longer registers it.
|
|
1270
|
+
Unparsable sources fall back to a plain scan so a template with a syntax
|
|
1271
|
+
error still behaves as it did before.
|
|
1272
|
+
"""
|
|
1273
|
+
try:
|
|
1274
|
+
tokens = list(tokenize.generate_tokens(io.StringIO(content).readline))
|
|
1275
|
+
except (tokenize.TokenError, IndentationError, SyntaxError):
|
|
1276
|
+
return [
|
|
1277
|
+
index
|
|
1278
|
+
for index, line in enumerate(content.split("\n"), start=1)
|
|
1279
|
+
if line.strip().startswith(anchor)
|
|
1280
|
+
]
|
|
1281
|
+
|
|
1282
|
+
return [
|
|
1283
|
+
token.start[0]
|
|
1284
|
+
for token in tokens
|
|
1285
|
+
if token.type == tokenize.COMMENT and token.string.strip().startswith(anchor)
|
|
1286
|
+
]
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def _insert_after_anchor(content: str, anchor: str, line: str) -> Optional[str]:
|
|
1290
|
+
"""Insert ``line`` right below ``anchor``, preserving the anchor's indent.
|
|
1291
|
+
|
|
1292
|
+
When a module declares the same anchor more than once the first one wins
|
|
1293
|
+
and the duplicates are reported: silently picking one of several
|
|
1294
|
+
insertion points makes generated code land somewhere the template author
|
|
1295
|
+
did not intend.
|
|
1296
|
+
|
|
1297
|
+
Returns None when the anchor isn't present so callers can fall back.
|
|
1298
|
+
"""
|
|
1299
|
+
anchor_lines = _anchor_line_numbers(content, anchor)
|
|
1300
|
+
if not anchor_lines:
|
|
1301
|
+
return None
|
|
1302
|
+
|
|
1303
|
+
if len(anchor_lines) > 1:
|
|
1304
|
+
print_warning(
|
|
1305
|
+
f"Found {len(anchor_lines)} '{anchor}' anchors "
|
|
1306
|
+
f"(lines {', '.join(str(n) for n in anchor_lines)}); "
|
|
1307
|
+
"inserting at the first one.",
|
|
1308
|
+
title="Duplicate anchor",
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
lines = content.split("\n")
|
|
1312
|
+
index = anchor_lines[0] - 1
|
|
1313
|
+
existing = lines[index]
|
|
1314
|
+
indent = existing[: len(existing) - len(existing.lstrip())]
|
|
1315
|
+
lines.insert(index + 1, f"{indent}{line}")
|
|
1316
|
+
return "\n".join(lines)
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _last_import_line(content: str) -> int:
|
|
1320
|
+
"""
|
|
1321
|
+
Return the 0-based line index just after the last top-level import.
|
|
1322
|
+
|
|
1323
|
+
Uses ``ast`` so multi-line ``from x import (a, b)`` statements and
|
|
1324
|
+
imports appearing inside strings or functions are handled correctly.
|
|
1325
|
+
|
|
1326
|
+
A module with no imports at all still has places an import may not go:
|
|
1327
|
+
a new import inserted at line 0 would land above the module docstring
|
|
1328
|
+
(demoting it to a plain expression) or above a ``from __future__``
|
|
1329
|
+
statement (which must stay first). So the fallback point is just after
|
|
1330
|
+
the docstring and any ``__future__`` imports.
|
|
1331
|
+
"""
|
|
1332
|
+
try:
|
|
1333
|
+
tree = ast.parse(content)
|
|
1334
|
+
except SyntaxError:
|
|
1335
|
+
return 0
|
|
1336
|
+
|
|
1337
|
+
last_line = 0
|
|
1338
|
+
prelude_line = 0
|
|
1339
|
+
for node in tree.body:
|
|
1340
|
+
if isinstance(node, ast.ImportFrom) and node.module == "__future__":
|
|
1341
|
+
prelude_line = max(prelude_line, node.end_lineno or node.lineno)
|
|
1342
|
+
last_line = max(last_line, node.end_lineno or node.lineno)
|
|
1343
|
+
continue
|
|
1344
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
1345
|
+
last_line = max(last_line, node.end_lineno or node.lineno)
|
|
1346
|
+
|
|
1347
|
+
if last_line:
|
|
1348
|
+
return last_line
|
|
1349
|
+
|
|
1350
|
+
docstring_node = tree.body[0] if tree.body else None
|
|
1351
|
+
if (
|
|
1352
|
+
isinstance(docstring_node, ast.Expr)
|
|
1353
|
+
and isinstance(docstring_node.value, ast.Constant)
|
|
1354
|
+
and isinstance(docstring_node.value.value, str)
|
|
1355
|
+
):
|
|
1356
|
+
prelude_line = max(
|
|
1357
|
+
prelude_line, docstring_node.end_lineno or docstring_node.lineno
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
return prelude_line
|
|
1361
|
+
|
|
1362
|
+
|
|
530
1363
|
def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
|
|
531
1364
|
"""
|
|
532
1365
|
Ensure the project structure exists for adding a new route.
|
|
@@ -554,23 +1387,35 @@ def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
|
|
|
554
1387
|
# Create __init__.py if it doesn't exist
|
|
555
1388
|
init_file = os.path.join(dir_path, "__init__.py")
|
|
556
1389
|
if not os.path.exists(init_file):
|
|
557
|
-
with open(init_file, "w")
|
|
1390
|
+
with open(init_file, "w"):
|
|
558
1391
|
pass
|
|
559
1392
|
|
|
560
1393
|
return target_dirs
|
|
561
1394
|
|
|
562
1395
|
|
|
563
1396
|
def _create_route_files(
|
|
564
|
-
modules_dir: str,
|
|
1397
|
+
modules_dir: str,
|
|
1398
|
+
target_dirs: Dict[str, str],
|
|
1399
|
+
route_name: str,
|
|
1400
|
+
package_module: str = "src",
|
|
565
1401
|
) -> None:
|
|
566
1402
|
"""
|
|
567
1403
|
Create route files from templates.
|
|
568
1404
|
|
|
1405
|
+
The route template imports its crud/schemas siblings through the
|
|
1406
|
+
``<package_root>`` placeholder, so the generated imports follow the
|
|
1407
|
+
project's real package (``src`` for the classic layout, ``src.app`` for
|
|
1408
|
+
the domain-starter one) instead of a hardcoded ``src``.
|
|
1409
|
+
|
|
569
1410
|
:param modules_dir: Path to the modules directory
|
|
570
1411
|
:param target_dirs: Dictionary with paths to target directories
|
|
571
1412
|
:param route_name: Name of the route to create
|
|
1413
|
+
:param package_module: Dotted package holding ``api``/``crud``/``schemas``
|
|
572
1414
|
"""
|
|
573
|
-
replacements = {
|
|
1415
|
+
replacements = {
|
|
1416
|
+
"<new_route>": route_name,
|
|
1417
|
+
"<package_root>": package_module or "src",
|
|
1418
|
+
}
|
|
574
1419
|
module_types = ["api/routes", "crud", "schemas"]
|
|
575
1420
|
|
|
576
1421
|
for module_type in module_types:
|
|
@@ -595,7 +1440,10 @@ def _create_route_files(
|
|
|
595
1440
|
|
|
596
1441
|
|
|
597
1442
|
def _handle_api_router_file(
|
|
598
|
-
target_dirs: Dict[str, str],
|
|
1443
|
+
target_dirs: Dict[str, str],
|
|
1444
|
+
modules_dir: str,
|
|
1445
|
+
route_name: str,
|
|
1446
|
+
api_router_file: str = "",
|
|
599
1447
|
) -> None:
|
|
600
1448
|
"""
|
|
601
1449
|
Handle API router file creation or update.
|
|
@@ -603,12 +1451,15 @@ def _handle_api_router_file(
|
|
|
603
1451
|
:param target_dirs: Dictionary with paths to target directories
|
|
604
1452
|
:param modules_dir: Path to the modules directory
|
|
605
1453
|
:param route_name: Name of the route
|
|
1454
|
+
:param api_router_file: Router file resolved from the project layout.
|
|
1455
|
+
Defaults to ``<api dir>/api.py`` for callers that don't resolve one.
|
|
606
1456
|
"""
|
|
607
1457
|
api_dir = target_dirs["api"]
|
|
608
|
-
|
|
1458
|
+
if not api_router_file:
|
|
1459
|
+
api_router_file = os.path.join(api_dir, "api.py")
|
|
609
1460
|
|
|
610
1461
|
if not os.path.exists(api_router_file):
|
|
611
|
-
# Create
|
|
1462
|
+
# Create the router module if it doesn't exist
|
|
612
1463
|
api_source = os.path.join(modules_dir, "api", "__init__.py-tpl")
|
|
613
1464
|
if os.path.exists(api_source):
|
|
614
1465
|
copy_and_convert_template_file(api_source, api_router_file)
|
|
@@ -622,6 +1473,10 @@ def _update_api_router(api_router_file: str, route_name: str) -> None:
|
|
|
622
1473
|
"""
|
|
623
1474
|
Update API router file to include new route.
|
|
624
1475
|
|
|
1476
|
+
Insertion points come from the ``# fastkit:imports`` /
|
|
1477
|
+
``# fastkit:routes`` anchors when the template declares them, and from
|
|
1478
|
+
the module's AST otherwise.
|
|
1479
|
+
|
|
625
1480
|
:param api_router_file: Path to API router file
|
|
626
1481
|
:param route_name: Name of the route
|
|
627
1482
|
"""
|
|
@@ -629,46 +1484,20 @@ def _update_api_router(api_router_file: str, route_name: str) -> None:
|
|
|
629
1484
|
with open(api_router_file, "r", encoding="utf-8") as f:
|
|
630
1485
|
content = f.read()
|
|
631
1486
|
|
|
632
|
-
# Check if route is already included
|
|
633
1487
|
route_import = f"from .routes import {route_name}"
|
|
634
|
-
route_include =
|
|
1488
|
+
route_include = (
|
|
1489
|
+
f"api_router.include_router({route_name}.router, "
|
|
1490
|
+
f'prefix="/{route_name}", tags=["{route_name}"])'
|
|
1491
|
+
)
|
|
635
1492
|
|
|
636
|
-
if
|
|
1493
|
+
if (
|
|
1494
|
+
route_import in content
|
|
1495
|
+
and f"api_router.include_router({route_name}.router" in content
|
|
1496
|
+
):
|
|
637
1497
|
return # Already included
|
|
638
1498
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
# Find where to add the import
|
|
642
|
-
lines = content.split("\n")
|
|
643
|
-
last_import_idx = -1
|
|
644
|
-
for i, line in enumerate(lines):
|
|
645
|
-
if line.startswith("from .routes import") or line.startswith(
|
|
646
|
-
"from routes import"
|
|
647
|
-
):
|
|
648
|
-
last_import_idx = i
|
|
649
|
-
|
|
650
|
-
if last_import_idx >= 0:
|
|
651
|
-
lines.insert(last_import_idx + 1, route_import)
|
|
652
|
-
else:
|
|
653
|
-
# Add at the end of imports section
|
|
654
|
-
for i, line in enumerate(lines):
|
|
655
|
-
if (
|
|
656
|
-
not line.startswith("from")
|
|
657
|
-
and not line.startswith("import")
|
|
658
|
-
and line.strip()
|
|
659
|
-
):
|
|
660
|
-
lines.insert(i, route_import)
|
|
661
|
-
break
|
|
662
|
-
|
|
663
|
-
content = "\n".join(lines)
|
|
664
|
-
|
|
665
|
-
# Add router inclusion if not present
|
|
666
|
-
if route_include not in content:
|
|
667
|
-
# Add before the last line or at the end
|
|
668
|
-
if content.strip():
|
|
669
|
-
content += f'\napi_router.include_router({route_name}.router, prefix="/{route_name}", tags=["{route_name}"])\n'
|
|
670
|
-
else:
|
|
671
|
-
content = f'api_router.include_router({route_name}.router, prefix="/{route_name}", tags=["{route_name}"])\n'
|
|
1499
|
+
content = insert_import_line(content, route_import)
|
|
1500
|
+
content = insert_statement_line(content, route_include)
|
|
672
1501
|
|
|
673
1502
|
with open(api_router_file, "w", encoding="utf-8") as f:
|
|
674
1503
|
f.write(content)
|
|
@@ -703,15 +1532,28 @@ def _process_init_files(
|
|
|
703
1532
|
copy_and_convert_template_file(init_source, init_target)
|
|
704
1533
|
|
|
705
1534
|
|
|
706
|
-
def _update_main_app(
|
|
1535
|
+
def _update_main_app(
|
|
1536
|
+
src_dir: str,
|
|
1537
|
+
route_name: str,
|
|
1538
|
+
router_module: str = "",
|
|
1539
|
+
main_py_path: str = "",
|
|
1540
|
+
) -> None:
|
|
707
1541
|
"""
|
|
708
1542
|
Update the main application file to include the API router.
|
|
709
|
-
Adds the router import and include statement at the end of the file.
|
|
710
1543
|
|
|
711
|
-
:param src_dir:
|
|
1544
|
+
:param src_dir: Package directory holding ``main.py`` (``src`` or
|
|
1545
|
+
``src/app`` depending on the template layout)
|
|
712
1546
|
:param route_name: Name of the route to add
|
|
1547
|
+
:param router_module: Dotted module exposing ``api_router``. Derived from
|
|
1548
|
+
``src_dir`` when omitted, preserving the classic ``src.api.api``.
|
|
1549
|
+
:param main_py_path: Explicit entrypoint path; defaults to
|
|
1550
|
+
``<src_dir>/main.py``.
|
|
713
1551
|
"""
|
|
714
|
-
|
|
1552
|
+
if not main_py_path:
|
|
1553
|
+
main_py_path = os.path.join(src_dir, "main.py")
|
|
1554
|
+
if not router_module:
|
|
1555
|
+
router_module = f"{os.path.basename(src_dir)}.api.api"
|
|
1556
|
+
|
|
715
1557
|
if not os.path.exists(main_py_path):
|
|
716
1558
|
print_warning("main.py not found. Please manually add the API router.")
|
|
717
1559
|
return
|
|
@@ -721,7 +1563,7 @@ def _update_main_app(src_dir: str, route_name: str) -> None:
|
|
|
721
1563
|
main_content = f.read()
|
|
722
1564
|
|
|
723
1565
|
# Check if router is already imported or included
|
|
724
|
-
router_import = "from
|
|
1566
|
+
router_import = f"from {router_module} import api_router"
|
|
725
1567
|
router_include = "app.include_router(api_router"
|
|
726
1568
|
|
|
727
1569
|
if router_import in main_content and router_include in main_content:
|
|
@@ -735,30 +1577,12 @@ def _update_main_app(src_dir: str, route_name: str) -> None:
|
|
|
735
1577
|
)
|
|
736
1578
|
return
|
|
737
1579
|
|
|
738
|
-
|
|
739
|
-
if router_import not in main_content:
|
|
740
|
-
# Add import at the end of imports section or beginning of file
|
|
741
|
-
lines = main_content.split("\n")
|
|
742
|
-
|
|
743
|
-
# Find last import line
|
|
744
|
-
last_import_idx = -1
|
|
745
|
-
for i, line in enumerate(lines):
|
|
746
|
-
if line.startswith("import ") or line.startswith("from "):
|
|
747
|
-
last_import_idx = i
|
|
748
|
-
|
|
749
|
-
if last_import_idx >= 0:
|
|
750
|
-
# Insert after the last import
|
|
751
|
-
lines.insert(last_import_idx + 1, router_import)
|
|
752
|
-
else:
|
|
753
|
-
# Insert at the beginning
|
|
754
|
-
lines.insert(0, router_import)
|
|
1580
|
+
main_content = insert_import_line(main_content, router_import)
|
|
755
1581
|
|
|
756
|
-
main_content = "\n".join(lines)
|
|
757
|
-
|
|
758
|
-
# Add router inclusion if needed
|
|
759
1582
|
if router_include not in main_content:
|
|
760
|
-
|
|
761
|
-
|
|
1583
|
+
main_content = insert_statement_line(
|
|
1584
|
+
main_content, "app.include_router(api_router)"
|
|
1585
|
+
)
|
|
762
1586
|
|
|
763
1587
|
with open(main_py_path, "w", encoding="utf-8") as f:
|
|
764
1588
|
f.write(main_content)
|
|
@@ -774,6 +1598,10 @@ def add_new_route(project_dir: str, route_name: str) -> None:
|
|
|
774
1598
|
"""
|
|
775
1599
|
Add a new API route to an existing FastAPI project.
|
|
776
1600
|
|
|
1601
|
+
The target locations come from :func:`resolve_project_layout`, so the
|
|
1602
|
+
command works for both the classic ``src/main.py`` layout and the
|
|
1603
|
+
domain-starter's ``src/app/main.py``.
|
|
1604
|
+
|
|
777
1605
|
:param project_dir: Path to the project directory
|
|
778
1606
|
:param route_name: Name of the new route to add
|
|
779
1607
|
:raises BackendExceptions: If route addition fails
|
|
@@ -781,23 +1609,33 @@ def add_new_route(project_dir: str, route_name: str) -> None:
|
|
|
781
1609
|
try:
|
|
782
1610
|
# Setup paths
|
|
783
1611
|
modules_dir = os.path.join(settings.FASTKIT_TEMPLATE_ROOT, "modules")
|
|
784
|
-
|
|
1612
|
+
layout = resolve_project_layout(project_dir)
|
|
1613
|
+
src_dir = layout["package_dir"]
|
|
785
1614
|
|
|
786
1615
|
# Ensure project structure exists
|
|
787
1616
|
target_dirs = _ensure_project_structure(src_dir)
|
|
788
1617
|
|
|
789
1618
|
# Create route files
|
|
790
|
-
_create_route_files(
|
|
1619
|
+
_create_route_files(
|
|
1620
|
+
modules_dir, target_dirs, route_name, layout["package_module"]
|
|
1621
|
+
)
|
|
791
1622
|
|
|
792
1623
|
# Handle API router file
|
|
793
|
-
_handle_api_router_file(
|
|
1624
|
+
_handle_api_router_file(
|
|
1625
|
+
target_dirs, modules_dir, route_name, layout["api_router_file"]
|
|
1626
|
+
)
|
|
794
1627
|
|
|
795
1628
|
# Process init files
|
|
796
1629
|
module_types = ["api/routes", "crud", "schemas"]
|
|
797
1630
|
_process_init_files(modules_dir, target_dirs, module_types)
|
|
798
1631
|
|
|
799
1632
|
# Update main application
|
|
800
|
-
_update_main_app(
|
|
1633
|
+
_update_main_app(
|
|
1634
|
+
src_dir,
|
|
1635
|
+
route_name,
|
|
1636
|
+
router_module=layout["api_router_module"],
|
|
1637
|
+
main_py_path=layout["main"],
|
|
1638
|
+
)
|
|
801
1639
|
|
|
802
1640
|
debug_log(f"Successfully added new route: {route_name}", "info")
|
|
803
1641
|
print_success(f"Route '{route_name}' has been added successfully!")
|
|
@@ -835,9 +1673,9 @@ def ask_create_project_folder(project_name: str) -> bool:
|
|
|
835
1673
|
)
|
|
836
1674
|
|
|
837
1675
|
|
|
838
|
-
def
|
|
1676
|
+
def deploy_template_files(
|
|
839
1677
|
target_template: str, user_local: str, project_name: str, create_folder: bool
|
|
840
|
-
) -> tuple[str, str]:
|
|
1678
|
+
) -> tuple[str, str, List[str]]:
|
|
841
1679
|
"""
|
|
842
1680
|
Deploy template based on folder creation option.
|
|
843
1681
|
|
|
@@ -845,20 +1683,43 @@ def deploy_template_with_folder_option(
|
|
|
845
1683
|
:param user_local: User's workspace directory
|
|
846
1684
|
:param project_name: Name of the project
|
|
847
1685
|
:param create_folder: Whether to create a new folder
|
|
848
|
-
:return: Tuple of (project_dir, deployment_message)
|
|
1686
|
+
:return: Tuple of (project_dir, deployment_message, copied file paths). The
|
|
1687
|
+
file list lets callers keep later rewriting passes scoped to what this
|
|
1688
|
+
deployment actually wrote.
|
|
849
1689
|
"""
|
|
850
1690
|
if create_folder:
|
|
851
1691
|
project_dir = os.path.join(user_local, project_name)
|
|
852
1692
|
deployment_message = f"FastAPI template project will deploy at '{user_local}' in folder '{project_name}'"
|
|
853
|
-
copy_and_convert_template(target_template, user_local, project_name)
|
|
1693
|
+
copied = copy_and_convert_template(target_template, user_local, project_name)
|
|
854
1694
|
else:
|
|
855
1695
|
project_dir = user_local
|
|
856
1696
|
deployment_message = (
|
|
857
1697
|
f"FastAPI template project will deploy directly at '{user_local}'"
|
|
858
1698
|
)
|
|
859
|
-
copy_and_convert_template(target_template, user_local, "")
|
|
1699
|
+
copied = copy_and_convert_template(target_template, user_local, "")
|
|
860
1700
|
|
|
861
1701
|
click.echo(deployment_message)
|
|
1702
|
+
return project_dir, deployment_message, copied
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def deploy_template_with_folder_option(
|
|
1706
|
+
target_template: str, user_local: str, project_name: str, create_folder: bool
|
|
1707
|
+
) -> tuple[str, str]:
|
|
1708
|
+
"""
|
|
1709
|
+
Deploy template based on folder creation option.
|
|
1710
|
+
|
|
1711
|
+
Thin wrapper over :func:`deploy_template_files` kept for callers that do
|
|
1712
|
+
not need the list of copied files.
|
|
1713
|
+
|
|
1714
|
+
:param target_template: Path to template directory
|
|
1715
|
+
:param user_local: User's workspace directory
|
|
1716
|
+
:param project_name: Name of the project
|
|
1717
|
+
:param create_folder: Whether to create a new folder
|
|
1718
|
+
:return: Tuple of (project_dir, deployment_message)
|
|
1719
|
+
"""
|
|
1720
|
+
project_dir, deployment_message, _ = deploy_template_files(
|
|
1721
|
+
target_template, user_local, project_name, create_folder
|
|
1722
|
+
)
|
|
862
1723
|
return project_dir, deployment_message
|
|
863
1724
|
|
|
864
1725
|
|