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.
Files changed (356) hide show
  1. fastapi_fastkit/__init__.py +1 -1
  2. fastapi_fastkit/backend/inspection/__init__.py +26 -0
  3. fastapi_fastkit/backend/inspection/checks.py +349 -0
  4. fastapi_fastkit/backend/inspection/consistency.py +125 -0
  5. fastapi_fastkit/backend/inspection/context.py +67 -0
  6. fastapi_fastkit/backend/inspection/core.py +347 -0
  7. fastapi_fastkit/backend/inspection/docker.py +185 -0
  8. fastapi_fastkit/backend/inspection/freshness.py +145 -0
  9. fastapi_fastkit/backend/inspection/fsutils.py +139 -0
  10. fastapi_fastkit/backend/inspection/lint.py +89 -0
  11. fastapi_fastkit/backend/inspection/report.py +58 -0
  12. fastapi_fastkit/backend/inspection/smoke.py +327 -0
  13. fastapi_fastkit/backend/inspection/strategies.py +336 -0
  14. fastapi_fastkit/backend/inspector.py +18 -1279
  15. fastapi_fastkit/backend/interactive/__init__.py +8 -0
  16. fastapi_fastkit/backend/interactive/config_builder.py +66 -65
  17. fastapi_fastkit/backend/interactive/prompts.py +205 -5
  18. fastapi_fastkit/backend/interactive/selectors.py +28 -2
  19. fastapi_fastkit/backend/main.py +956 -95
  20. fastapi_fastkit/backend/package_managers/base.py +172 -0
  21. fastapi_fastkit/backend/package_managers/factory.py +6 -5
  22. fastapi_fastkit/backend/package_managers/pdm_manager.py +32 -85
  23. fastapi_fastkit/backend/package_managers/pip_manager.py +31 -85
  24. fastapi_fastkit/backend/package_managers/poetry_manager.py +73 -113
  25. fastapi_fastkit/backend/package_managers/uv_manager.py +46 -115
  26. fastapi_fastkit/backend/project_builder/__init__.py +10 -0
  27. fastapi_fastkit/backend/project_builder/config_generator.py +813 -450
  28. fastapi_fastkit/backend/project_builder/config_schema.py +384 -0
  29. fastapi_fastkit/backend/project_builder/dependency_collector.py +105 -66
  30. fastapi_fastkit/backend/project_builder/preset_layout.py +429 -0
  31. fastapi_fastkit/backend/scaffolder.py +578 -0
  32. fastapi_fastkit/backend/transducer.py +54 -22
  33. fastapi_fastkit/cli.py +452 -284
  34. fastapi_fastkit/core/settings.py +248 -43
  35. fastapi_fastkit/fastapi_project_template/README.md +88 -23
  36. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +3 -0
  37. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +25 -23
  38. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +39 -36
  39. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +3 -0
  40. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +1 -2
  41. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +5 -6
  42. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +1 -2
  43. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +9 -1
  44. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +1 -2
  45. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +0 -1
  46. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/.env-tpl +21 -0
  47. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/.gitignore-tpl +33 -0
  48. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/Dockerfile-tpl +18 -0
  49. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl +210 -0
  50. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/alembic.ini-tpl +119 -0
  51. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/pyproject.toml-tpl +86 -0
  52. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/requirements.txt-tpl +23 -0
  53. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/format.sh-tpl +5 -0
  54. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/lint.sh-tpl +6 -0
  55. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/run-server.sh-tpl +8 -0
  56. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/scripts/test.sh-tpl +6 -0
  57. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/__init__.py-tpl +0 -0
  58. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/__init__.py-tpl +0 -0
  59. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/README-tpl +1 -0
  60. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/env.py-tpl +51 -0
  61. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/script.py.mako-tpl +26 -0
  62. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/alembic/versions/0001_initial_auth_schema.py-tpl +62 -0
  63. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/__init__.py-tpl +0 -0
  64. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/deps.py-tpl +91 -0
  65. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/health.py-tpl +11 -0
  66. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/api/router.py-tpl +17 -0
  67. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/__init__.py-tpl +0 -0
  68. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/config.py-tpl +64 -0
  69. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/core/security.py-tpl +100 -0
  70. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/db/__init__.py-tpl +0 -0
  71. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/db/session.py-tpl +36 -0
  72. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/__init__.py-tpl +0 -0
  73. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/__init__.py-tpl +0 -0
  74. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/models.py-tpl +25 -0
  75. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/repository.py-tpl +41 -0
  76. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/router.py-tpl +82 -0
  77. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/schemas.py-tpl +14 -0
  78. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/auth/service.py-tpl +65 -0
  79. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/__init__.py-tpl +0 -0
  80. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/models.py-tpl +35 -0
  81. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/repository.py-tpl +33 -0
  82. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/router.py-tpl +71 -0
  83. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/schemas.py-tpl +29 -0
  84. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/domains/users/service.py-tpl +55 -0
  85. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/src/app/main.py-tpl +69 -0
  86. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/template-config.yml-tpl +16 -0
  87. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/__init__.py-tpl +0 -0
  88. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/conftest.py-tpl +73 -0
  89. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_auth.py-tpl +201 -0
  90. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_health.py-tpl +15 -0
  91. fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/tests/test_users.py-tpl +77 -0
  92. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +25 -23
  93. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +39 -36
  94. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +3 -0
  95. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +3 -3
  96. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +5 -6
  97. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +1 -2
  98. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +8 -8
  99. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +9 -0
  100. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +1 -1
  101. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +1 -2
  102. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +0 -1
  103. fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +22 -20
  104. fastapi_fastkit/fastapi_project_template/fastapi-default/requirements.txt-tpl +37 -34
  105. fastapi_fastkit/fastapi_project_template/fastapi-default/src/api/api.py-tpl +3 -0
  106. fastapi_fastkit/fastapi_project_template/fastapi-default/src/api/routes/items.py-tpl +1 -2
  107. fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl +5 -6
  108. fastapi_fastkit/fastapi_project_template/fastapi-default/src/crud/items.py-tpl +1 -2
  109. fastapi_fastkit/fastapi_project_template/fastapi-default/src/main.py-tpl +9 -1
  110. fastapi_fastkit/fastapi_project_template/fastapi-default/src/schemas/items.py-tpl +1 -2
  111. fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +2 -3
  112. fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +0 -2
  113. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +1 -1
  114. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +3 -0
  115. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +25 -23
  116. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +41 -36
  117. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +3 -0
  118. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +1 -2
  119. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +5 -6
  120. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +1 -2
  121. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +9 -1
  122. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +1 -2
  123. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +2 -3
  124. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +0 -2
  125. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.env-tpl +2 -0
  126. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.gitignore-tpl +31 -0
  127. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl +128 -0
  128. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/pyproject.toml-tpl +69 -0
  129. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +37 -0
  130. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/format.sh-tpl +5 -0
  131. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/lint.sh-tpl +6 -0
  132. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/run-server.sh-tpl +8 -0
  133. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/test.sh-tpl +6 -0
  134. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/__init__.py-tpl +0 -0
  135. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/__init__.py-tpl +0 -0
  136. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/__init__.py-tpl +0 -0
  137. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/health.py-tpl +11 -0
  138. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +15 -0
  139. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/__init__.py-tpl +0 -0
  140. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +49 -0
  141. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/__init__.py-tpl +0 -0
  142. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +49 -0
  143. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/__init__.py-tpl +0 -0
  144. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/__init__.py-tpl +9 -0
  145. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/models.py-tpl +17 -0
  146. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +47 -0
  147. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +58 -0
  148. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/schemas.py-tpl +23 -0
  149. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +51 -0
  150. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +28 -0
  151. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/__init__.py-tpl +0 -0
  152. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +23 -0
  153. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_health.py-tpl +11 -0
  154. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_items.py-tpl +77 -0
  155. fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +2 -2
  156. fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +18 -16
  157. fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +36 -10
  158. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +5 -6
  159. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +3 -1
  160. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +0 -1
  161. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +0 -1
  162. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.env-tpl +15 -0
  163. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.gitignore-tpl +31 -0
  164. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/Dockerfile-tpl +18 -0
  165. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl +193 -0
  166. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/pyproject.toml-tpl +76 -0
  167. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/requirements.txt-tpl +15 -0
  168. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/chat.sh-tpl +10 -0
  169. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/format.sh-tpl +5 -0
  170. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/lint.sh-tpl +6 -0
  171. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/run-server.sh-tpl +8 -0
  172. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/test.sh-tpl +6 -0
  173. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/__init__.py-tpl +0 -0
  174. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/__init__.py-tpl +0 -0
  175. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/__init__.py-tpl +0 -0
  176. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/chat.py-tpl +62 -0
  177. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/deps.py-tpl +23 -0
  178. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/health.py-tpl +11 -0
  179. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/router.py-tpl +13 -0
  180. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/__init__.py-tpl +0 -0
  181. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/config.py-tpl +67 -0
  182. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/__init__.py-tpl +0 -0
  183. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/agent.py-tpl +136 -0
  184. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/client.py-tpl +30 -0
  185. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/tools.py-tpl +131 -0
  186. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/main.py-tpl +53 -0
  187. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/__init__.py-tpl +0 -0
  188. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/base.py-tpl +27 -0
  189. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/in_memory.py-tpl +32 -0
  190. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/__init__.py-tpl +0 -0
  191. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/chat.py-tpl +46 -0
  192. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/template-config.yml-tpl +16 -0
  193. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/__init__.py-tpl +0 -0
  194. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/conftest.py-tpl +158 -0
  195. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_chat.py-tpl +160 -0
  196. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_health.py-tpl +12 -0
  197. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_memory.py-tpl +38 -0
  198. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_tools.py-tpl +37 -0
  199. fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +1 -1
  200. fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +29 -26
  201. fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +68 -39
  202. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +3 -0
  203. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +4 -5
  204. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +3 -4
  205. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +4 -5
  206. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +4 -5
  207. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +4 -1
  208. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/router.py-tpl +3 -5
  209. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +6 -7
  210. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +1 -1
  211. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +0 -1
  212. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +0 -1
  213. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +0 -1
  214. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +1 -1
  215. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +26 -24
  216. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +44 -41
  217. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +1 -3
  218. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +5 -6
  219. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +2 -0
  220. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +2 -4
  221. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +1 -2
  222. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +1 -1
  223. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +4 -6
  224. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +9 -0
  225. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +4 -5
  226. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +1 -5
  227. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +0 -2
  228. fastapi_fastkit/fastapi_project_template/fastapi-single-module/.env-tpl +1 -0
  229. fastapi_fastkit/fastapi_project_template/fastapi-single-module/.gitignore-tpl +30 -0
  230. fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +7 -5
  231. fastapi_fastkit/fastapi_project_template/fastapi-single-module/requirements.txt-tpl +26 -6
  232. fastapi_fastkit/fastapi_project_template/fastapi-single-module/src/main.py-tpl +3 -0
  233. fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/__init__.py-tpl +0 -0
  234. fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/conftest.py-tpl +0 -1
  235. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.env-tpl +7 -0
  236. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.gitignore-tpl +35 -0
  237. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/Dockerfile-tpl +18 -0
  238. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl +166 -0
  239. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/alembic.ini-tpl +44 -0
  240. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/pyproject.toml-tpl +82 -0
  241. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/requirements.txt-tpl +21 -0
  242. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/format.sh-tpl +5 -0
  243. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/lint.sh-tpl +6 -0
  244. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/migrate.sh-tpl +7 -0
  245. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/run-server.sh-tpl +8 -0
  246. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/test.sh-tpl +6 -0
  247. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/__init__.py-tpl +0 -0
  248. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/__init__.py-tpl +0 -0
  249. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/README-tpl +9 -0
  250. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/env.py-tpl +72 -0
  251. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/script.py.mako-tpl +27 -0
  252. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/versions/0001_create_items_table.py-tpl +38 -0
  253. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/__init__.py-tpl +0 -0
  254. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/health.py-tpl +11 -0
  255. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/router.py-tpl +15 -0
  256. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/__init__.py-tpl +0 -0
  257. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/config.py-tpl +62 -0
  258. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/__init__.py-tpl +0 -0
  259. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/base.py-tpl +62 -0
  260. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/pagination.py-tpl +21 -0
  261. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/__init__.py-tpl +0 -0
  262. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/base.py-tpl +12 -0
  263. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/session.py-tpl +50 -0
  264. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/__init__.py-tpl +0 -0
  265. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/__init__.py-tpl +0 -0
  266. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/crud.py-tpl +26 -0
  267. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/models.py-tpl +70 -0
  268. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/router.py-tpl +69 -0
  269. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/main.py-tpl +43 -0
  270. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/template-config.yml-tpl +16 -0
  271. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/__init__.py-tpl +0 -0
  272. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/conftest.py-tpl +60 -0
  273. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_crud_base.py-tpl +43 -0
  274. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_health.py-tpl +12 -0
  275. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_items.py-tpl +110 -0
  276. fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +2 -2
  277. fastapi_fastkit/fragments/README.md +90 -0
  278. fastapi_fastkit/fragments/auth/fastapi_users.py.j2 +8 -0
  279. fastapi_fastkit/fragments/auth/jwt.py.j2 +44 -0
  280. fastapi_fastkit/fragments/auth/oauth2.py.j2 +105 -0
  281. fastapi_fastkit/fragments/auth/session.py.j2 +67 -0
  282. fastapi_fastkit/fragments/db/mongodb.py.j2 +19 -0
  283. fastapi_fastkit/fragments/db/redis.py.j2 +26 -0
  284. fastapi_fastkit/fragments/db/sqlalchemy.py.j2 +45 -0
  285. fastapi_fastkit/fragments/docker/docker_compose.j2 +84 -0
  286. fastapi_fastkit/fragments/docker/dockerfile.j2 +19 -0
  287. fastapi_fastkit/fragments/features/cache.py.j2 +26 -0
  288. fastapi_fastkit/fragments/features/pagination.py.j2 +31 -0
  289. fastapi_fastkit/fragments/features/tasks_celery.py.j2 +50 -0
  290. fastapi_fastkit/fragments/features/tasks_dramatiq.py.j2 +59 -0
  291. fastapi_fastkit/fragments/features/websocket.py.j2 +66 -0
  292. fastapi_fastkit/fragments/header.j2 +4 -0
  293. fastapi_fastkit/fragments/logging/structured.py.j2 +88 -0
  294. fastapi_fastkit/fragments/main/imports/async_tasks.j2 +1 -0
  295. fastapi_fastkit/fragments/main/imports/caching.j2 +5 -0
  296. fastapi_fastkit/fragments/main/imports/fastapi_users.j2 +1 -0
  297. fastapi_fastkit/fragments/main/imports/jwt.j2 +1 -0
  298. fastapi_fastkit/fragments/main/imports/loguru.j2 +1 -0
  299. fastapi_fastkit/fragments/main/imports/mongodb.j2 +1 -0
  300. fastapi_fastkit/fragments/main/imports/oauth2.j2 +4 -0
  301. fastapi_fastkit/fragments/main/imports/opentelemetry.j2 +6 -0
  302. fastapi_fastkit/fragments/main/imports/pagination.j2 +3 -0
  303. fastapi_fastkit/fragments/main/imports/prometheus.j2 +1 -0
  304. fastapi_fastkit/fragments/main/imports/rate_limiting.j2 +3 -0
  305. fastapi_fastkit/fragments/main/imports/session_auth.j2 +4 -0
  306. fastapi_fastkit/fragments/main/imports/sqlalchemy.j2 +2 -0
  307. fastapi_fastkit/fragments/main/imports/structured_logging.j2 +1 -0
  308. fastapi_fastkit/fragments/main/imports/websocket.j2 +1 -0
  309. fastapi_fastkit/fragments/main/lifespan/caching.j2 +7 -0
  310. fastapi_fastkit/fragments/main/lifespan/mongodb.j2 +2 -0
  311. fastapi_fastkit/fragments/main/lifespan/prometheus.j2 +2 -0
  312. fastapi_fastkit/fragments/main/lifespan/sqlalchemy.j2 +2 -0
  313. fastapi_fastkit/fragments/main/setup/async_tasks.j2 +1 -0
  314. fastapi_fastkit/fragments/main/setup/caching.j2 +1 -0
  315. fastapi_fastkit/fragments/main/setup/cors.j2 +8 -0
  316. fastapi_fastkit/fragments/main/setup/oauth2.j2 +3 -0
  317. fastapi_fastkit/fragments/main/setup/opentelemetry.j2 +19 -0
  318. fastapi_fastkit/fragments/main/setup/pagination.j2 +2 -0
  319. fastapi_fastkit/fragments/main/setup/rate_limiting.j2 +3 -0
  320. fastapi_fastkit/fragments/main/setup/session_auth.j2 +6 -0
  321. fastapi_fastkit/fragments/main/setup/structured_logging.j2 +2 -0
  322. fastapi_fastkit/fragments/main/setup/websocket.j2 +1 -0
  323. fastapi_fastkit/fragments/main.py.j2 +57 -0
  324. fastapi_fastkit/fragments/migrations/alembic_ini.j2 +42 -0
  325. fastapi_fastkit/fragments/migrations/env.py.j2 +76 -0
  326. fastapi_fastkit/fragments/migrations/initial_revision.py.j2 +27 -0
  327. fastapi_fastkit/fragments/migrations/migrate_sh.j2 +34 -0
  328. fastapi_fastkit/fragments/migrations/script.py.mako.j2 +26 -0
  329. fastapi_fastkit/fragments/tasks/celery_worker.py.j2 +49 -0
  330. fastapi_fastkit/fragments/tasks/dramatiq_worker.py.j2 +41 -0
  331. fastapi_fastkit/fragments/test/factories.py.j2 +68 -0
  332. fastapi_fastkit/fragments/test/pytest_ini.j2 +16 -0
  333. fastapi_fastkit/fragments/test/test_factories.py.j2 +27 -0
  334. fastapi_fastkit/fragments/test/test_main.py.j2 +46 -0
  335. fastapi_fastkit/fragments/tooling/devcontainer.j2 +26 -0
  336. fastapi_fastkit/fragments/tooling/github_actions.j2 +37 -0
  337. fastapi_fastkit/fragments/tooling/makefile.j2 +40 -0
  338. fastapi_fastkit/fragments/tooling/pre_commit.j2 +36 -0
  339. fastapi_fastkit/fragments/tooling/ruff_toml.j2 +16 -0
  340. fastapi_fastkit/utils/config_file.py +263 -0
  341. fastapi_fastkit/utils/main.py +111 -6
  342. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/METADATA +33 -6
  343. fastapi_fastkit-1.4.0.dist-info/RECORD +484 -0
  344. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/WHEEL +1 -1
  345. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +0 -25
  346. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +0 -27
  347. fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +0 -22
  348. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +0 -22
  349. fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +0 -22
  350. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +0 -51
  351. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +0 -25
  352. fastapi_fastkit/fastapi_project_template/fastapi-single-module/setup.py-tpl +0 -17
  353. fastapi_fastkit-1.2.1.dist-info/RECORD +0 -256
  354. /fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/__init__.py-tpl +0 -0
  355. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/entry_points.txt +0 -0
  356. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,139 @@
