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
fastapi_fastkit/backend/main.py
CHANGED
|
@@ -3,20 +3,28 @@
|
|
|
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
|
|
11
|
+
import tokenize
|
|
8
12
|
import tomllib
|
|
9
|
-
from typing import Dict, List
|
|
13
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
10
14
|
|
|
11
15
|
import click
|
|
12
16
|
|
|
13
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
|
+
)
|
|
14
21
|
from fastapi_fastkit.backend.transducer import (
|
|
15
22
|
copy_and_convert_template,
|
|
16
23
|
copy_and_convert_template_file,
|
|
17
24
|
)
|
|
18
25
|
from fastapi_fastkit.core.exceptions import BackendExceptions
|
|
19
26
|
from fastapi_fastkit.core.settings import settings
|
|
27
|
+
from fastapi_fastkit.utils.config_file import format_toml_value
|
|
20
28
|
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
21
29
|
from fastapi_fastkit.utils.main import (
|
|
22
30
|
FASTKIT_DESCRIPTION_MARKER,
|
|
@@ -25,8 +33,15 @@ from fastapi_fastkit.utils.main import (
|
|
|
25
33
|
print_info,
|
|
26
34
|
print_success,
|
|
27
35
|
print_warning,
|
|
36
|
+
read_fastkit_metadata,
|
|
28
37
|
)
|
|
29
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
|
+
|
|
30
45
|
logger = get_logger(__name__)
|
|
31
46
|
|
|
32
47
|
|
|
@@ -194,15 +209,258 @@ def _parse_pyproject_dependencies(pyproject_path: str) -> List[str]:
|
|
|
194
209
|
# ------------------------------------------------------------
|
|
195
210
|
|
|
196
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
|
+
|
|
197
444
|
def inject_project_metadata(
|
|
198
445
|
target_dir: str,
|
|
199
446
|
project_name: str,
|
|
200
447
|
author: str,
|
|
201
448
|
author_email: str,
|
|
202
449
|
description: str,
|
|
450
|
+
files: Optional[Iterable[str]] = None,
|
|
203
451
|
) -> None:
|
|
204
452
|
"""
|
|
205
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.
|
|
206
464
|
"""
|
|
207
465
|
try:
|
|
208
466
|
core_modules = find_template_core_modules(target_dir)
|
|
@@ -223,8 +481,31 @@ def inject_project_metadata(
|
|
|
223
481
|
_process_config_file(core_modules.get("config", ""), project_name)
|
|
224
482
|
_process_main_file(core_modules.get("main", ""), project_name)
|
|
225
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
|
+
|
|
226
505
|
print_success("Project metadata injected successfully")
|
|
227
506
|
|
|
507
|
+
except BackendExceptions:
|
|
508
|
+
raise
|
|
228
509
|
except Exception as e:
|
|
229
510
|
debug_log(f"Failed to inject project metadata: {e}", "error")
|
|
230
511
|
handle_exception(e, "Failed to inject project metadata")
|
|
@@ -258,8 +539,7 @@ def _process_setup_file(
|
|
|
258
539
|
"<description>": description,
|
|
259
540
|
}
|
|
260
541
|
|
|
261
|
-
|
|
262
|
-
content = content.replace(placeholder, value)
|
|
542
|
+
content = _apply_escaped_replacements(content, replacements, setup_py)
|
|
263
543
|
|
|
264
544
|
with open(setup_py, "w", encoding="utf-8") as f:
|
|
265
545
|
f.write(content)
|
|
@@ -293,7 +573,9 @@ def _process_main_file(main_py: str, project_name: str) -> None:
|
|
|
293
573
|
if "<project_name>" not in content:
|
|
294
574
|
return
|
|
295
575
|
|
|
296
|
-
content = content.replace(
|
|
576
|
+
content = content.replace(
|
|
577
|
+
"<project_name>", escape_placeholder_value(project_name, main_py)
|
|
578
|
+
)
|
|
297
579
|
|
|
298
580
|
with open(main_py, "w", encoding="utf-8") as f:
|
|
299
581
|
f.write(content)
|
|
@@ -321,7 +603,9 @@ def _process_config_file(config_py: str, project_name: str) -> None:
|
|
|
321
603
|
content = f.read()
|
|
322
604
|
|
|
323
605
|
# Replace project name placeholder
|
|
324
|
-
content = content.replace(
|
|
606
|
+
content = content.replace(
|
|
607
|
+
"<project_name>", escape_placeholder_value(project_name, config_py)
|
|
608
|
+
)
|
|
325
609
|
|
|
326
610
|
with open(config_py, "w", encoding="utf-8") as f:
|
|
327
611
|
f.write(content)
|
|
@@ -365,8 +649,7 @@ def _process_pyproject_file(
|
|
|
365
649
|
"<description>": description,
|
|
366
650
|
}
|
|
367
651
|
|
|
368
|
-
|
|
369
|
-
content = content.replace(placeholder, value)
|
|
652
|
+
content = _apply_escaped_replacements(content, replacements, pyproject_toml)
|
|
370
653
|
|
|
371
654
|
content = _ensure_pyproject_fastkit_markers(content)
|
|
372
655
|
|
|
@@ -387,14 +670,106 @@ _DESCRIPTION_LINE_PATTERN = re.compile(
|
|
|
387
670
|
)
|
|
388
671
|
|
|
389
672
|
|
|
390
|
-
|
|
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:
|
|
391
765
|
"""Idempotently ensure pyproject content carries FastAPI-fastkit markers.
|
|
392
766
|
|
|
393
767
|
Adds the canonical description marker if the ``description`` field does
|
|
394
|
-
not already include it (case-insensitive check), and
|
|
395
|
-
``[tool.fastapi-fastkit]`` section
|
|
396
|
-
|
|
397
|
-
|
|
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.
|
|
398
773
|
"""
|
|
399
774
|
marker = FASTKIT_DESCRIPTION_MARKER
|
|
400
775
|
match = _DESCRIPTION_LINE_PATTERN.search(content)
|
|
@@ -408,28 +783,169 @@ def _ensure_pyproject_fastkit_markers(content: str) -> str:
|
|
|
408
783
|
)
|
|
409
784
|
|
|
410
785
|
tool_header = f"[tool.{FASTKIT_TOOL_SECTION}]"
|
|
411
|
-
|
|
412
|
-
if not content.endswith("\n"):
|
|
413
|
-
content += "\n"
|
|
414
|
-
content += f"\n{tool_header}\nmanaged = true\n"
|
|
786
|
+
has_section = tool_header in content
|
|
415
787
|
|
|
416
|
-
|
|
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
|
|
417
913
|
|
|
418
914
|
|
|
419
915
|
def update_setup_py_dependencies(project_dir: str, dependencies: List[str]) -> None:
|
|
420
916
|
"""
|
|
421
|
-
Update
|
|
917
|
+
Update the project's declared dependencies with the selected packages.
|
|
422
918
|
|
|
423
|
-
|
|
424
|
-
|
|
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.
|
|
425
926
|
|
|
426
927
|
:param project_dir: Path to the project directory
|
|
427
928
|
:param dependencies: List of dependency specifications
|
|
428
929
|
"""
|
|
429
|
-
|
|
930
|
+
updated_any = False
|
|
430
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")
|
|
431
943
|
if not os.path.exists(setup_py_path):
|
|
432
|
-
|
|
944
|
+
if not updated_any:
|
|
945
|
+
debug_log(
|
|
946
|
+
"No pyproject.toml/setup.py dependency table found, skipping update",
|
|
947
|
+
"info",
|
|
948
|
+
)
|
|
433
949
|
return
|
|
434
950
|
|
|
435
951
|
try:
|
|
@@ -600,6 +1116,250 @@ def generate_dependency_file_with_manager(
|
|
|
600
1116
|
# ------------------------------------------------------------
|
|
601
1117
|
|
|
602
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
|
+
|
|
603
1363
|
def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
|
|
604
1364
|
"""
|
|
605
1365
|
Ensure the project structure exists for adding a new route.
|
|
@@ -634,16 +1394,28 @@ def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
|
|
|
634
1394
|
|
|
635
1395
|
|
|
636
1396
|
def _create_route_files(
|
|
637
|
-
modules_dir: str,
|
|
1397
|
+
modules_dir: str,
|
|
1398
|
+
target_dirs: Dict[str, str],
|
|
1399
|
+
route_name: str,
|
|
1400
|
+
package_module: str = "src",
|
|
638
1401
|
) -> None:
|
|
639
1402
|
"""
|
|
640
1403
|
Create route files from templates.
|
|
641
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
|
+
|
|
642
1410
|
:param modules_dir: Path to the modules directory
|
|
643
1411
|
:param target_dirs: Dictionary with paths to target directories
|
|
644
1412
|
:param route_name: Name of the route to create
|
|
1413
|
+
:param package_module: Dotted package holding ``api``/``crud``/``schemas``
|
|
645
1414
|
"""
|
|
646
|
-
replacements = {
|
|
1415
|
+
replacements = {
|
|
1416
|
+
"<new_route>": route_name,
|
|
1417
|
+
"<package_root>": package_module or "src",
|
|
1418
|
+
}
|
|
647
1419
|
module_types = ["api/routes", "crud", "schemas"]
|
|
648
1420
|
|
|
649
1421
|
for module_type in module_types:
|
|
@@ -668,7 +1440,10 @@ def _create_route_files(
|
|
|
668
1440
|
|
|
669
1441
|
|
|
670
1442
|
def _handle_api_router_file(
|
|
671
|
-
target_dirs: Dict[str, str],
|
|
1443
|
+
target_dirs: Dict[str, str],
|
|
1444
|
+
modules_dir: str,
|
|
1445
|
+
route_name: str,
|
|
1446
|
+
api_router_file: str = "",
|
|
672
1447
|
) -> None:
|
|
673
1448
|
"""
|
|
674
1449
|
Handle API router file creation or update.
|
|
@@ -676,12 +1451,15 @@ def _handle_api_router_file(
|
|
|
676
1451
|
:param target_dirs: Dictionary with paths to target directories
|
|
677
1452
|
:param modules_dir: Path to the modules directory
|
|
678
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.
|
|
679
1456
|
"""
|
|
680
1457
|
api_dir = target_dirs["api"]
|
|
681
|
-
|
|
1458
|
+
if not api_router_file:
|
|
1459
|
+
api_router_file = os.path.join(api_dir, "api.py")
|
|
682
1460
|
|
|
683
1461
|
if not os.path.exists(api_router_file):
|
|
684
|
-
# Create
|
|
1462
|
+
# Create the router module if it doesn't exist
|
|
685
1463
|
api_source = os.path.join(modules_dir, "api", "__init__.py-tpl")
|
|
686
1464
|
if os.path.exists(api_source):
|
|
687
1465
|
copy_and_convert_template_file(api_source, api_router_file)
|
|
@@ -695,6 +1473,10 @@ def _update_api_router(api_router_file: str, route_name: str) -> None:
|
|
|
695
1473
|
"""
|
|
696
1474
|
Update API router file to include new route.
|
|
697
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
|
+
|
|
698
1480
|
:param api_router_file: Path to API router file
|
|
699
1481
|
:param route_name: Name of the route
|
|
700
1482
|
"""
|
|
@@ -702,46 +1484,20 @@ def _update_api_router(api_router_file: str, route_name: str) -> None:
|
|
|
702
1484
|
with open(api_router_file, "r", encoding="utf-8") as f:
|
|
703
1485
|
content = f.read()
|
|
704
1486
|
|
|
705
|
-
# Check if route is already included
|
|
706
1487
|
route_import = f"from .routes import {route_name}"
|
|
707
|
-
route_include =
|
|
1488
|
+
route_include = (
|
|
1489
|
+
f"api_router.include_router({route_name}.router, "
|
|
1490
|
+
f'prefix="/{route_name}", tags=["{route_name}"])'
|
|
1491
|
+
)
|
|
708
1492
|
|
|
709
|
-
if
|
|
1493
|
+
if (
|
|
1494
|
+
route_import in content
|
|
1495
|
+
and f"api_router.include_router({route_name}.router" in content
|
|
1496
|
+
):
|
|
710
1497
|
return # Already included
|
|
711
1498
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
# Find where to add the import
|
|
715
|
-
lines = content.split("\n")
|
|
716
|
-
last_import_idx = -1
|
|
717
|
-
for i, line in enumerate(lines):
|
|
718
|
-
if line.startswith("from .routes import") or line.startswith(
|
|
719
|
-
"from routes import"
|
|
720
|
-
):
|
|
721
|
-
last_import_idx = i
|
|
722
|
-
|
|
723
|
-
if last_import_idx >= 0:
|
|
724
|
-
lines.insert(last_import_idx + 1, route_import)
|
|
725
|
-
else:
|
|
726
|
-
# Add at the end of imports section
|
|
727
|
-
for i, line in enumerate(lines):
|
|
728
|
-
if (
|
|
729
|
-
not line.startswith("from")
|
|
730
|
-
and not line.startswith("import")
|
|
731
|
-
and line.strip()
|
|
732
|
-
):
|
|
733
|
-
lines.insert(i, route_import)
|
|
734
|
-
break
|
|
735
|
-
|
|
736
|
-
content = "\n".join(lines)
|
|
737
|
-
|
|
738
|
-
# Add router inclusion if not present
|
|
739
|
-
if route_include not in content:
|
|
740
|
-
# Add before the last line or at the end
|
|
741
|
-
if content.strip():
|
|
742
|
-
content += f'\napi_router.include_router({route_name}.router, prefix="/{route_name}", tags=["{route_name}"])\n'
|
|
743
|
-
else:
|
|
744
|
-
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)
|
|
745
1501
|
|
|
746
1502
|
with open(api_router_file, "w", encoding="utf-8") as f:
|
|
747
1503
|
f.write(content)
|
|
@@ -776,15 +1532,28 @@ def _process_init_files(
|
|
|
776
1532
|
copy_and_convert_template_file(init_source, init_target)
|
|
777
1533
|
|
|
778
1534
|
|
|
779
|
-
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:
|
|
780
1541
|
"""
|
|
781
1542
|
Update the main application file to include the API router.
|
|
782
|
-
Adds the router import and include statement at the end of the file.
|
|
783
1543
|
|
|
784
|
-
:param src_dir:
|
|
1544
|
+
:param src_dir: Package directory holding ``main.py`` (``src`` or
|
|
1545
|
+
``src/app`` depending on the template layout)
|
|
785
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``.
|
|
786
1551
|
"""
|
|
787
|
-
|
|
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
|
+
|
|
788
1557
|
if not os.path.exists(main_py_path):
|
|
789
1558
|
print_warning("main.py not found. Please manually add the API router.")
|
|
790
1559
|
return
|
|
@@ -794,7 +1563,7 @@ def _update_main_app(src_dir: str, route_name: str) -> None:
|
|
|
794
1563
|
main_content = f.read()
|
|
795
1564
|
|
|
796
1565
|
# Check if router is already imported or included
|
|
797
|
-
router_import = "from
|
|
1566
|
+
router_import = f"from {router_module} import api_router"
|
|
798
1567
|
router_include = "app.include_router(api_router"
|
|
799
1568
|
|
|
800
1569
|
if router_import in main_content and router_include in main_content:
|
|
@@ -808,30 +1577,12 @@ def _update_main_app(src_dir: str, route_name: str) -> None:
|
|
|
808
1577
|
)
|
|
809
1578
|
return
|
|
810
1579
|
|
|
811
|
-
|
|
812
|
-
if router_import not in main_content:
|
|
813
|
-
# Add import at the end of imports section or beginning of file
|
|
814
|
-
lines = main_content.split("\n")
|
|
815
|
-
|
|
816
|
-
# Find last import line
|
|
817
|
-
last_import_idx = -1
|
|
818
|
-
for i, line in enumerate(lines):
|
|
819
|
-
if line.startswith("import ") or line.startswith("from "):
|
|
820
|
-
last_import_idx = i
|
|
1580
|
+
main_content = insert_import_line(main_content, router_import)
|
|
821
1581
|
|
|
822
|
-
if last_import_idx >= 0:
|
|
823
|
-
# Insert after the last import
|
|
824
|
-
lines.insert(last_import_idx + 1, router_import)
|
|
825
|
-
else:
|
|
826
|
-
# Insert at the beginning
|
|
827
|
-
lines.insert(0, router_import)
|
|
828
|
-
|
|
829
|
-
main_content = "\n".join(lines)
|
|
830
|
-
|
|
831
|
-
# Add router inclusion if needed
|
|
832
1582
|
if router_include not in main_content:
|
|
833
|
-
|
|
834
|
-
|
|
1583
|
+
main_content = insert_statement_line(
|
|
1584
|
+
main_content, "app.include_router(api_router)"
|
|
1585
|
+
)
|
|
835
1586
|
|
|
836
1587
|
with open(main_py_path, "w", encoding="utf-8") as f:
|
|
837
1588
|
f.write(main_content)
|
|
@@ -847,6 +1598,10 @@ def add_new_route(project_dir: str, route_name: str) -> None:
|
|
|
847
1598
|
"""
|
|
848
1599
|
Add a new API route to an existing FastAPI project.
|
|
849
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
|
+
|
|
850
1605
|
:param project_dir: Path to the project directory
|
|
851
1606
|
:param route_name: Name of the new route to add
|
|
852
1607
|
:raises BackendExceptions: If route addition fails
|
|
@@ -854,23 +1609,33 @@ def add_new_route(project_dir: str, route_name: str) -> None:
|
|
|
854
1609
|
try:
|
|
855
1610
|
# Setup paths
|
|
856
1611
|
modules_dir = os.path.join(settings.FASTKIT_TEMPLATE_ROOT, "modules")
|
|
857
|
-
|
|
1612
|
+
layout = resolve_project_layout(project_dir)
|
|
1613
|
+
src_dir = layout["package_dir"]
|
|
858
1614
|
|
|
859
1615
|
# Ensure project structure exists
|
|
860
1616
|
target_dirs = _ensure_project_structure(src_dir)
|
|
861
1617
|
|
|
862
1618
|
# Create route files
|
|
863
|
-
_create_route_files(
|
|
1619
|
+
_create_route_files(
|
|
1620
|
+
modules_dir, target_dirs, route_name, layout["package_module"]
|
|
1621
|
+
)
|
|
864
1622
|
|
|
865
1623
|
# Handle API router file
|
|
866
|
-
_handle_api_router_file(
|
|
1624
|
+
_handle_api_router_file(
|
|
1625
|
+
target_dirs, modules_dir, route_name, layout["api_router_file"]
|
|
1626
|
+
)
|
|
867
1627
|
|
|
868
1628
|
# Process init files
|
|
869
1629
|
module_types = ["api/routes", "crud", "schemas"]
|
|
870
1630
|
_process_init_files(modules_dir, target_dirs, module_types)
|
|
871
1631
|
|
|
872
1632
|
# Update main application
|
|
873
|
-
_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
|
+
)
|
|
874
1639
|
|
|
875
1640
|
debug_log(f"Successfully added new route: {route_name}", "info")
|
|
876
1641
|
print_success(f"Route '{route_name}' has been added successfully!")
|
|
@@ -908,9 +1673,9 @@ def ask_create_project_folder(project_name: str) -> bool:
|
|
|
908
1673
|
)
|
|
909
1674
|
|
|
910
1675
|
|
|
911
|
-
def
|
|
1676
|
+
def deploy_template_files(
|
|
912
1677
|
target_template: str, user_local: str, project_name: str, create_folder: bool
|
|
913
|
-
) -> tuple[str, str]:
|
|
1678
|
+
) -> tuple[str, str, List[str]]:
|
|
914
1679
|
"""
|
|
915
1680
|
Deploy template based on folder creation option.
|
|
916
1681
|
|
|
@@ -918,20 +1683,43 @@ def deploy_template_with_folder_option(
|
|
|
918
1683
|
:param user_local: User's workspace directory
|
|
919
1684
|
:param project_name: Name of the project
|
|
920
1685
|
:param create_folder: Whether to create a new folder
|
|
921
|
-
: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.
|
|
922
1689
|
"""
|
|
923
1690
|
if create_folder:
|
|
924
1691
|
project_dir = os.path.join(user_local, project_name)
|
|
925
1692
|
deployment_message = f"FastAPI template project will deploy at '{user_local}' in folder '{project_name}'"
|
|
926
|
-
copy_and_convert_template(target_template, user_local, project_name)
|
|
1693
|
+
copied = copy_and_convert_template(target_template, user_local, project_name)
|
|
927
1694
|
else:
|
|
928
1695
|
project_dir = user_local
|
|
929
1696
|
deployment_message = (
|
|
930
1697
|
f"FastAPI template project will deploy directly at '{user_local}'"
|
|
931
1698
|
)
|
|
932
|
-
copy_and_convert_template(target_template, user_local, "")
|
|
1699
|
+
copied = copy_and_convert_template(target_template, user_local, "")
|
|
933
1700
|
|
|
934
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
|
+
)
|
|
935
1723
|
return project_dir, deployment_message
|
|
936
1724
|
|
|
937
1725
|
|