1
+ # --------------------------------------------------------------------------
2
+ # Filesystem helpers shared by the inspection pipeline: shell script line
3
+ # ending normalisation and best-effort removal of temporary directories that
4
+ # Docker or a test run may have left behind with awkward permissions.
5
+ #
6
+ # @author bnbong
7
+ # --------------------------------------------------------------------------
8
+ import os
9
+ import shutil
10
+ import stat
11
+ import subprocess
12
+ import time
13
+
14
+ from fastapi_fastkit.utils.logging import debug_log
15
+
16
+ CLEANUP_MAX_RETRIES = 5
17
+
18
+
19
+ def fix_script_line_endings(script_path: str) -> None:
20
+ """Convert Windows line endings in a script to Unix ones."""
21
+ try:
22
+ with open(script_path, "rb") as f:
23
+ content = f.read()
24
+
25
+ content = content.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
26
+
27
+ with open(script_path, "wb") as f:
28
+ f.write(content)
29
+
30
+ debug_log(f"Fixed line endings in {script_path}", "debug")
31
+ except OSError as e:
32
+ debug_log(f"Failed to fix line endings in {script_path}: {e}", "warning")
33
+
34
+
35
+ def fix_all_script_line_endings(directory: str) -> None:
36
+ """Normalise line endings for every shell script below ``directory``."""
37
+ for root, _dirs, files in os.walk(directory):
38
+ for file_name in files:
39
+ if file_name.endswith((".sh", ".bash")):
40
+ fix_script_line_endings(os.path.join(root, file_name))
41
+
42
+ debug_log("Fixed line endings for all shell scripts", "info")
43
+
44
+
45
+ def _fix_permissions(path: str) -> None:
46
+ """Grant full permissions to ``path``, ignoring failures."""
47
+ try:
48
+ os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
49
+ except OSError:
50
+ pass
51
+
52
+
53
+ def fix_directory_permissions(directory_path: str) -> None:
54
+ """Make a directory tree writable so it can be removed."""
55
+ _fix_permissions(directory_path)
56
+ for root, dirs, files in os.walk(directory_path):
57
+ _fix_permissions(root)
58
+ for name in dirs + files:
59
+ _fix_permissions(os.path.join(root, name))
60
+
61
+
62
+ def remove_directory_contents(directory_path: str) -> None:
63
+ """Remove a directory's contents entry by entry, ignoring failures."""
64
+ for root, dirs, files in os.walk(directory_path, topdown=False):
65
+ for file_name in files:
66
+ file_path = os.path.join(root, file_name)
67
+ _fix_permissions(file_path)
68
+ try:
69
+ os.remove(file_path)
70
+ except OSError:
71
+ pass
72
+
73
+ for dir_name in dirs:
74
+ dir_path = os.path.join(root, dir_name)
75
+ _fix_permissions(dir_path)
76
+ try:
77
+ os.rmdir(dir_path)
78
+ except OSError:
79
+ pass
80
+
81
+
82
+ def force_cleanup_directory(directory_path: str) -> None:
83
+ """Remove ``directory_path`` using progressively more forceful strategies."""
84
+ for attempt in range(CLEANUP_MAX_RETRIES):
85
+ try:
86
+ shutil.rmtree(directory_path)
87
+ debug_log(
88
+ f"Successfully cleaned up temp directory: {directory_path}", "info"
89
+ )
90
+ return
91
+ except (OSError, PermissionError) as e:
92
+ if attempt == CLEANUP_MAX_RETRIES - 1:
93
+ break
94
+ debug_log(
95
+ f"Attempt {attempt + 1} failed to cleanup {directory_path}: {e}",
96
+ "warning",
97
+ )
98
+
99
+ try:
100
+ fix_directory_permissions(directory_path)
101
+ shutil.rmtree(directory_path)
102
+ debug_log(
103
+ "Successfully cleaned up temp directory after permission fix: "
104
+ f"{directory_path}",
105
+ "info",
106
+ )
107
+ return
108
+ except OSError:
109
+ pass
110
+
111
+ try:
112
+ remove_directory_contents(directory_path)
113
+ os.rmdir(directory_path)
114
+ debug_log(
115
+ "Successfully cleaned up temp directory by removing contents: "
116
+ f"{directory_path}",
117
+ "info",
118
+ )
119
+ return
120
+ except OSError:
121
+ pass
122
+
123
+ time.sleep(2)
124
+
125
+ debug_log(
126
+ f"Failed to cleanup temp directory after {CLEANUP_MAX_RETRIES} attempts: "
127
+ f"{directory_path}",
128
+ "warning",
129
+ )
130
+ try:
131
+ if os.name == "nt": # pragma: no cover - Windows-only branch
132
+ subprocess.run(
133
+ ["rmdir", "/s", "/q", directory_path], check=False, shell=True
134
+ )
135
+ else:
136
+ subprocess.run(["rm", "-rf", directory_path], check=False)
137
+ debug_log(f"Force cleanup completed for: {directory_path}", "info")
138
+ except OSError:
139
+ debug_log(f"All cleanup attempts failed for: {directory_path}", "warning")
@@ -0,0 +1,89 @@
1
+ # --------------------------------------------------------------------------
2
+ # Static analysis of the generated project.
3
+ #
4
+ # ``compileall`` is mandatory - a template that cannot be byte-compiled is
5
+ # broken beyond argument. ``mypy`` is opt-in because it needs the template's
6
+ # own dependencies installed and is far slower.
7
+ #
8
+ # @author bnbong
9
+ # --------------------------------------------------------------------------
10
+ import subprocess
11
+ import sys
12
+ from typing import List
13
+
14
+ from fastapi_fastkit.utils.logging import debug_log
15
+
16
+ from .context import InspectionContext
17
+
18
+ COMPILE_TIMEOUT = 120
19
+ MYPY_TIMEOUT = 300
20
+
21
+
22
+ def _interpreter(ctx: InspectionContext) -> str:
23
+ """Prefer the inspection venv interpreter, falling back to the host one."""
24
+ return ctx.python_executable() or sys.executable
25
+
26
+
27
+ def check_compileall(ctx: InspectionContext) -> bool:
28
+ """Byte-compile every Python file of the generated project."""
29
+ command: List[str] = [
30
+ _interpreter(ctx),
31
+ "-m",
32
+ "compileall",
33
+ "-q",
34
+ "-x",
35
+ r"(\.venv|venv|node_modules)",
36
+ ctx.temp_dir,
37
+ ]
38
+ try:
39
+ result = subprocess.run(
40
+ command,
41
+ cwd=ctx.temp_dir,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=COMPILE_TIMEOUT,
45
+ )
46
+ except subprocess.TimeoutExpired:
47
+ ctx.add_error("compileall timed out")
48
+ return False
49
+ except OSError as e:
50
+ ctx.add_error(f"Failed to run compileall: {e}")
51
+ return False
52
+
53
+ if result.returncode != 0:
54
+ detail = (result.stderr or result.stdout or "").strip()
55
+ ctx.add_error(f"Generated project failed to compile:\n{detail}")
56
+ return False
57
+
58
+ debug_log("compileall check passed", "info")
59
+ return True
60
+
61
+
62
+ def check_mypy(ctx: InspectionContext) -> bool:
63
+ """Run mypy inside the generated project (opt-in via ``--mypy``)."""
64
+ if not ctx.options.run_mypy:
65
+ debug_log("mypy check disabled", "info")
66
+ return True
67
+
68
+ try:
69
+ result = subprocess.run(
70
+ [_interpreter(ctx), "-m", "mypy", "."],
71
+ cwd=ctx.temp_dir,
72
+ capture_output=True,
73
+ text=True,
74
+ timeout=MYPY_TIMEOUT,
75
+ )
76
+ except subprocess.TimeoutExpired:
77
+ ctx.add_error("mypy timed out")
78
+ return False
79
+ except OSError as e:
80
+ ctx.add_warning(f"Could not run mypy: {e}")
81
+ return True
82
+
83
+ if result.returncode != 0:
84
+ detail = (result.stdout or result.stderr or "").strip()
85
+ ctx.add_error(f"mypy reported errors in the generated project:\n{detail}")
86
+ return False
87
+
88
+ debug_log("mypy check passed", "info")
89
+ return True
@@ -0,0 +1,58 @@
1
+ # --------------------------------------------------------------------------
2
+ # Inspection reporting: turn an InspectionContext into the report dictionary
3
+ # consumed by the CLI, the local scripts and the CI workflows.
4
+ #
5
+ # @author bnbong
6
+ # --------------------------------------------------------------------------
7
+ from typing import Any, Dict
8
+
9
+ from fastapi_fastkit.utils.logging import debug_log
10
+ from fastapi_fastkit.utils.main import print_error, print_success, print_warning
11
+
12
+ from .context import InspectionContext
13
+
14
+
15
+ def build_report(ctx: InspectionContext) -> Dict[str, Any]:
16
+ """Build the inspection report and log a summary of the findings."""
17
+ is_valid = len(ctx.errors) == 0
18
+ template_name = ctx.template_path.name
19
+
20
+ if is_valid:
21
+ debug_log(
22
+ f"Template inspection completed successfully for {template_name}", "info"
23
+ )
24
+ else:
25
+ debug_log(
26
+ f"Template inspection failed for {template_name} "
27
+ f"with {len(ctx.errors)} errors",
28
+ "error",
29
+ )
30
+ for index, error in enumerate(ctx.errors, 1):
31
+ debug_log(f"Error {index}: {error}", "error")
32
+
33
+ if ctx.warnings:
34
+ debug_log(
35
+ f"Template {template_name} has {len(ctx.warnings)} warnings: "
36
+ f"{ctx.warnings}",
37
+ "warning",
38
+ )
39
+
40
+ return {
41
+ "template_path": str(ctx.template_path),
42
+ "errors": list(ctx.errors),
43
+ "warnings": list(ctx.warnings),
44
+ "is_valid": is_valid,
45
+ }
46
+
47
+
48
+ def print_report(ctx: InspectionContext, is_valid: bool) -> None:
49
+ """Print the human-facing inspection outcome."""
50
+ if is_valid:
51
+ print_success(f"Template {ctx.template_path} is valid!")
52
+ else:
53
+ print_error(f"Template {ctx.template_path} validation failed")
54
+ for error in ctx.errors:
55
+ print_error(f" - {error}")
56
+
57
+ for warning in ctx.warnings:
58
+ print_warning(f" - {warning}")
@@ -0,0 +1,327 @@
1
+ # --------------------------------------------------------------------------
2
+ # HTTP smoke test for a generated project.
3
+ #
4
+ # Booting the application is the only honest way to prove a template works:
5
+ # a substring search for "FastAPI" in main.py cannot tell a working app from
6
+ # one that raises on import. The generated project is started with uvicorn in
7
+ # a subprocess on a free port; ``/docs`` must answer 200 and ``/health``, when
8
+ # the template exposes one, must answer 200 as well.
9
+ #
10
+ # @author bnbong
11
+ # --------------------------------------------------------------------------
12
+ import os
13
+ import signal
14
+ import socket
15
+ import subprocess
16
+ import tempfile
17
+ import time
18
+ import tomllib
19
+ import urllib.error
20
+ import urllib.request
21
+ from typing import IO, List, Optional, Tuple
22
+
23
+ from fastapi_fastkit.utils.logging import debug_log
24
+
25
+ from .context import InspectionContext
26
+
27
+ #: Probed in order when no explicit app_module is declared.
28
+ MAIN_MODULE_CANDIDATES = [
29
+ ("src/app/main.py", "src.app.main"),
30
+ ("src/main.py", "src.main"),
31
+ ("app/main.py", "app.main"),
32
+ ("main.py", "main"),
33
+ ]
34
+ DEFAULT_APP_ATTRIBUTE = "app"
35
+ SHUTDOWN_TIMEOUT = 10
36
+
37
+ #: How many ports to try before giving up. ``find_free_port`` releases the
38
+ #: port before uvicorn binds it, so another process can win the race; on a
39
+ #: busy CI machine that is common enough to be worth retrying.
40
+ PORT_ATTEMPTS = 3
41
+
42
+ #: Bytes of captured server output attached to a failure report.
43
+ LOG_TAIL_BYTES = 4000
44
+
45
+
46
+ def find_free_port() -> int:
47
+ """Reserve and release a port so uvicorn can bind it."""
48
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
49
+ sock.bind(("127.0.0.1", 0))
50
+ port: int = sock.getsockname()[1]
51
+ return port
52
+
53
+
54
+ def _app_module_from_pyproject(ctx: InspectionContext) -> Optional[str]:
55
+ """Read ``[tool.fastapi-fastkit].app_module`` from the generated project."""
56
+ pyproject_path = ctx.temp_path("pyproject.toml")
57
+ if not os.path.exists(pyproject_path):
58
+ return None
59
+ try:
60
+ with open(pyproject_path, "rb") as f:
61
+ data = tomllib.load(f)
62
+ except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
63
+ debug_log(f"Could not read generated pyproject.toml: {e}", "warning")
64
+ return None
65
+ value = data.get("tool", {}).get("fastapi-fastkit", {}).get("app_module")
66
+ return str(value) if value else None
67
+
68
+
69
+ def _app_module_from_template_config(ctx: InspectionContext) -> Optional[str]:
70
+ """Read ``app_module`` from template-config.yml, if the template ships one."""
71
+ if not ctx.template_config:
72
+ return None
73
+ value = ctx.template_config.get("app_module")
74
+ return str(value) if value else None
75
+
76
+
77
+ def _app_module_from_layout(ctx: InspectionContext) -> Optional[str]:
78
+ """Fall back to the conventional main module locations."""
79
+ for relative_path, dotted in MAIN_MODULE_CANDIDATES:
80
+ if os.path.exists(ctx.temp_path(*relative_path.split("/"))):
81
+ return f"{dotted}:{DEFAULT_APP_ATTRIBUTE}"
82
+ return None
83
+
84
+
85
+ def resolve_app_module(ctx: InspectionContext) -> Optional[str]:
86
+ """Determine the ``module:attribute`` uvicorn target for the project.
87
+
88
+ Resolution order: generated ``pyproject.toml`` metadata, then
89
+ ``template-config.yml``, then the conventional file layout.
90
+ """
91
+ for resolver in (
92
+ _app_module_from_pyproject,
93
+ _app_module_from_template_config,
94
+ _app_module_from_layout,
95
+ ):
96
+ value = resolver(ctx)
97
+ if value:
98
+ if ":" not in value:
99
+ value = f"{value}:{DEFAULT_APP_ATTRIBUTE}"
100
+ debug_log(f"Resolved app module: {value}", "info")
101
+ return value
102
+ return None
103
+
104
+
105
+ def _probe(url: str, timeout: int = 5) -> Optional[int]:
106
+ """GET ``url`` and return its status code, or ``None`` when unreachable."""
107
+ try:
108
+ with urllib.request.urlopen(url, timeout=timeout) as response:
109
+ status: int = response.status
110
+ return status
111
+ except urllib.error.HTTPError as e:
112
+ return int(e.code)
113
+ except (urllib.error.URLError, OSError, TimeoutError):
114
+ return None
115
+
116
+
117
+ def _wait_for_server(
118
+ process: "subprocess.Popen[bytes]", base_url: str, timeout: int
119
+ ) -> Tuple[bool, str]:
120
+ """Poll ``/docs`` until the server answers, the process dies or time runs out."""
121
+ deadline = time.time() + timeout
122
+ while time.time() < deadline:
123
+ if process.poll() is not None:
124
+ return False, "server process exited before becoming reachable"
125
+ status = _probe(f"{base_url}/docs")
126
+ if status is not None:
127
+ if status == 200:
128
+ return True, ""
129
+ return False, f"/docs returned HTTP {status} (expected 200)"
130
+ time.sleep(0.5)
131
+ return False, f"server did not become reachable within {timeout}s"
132
+
133
+
134
+ def _signal_group(process: "subprocess.Popen[bytes]", sig: int) -> bool:
135
+ """Signal the server's whole process group, if it has one of its own.
136
+
137
+ uvicorn spawns reloader / worker children, and signalling only the
138
+ parent leaves them holding the port. The process is started with
139
+ ``start_new_session=True`` so the group id equals its pid.
140
+
141
+ Only a group we created ourselves is ever signalled: the pid must be a
142
+ real positive integer and must be its own group leader. Anything else --
143
+ a mocked process object, a pid that has been reaped and rejoined another
144
+ group, the init group, or the group this interpreter itself belongs to --
145
+ falls back to signalling the single process, because signalling the wrong
146
+ group would tear down unrelated processes (a CI runner, for one).
147
+
148
+ :return: True when the group was signalled, False to fall back
149
+ """
150
+ if not hasattr(os, "killpg"): # pragma: no cover - non-POSIX
151
+ return False
152
+
153
+ pid = process.pid
154
+ if type(pid) is not int or pid <= 1:
155
+ debug_log(f"Refusing to signal process group for pid {pid!r}", "debug")
156
+ return False
157
+
158
+ try:
159
+ pgid = os.getpgid(pid)
160
+ except OSError as e:
161
+ debug_log(f"Could not read smoke test process group: {e}", "debug")
162
+ return False
163
+
164
+ # start_new_session=True makes the child its own group leader, so a pgid
165
+ # that differs from the pid means the process is sharing someone else's
166
+ # group -- never ours to signal.
167
+ if pgid != pid or pgid <= 1 or pgid == os.getpgrp():
168
+ debug_log(
169
+ f"Smoke test process {pid} is not its own group leader "
170
+ f"(pgid {pgid}); signalling the process only",
171
+ "debug",
172
+ )
173
+ return False
174
+
175
+ try:
176
+ os.killpg(pgid, sig)
177
+ return True
178
+ except OSError as e:
179
+ debug_log(f"Could not signal smoke test process group: {e}", "debug")
180
+ return False
181
+
182
+
183
+ def _terminate(process: "subprocess.Popen[bytes]") -> None:
184
+ """Stop the server process (and its group), escalating to SIGKILL."""
185
+ if process.poll() is not None:
186
+ return
187
+
188
+ if not _signal_group(process, signal.SIGTERM):
189
+ process.terminate()
190
+ try:
191
+ process.wait(timeout=SHUTDOWN_TIMEOUT)
192
+ return
193
+ except subprocess.TimeoutExpired:
194
+ pass
195
+
196
+ if not _signal_group(process, signal.SIGKILL):
197
+ process.kill()
198
+ try:
199
+ process.wait(timeout=SHUTDOWN_TIMEOUT)
200
+ except subprocess.TimeoutExpired: # pragma: no cover - defensive
201
+ debug_log("Smoke test server could not be killed", "warning")
202
+
203
+
204
+ #: Phrases uvicorn / asyncio emit when the chosen port was taken between
205
+ #: ``find_free_port`` releasing it and uvicorn binding it.
206
+ _PORT_CONFLICT_MARKERS = (
207
+ "address already in use",
208
+ "error while attempting to bind on address",
209
+ "only one usage of each socket address",
210
+ )
211
+
212
+
213
+ def _is_port_conflict(output: str) -> bool:
214
+ """Return True when captured server output shows a failed port bind."""
215
+ lowered = output.lower()
216
+ return any(marker in lowered for marker in _PORT_CONFLICT_MARKERS)
217
+
218
+
219
+ def _read_log_tail(log_file: IO[bytes]) -> str:
220
+ """Return the tail of the captured server output."""
221
+ try:
222
+ log_file.flush()
223
+ size = os.fstat(log_file.fileno()).st_size
224
+ log_file.seek(max(0, size - LOG_TAIL_BYTES))
225
+ return log_file.read().decode("utf-8", errors="replace").strip()
226
+ except OSError as e: # pragma: no cover - defensive
227
+ debug_log(f"Could not read smoke test server log: {e}", "warning")
228
+ return ""
229
+
230
+
231
+ def check_smoke_test(ctx: InspectionContext) -> bool:
232
+ """Boot the generated project and verify its HTTP surface."""
233
+ if not ctx.options.run_smoke_test:
234
+ debug_log("Smoke test disabled", "info")
235
+ return True
236
+
237
+ python_executable = ctx.python_executable()
238
+ if not python_executable or not os.path.exists(python_executable):
239
+ ctx.add_error(
240
+ "Smoke test requires an installed environment, but no virtual "
241
+ "environment was prepared for the generated project"
242
+ )
243
+ return False
244
+
245
+ app_module = resolve_app_module(ctx)
246
+ if not app_module:
247
+ ctx.add_error(
248
+ "Could not determine the ASGI app module: declare "
249
+ "[tool.fastapi-fastkit].app_module or ship a conventional main.py"
250
+ )
251
+ return False
252
+
253
+ env = os.environ.copy()
254
+ env["PYTHONPATH"] = ctx.temp_dir + os.pathsep + env.get("PYTHONPATH", "")
255
+
256
+ last_failure = ""
257
+ for attempt in range(1, PORT_ATTEMPTS + 1):
258
+ port = find_free_port()
259
+ base_url = f"http://127.0.0.1:{port}"
260
+ command: List[str] = [
261
+ python_executable,
262
+ "-m",
263
+ "uvicorn",
264
+ app_module,
265
+ "--host",
266
+ "127.0.0.1",
267
+ "--port",
268
+ str(port),
269
+ ]
270
+
271
+ debug_log(f"Starting smoke test server: {' '.join(command)}", "info")
272
+
273
+ # The server's output goes to a temp file rather than a pipe: uvicorn
274
+ # keeps logging for as long as it runs, and a pipe nobody drains fills
275
+ # its buffer and blocks the server mid-request.
276
+ with tempfile.TemporaryFile() as log_file:
277
+ try:
278
+ process = subprocess.Popen(
279
+ command,
280
+ cwd=ctx.temp_dir,
281
+ stdout=log_file,
282
+ stderr=subprocess.STDOUT,
283
+ env=env,
284
+ # Own process group, so terminating the server also stops
285
+ # any worker it spawned instead of orphaning it.
286
+ start_new_session=True,
287
+ )
288
+ except OSError as e:
289
+ ctx.add_error(f"Failed to start the application server: {e}")
290
+ return False
291
+
292
+ try:
293
+ reachable, reason = _wait_for_server(
294
+ process, base_url, ctx.options.smoke_timeout
295
+ )
296
+ if not reachable:
297
+ output = _read_log_tail(log_file)
298
+ last_failure = f"Smoke test failed: {reason}\n{output}".rstrip()
299
+ if _is_port_conflict(output) and attempt < PORT_ATTEMPTS:
300
+ debug_log(
301
+ f"Port {port} was taken before uvicorn bound it; "
302
+ f"retrying on another port ({attempt}/{PORT_ATTEMPTS})",
303
+ "warning",
304
+ )
305
+ continue
306
+ ctx.add_error(last_failure)
307
+ return False
308
+
309
+ health_status = _probe(f"{base_url}/health")
310
+ if health_status is None:
311
+ ctx.add_warning("Smoke test: /health did not respond")
312
+ elif health_status == 404:
313
+ debug_log("Template exposes no /health endpoint, skipping", "info")
314
+ elif health_status != 200:
315
+ ctx.add_error(
316
+ f"Smoke test failed: /health returned HTTP {health_status} "
317
+ "(expected 200)\n" + _read_log_tail(log_file)
318
+ )
319
+ return False
320
+ finally:
321
+ _terminate(process)
322
+
323
+ debug_log("Smoke test passed", "info")
324
+ return True
325
+
326
+ ctx.add_error(last_failure or "Smoke test failed: no free port could be bound")
327
+ return False