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.
Files changed (335) 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 -1400
  15. fastapi_fastkit/backend/interactive/__init__.py +6 -0
  16. fastapi_fastkit/backend/interactive/config_builder.py +45 -63
  17. fastapi_fastkit/backend/interactive/prompts.py +155 -6
  18. fastapi_fastkit/backend/interactive/selectors.py +18 -2
  19. fastapi_fastkit/backend/main.py +887 -99
  20. fastapi_fastkit/backend/package_managers/base.py +172 -0
  21. fastapi_fastkit/backend/package_managers/factory.py +5 -4
  22. fastapi_fastkit/backend/package_managers/pdm_manager.py +29 -82
  23. fastapi_fastkit/backend/package_managers/pip_manager.py +31 -85
  24. fastapi_fastkit/backend/package_managers/poetry_manager.py +70 -110
  25. fastapi_fastkit/backend/package_managers/uv_manager.py +43 -112
  26. fastapi_fastkit/backend/project_builder/__init__.py +8 -1
  27. fastapi_fastkit/backend/project_builder/config_generator.py +806 -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 +254 -28
  31. fastapi_fastkit/backend/scaffolder.py +578 -0
  32. fastapi_fastkit/backend/transducer.py +54 -21
  33. fastapi_fastkit/cli.py +434 -311
  34. fastapi_fastkit/core/settings.py +232 -43
  35. fastapi_fastkit/fastapi_project_template/README.md +26 -10
  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 +21 -22
  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 +21 -22
  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 +18 -19
  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 +21 -22
  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/pyproject.toml-tpl +13 -14
  126. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +36 -10
  127. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +3 -0
  128. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +4 -4
  129. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +4 -3
  130. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +4 -5
  131. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +2 -3
  132. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +2 -3
  133. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +3 -0
  134. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +0 -1
  135. fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +14 -15
  136. fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +36 -10
  137. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +5 -6
  138. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +3 -1
  139. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +0 -1
  140. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +0 -1
  141. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.env-tpl +15 -0
  142. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/.gitignore-tpl +31 -0
  143. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/Dockerfile-tpl +18 -0
  144. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl +193 -0
  145. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/pyproject.toml-tpl +76 -0
  146. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/requirements.txt-tpl +15 -0
  147. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/chat.sh-tpl +10 -0
  148. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/format.sh-tpl +5 -0
  149. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/lint.sh-tpl +6 -0
  150. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/run-server.sh-tpl +8 -0
  151. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/scripts/test.sh-tpl +6 -0
  152. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/__init__.py-tpl +0 -0
  153. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/__init__.py-tpl +0 -0
  154. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/__init__.py-tpl +0 -0
  155. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/chat.py-tpl +62 -0
  156. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/deps.py-tpl +23 -0
  157. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/health.py-tpl +11 -0
  158. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/api/router.py-tpl +13 -0
  159. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/__init__.py-tpl +0 -0
  160. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/core/config.py-tpl +67 -0
  161. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/__init__.py-tpl +0 -0
  162. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/agent.py-tpl +136 -0
  163. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/client.py-tpl +30 -0
  164. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/llm/tools.py-tpl +131 -0
  165. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/main.py-tpl +53 -0
  166. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/__init__.py-tpl +0 -0
  167. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/base.py-tpl +27 -0
  168. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/memory/in_memory.py-tpl +32 -0
  169. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/__init__.py-tpl +0 -0
  170. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/src/app/schemas/chat.py-tpl +46 -0
  171. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/template-config.yml-tpl +16 -0
  172. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/__init__.py-tpl +0 -0
  173. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/conftest.py-tpl +158 -0
  174. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_chat.py-tpl +160 -0
  175. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_health.py-tpl +12 -0
  176. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_memory.py-tpl +38 -0
  177. fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/tests/test_tools.py-tpl +37 -0
  178. fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +1 -1
  179. fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +25 -25
  180. fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +68 -39
  181. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +3 -0
  182. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +4 -5
  183. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +3 -4
  184. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +4 -5
  185. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +4 -5
  186. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +4 -1
  187. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/router.py-tpl +3 -5
  188. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +6 -7
  189. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +1 -1
  190. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +0 -1
  191. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +0 -1
  192. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +0 -1
  193. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +1 -1
  194. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +22 -23
  195. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +44 -41
  196. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +1 -3
  197. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +5 -6
  198. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +2 -0
  199. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +2 -4
  200. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +1 -2
  201. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +1 -1
  202. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +4 -6
  203. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +9 -0
  204. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +4 -5
  205. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +1 -5
  206. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +0 -2
  207. fastapi_fastkit/fastapi_project_template/fastapi-single-module/.env-tpl +1 -0
  208. fastapi_fastkit/fastapi_project_template/fastapi-single-module/.gitignore-tpl +30 -0
  209. fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +3 -4
  210. fastapi_fastkit/fastapi_project_template/fastapi-single-module/requirements.txt-tpl +26 -6
  211. fastapi_fastkit/fastapi_project_template/fastapi-single-module/src/main.py-tpl +3 -0
  212. fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/__init__.py-tpl +0 -0
  213. fastapi_fastkit/fastapi_project_template/fastapi-single-module/tests/conftest.py-tpl +0 -1
  214. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.env-tpl +7 -0
  215. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/.gitignore-tpl +35 -0
  216. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/Dockerfile-tpl +18 -0
  217. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl +166 -0
  218. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/alembic.ini-tpl +44 -0
  219. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/pyproject.toml-tpl +82 -0
  220. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/requirements.txt-tpl +21 -0
  221. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/format.sh-tpl +5 -0
  222. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/lint.sh-tpl +6 -0
  223. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/migrate.sh-tpl +7 -0
  224. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/run-server.sh-tpl +8 -0
  225. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/scripts/test.sh-tpl +6 -0
  226. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/__init__.py-tpl +0 -0
  227. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/__init__.py-tpl +0 -0
  228. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/README-tpl +9 -0
  229. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/env.py-tpl +72 -0
  230. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/script.py.mako-tpl +27 -0
  231. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/alembic/versions/0001_create_items_table.py-tpl +38 -0
  232. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/__init__.py-tpl +0 -0
  233. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/health.py-tpl +11 -0
  234. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/api/router.py-tpl +15 -0
  235. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/__init__.py-tpl +0 -0
  236. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/core/config.py-tpl +62 -0
  237. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/__init__.py-tpl +0 -0
  238. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/base.py-tpl +62 -0
  239. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/crud/pagination.py-tpl +21 -0
  240. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/__init__.py-tpl +0 -0
  241. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/base.py-tpl +12 -0
  242. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/db/session.py-tpl +50 -0
  243. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/__init__.py-tpl +0 -0
  244. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/__init__.py-tpl +0 -0
  245. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/crud.py-tpl +26 -0
  246. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/models.py-tpl +70 -0
  247. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/domains/items/router.py-tpl +69 -0
  248. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/src/app/main.py-tpl +43 -0
  249. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/template-config.yml-tpl +16 -0
  250. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/__init__.py-tpl +0 -0
  251. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/conftest.py-tpl +60 -0
  252. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_crud_base.py-tpl +43 -0
  253. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_health.py-tpl +12 -0
  254. fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/tests/test_items.py-tpl +110 -0
  255. fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +2 -2
  256. fastapi_fastkit/fragments/README.md +90 -0
  257. fastapi_fastkit/fragments/auth/fastapi_users.py.j2 +8 -0
  258. fastapi_fastkit/fragments/auth/jwt.py.j2 +44 -0
  259. fastapi_fastkit/fragments/auth/oauth2.py.j2 +105 -0
  260. fastapi_fastkit/fragments/auth/session.py.j2 +67 -0
  261. fastapi_fastkit/fragments/db/mongodb.py.j2 +19 -0
  262. fastapi_fastkit/fragments/db/redis.py.j2 +26 -0
  263. fastapi_fastkit/fragments/db/sqlalchemy.py.j2 +45 -0
  264. fastapi_fastkit/fragments/docker/docker_compose.j2 +84 -0
  265. fastapi_fastkit/fragments/docker/dockerfile.j2 +19 -0
  266. fastapi_fastkit/fragments/features/cache.py.j2 +26 -0
  267. fastapi_fastkit/fragments/features/pagination.py.j2 +31 -0
  268. fastapi_fastkit/fragments/features/tasks_celery.py.j2 +50 -0
  269. fastapi_fastkit/fragments/features/tasks_dramatiq.py.j2 +59 -0
  270. fastapi_fastkit/fragments/features/websocket.py.j2 +66 -0
  271. fastapi_fastkit/fragments/header.j2 +4 -0
  272. fastapi_fastkit/fragments/logging/structured.py.j2 +88 -0
  273. fastapi_fastkit/fragments/main/imports/async_tasks.j2 +1 -0
  274. fastapi_fastkit/fragments/main/imports/caching.j2 +5 -0
  275. fastapi_fastkit/fragments/main/imports/fastapi_users.j2 +1 -0
  276. fastapi_fastkit/fragments/main/imports/jwt.j2 +1 -0
  277. fastapi_fastkit/fragments/main/imports/loguru.j2 +1 -0
  278. fastapi_fastkit/fragments/main/imports/mongodb.j2 +1 -0
  279. fastapi_fastkit/fragments/main/imports/oauth2.j2 +4 -0
  280. fastapi_fastkit/fragments/main/imports/opentelemetry.j2 +6 -0
  281. fastapi_fastkit/fragments/main/imports/pagination.j2 +3 -0
  282. fastapi_fastkit/fragments/main/imports/prometheus.j2 +1 -0
  283. fastapi_fastkit/fragments/main/imports/rate_limiting.j2 +3 -0
  284. fastapi_fastkit/fragments/main/imports/session_auth.j2 +4 -0
  285. fastapi_fastkit/fragments/main/imports/sqlalchemy.j2 +2 -0
  286. fastapi_fastkit/fragments/main/imports/structured_logging.j2 +1 -0
  287. fastapi_fastkit/fragments/main/imports/websocket.j2 +1 -0
  288. fastapi_fastkit/fragments/main/lifespan/caching.j2 +7 -0
  289. fastapi_fastkit/fragments/main/lifespan/mongodb.j2 +2 -0
  290. fastapi_fastkit/fragments/main/lifespan/prometheus.j2 +2 -0
  291. fastapi_fastkit/fragments/main/lifespan/sqlalchemy.j2 +2 -0
  292. fastapi_fastkit/fragments/main/setup/async_tasks.j2 +1 -0
  293. fastapi_fastkit/fragments/main/setup/caching.j2 +1 -0
  294. fastapi_fastkit/fragments/main/setup/cors.j2 +8 -0
  295. fastapi_fastkit/fragments/main/setup/oauth2.j2 +3 -0
  296. fastapi_fastkit/fragments/main/setup/opentelemetry.j2 +19 -0
  297. fastapi_fastkit/fragments/main/setup/pagination.j2 +2 -0
  298. fastapi_fastkit/fragments/main/setup/rate_limiting.j2 +3 -0
  299. fastapi_fastkit/fragments/main/setup/session_auth.j2 +6 -0
  300. fastapi_fastkit/fragments/main/setup/structured_logging.j2 +2 -0
  301. fastapi_fastkit/fragments/main/setup/websocket.j2 +1 -0
  302. fastapi_fastkit/fragments/main.py.j2 +57 -0
  303. fastapi_fastkit/fragments/migrations/alembic_ini.j2 +42 -0
  304. fastapi_fastkit/fragments/migrations/env.py.j2 +76 -0
  305. fastapi_fastkit/fragments/migrations/initial_revision.py.j2 +27 -0
  306. fastapi_fastkit/fragments/migrations/migrate_sh.j2 +34 -0
  307. fastapi_fastkit/fragments/migrations/script.py.mako.j2 +26 -0
  308. fastapi_fastkit/fragments/tasks/celery_worker.py.j2 +49 -0
  309. fastapi_fastkit/fragments/tasks/dramatiq_worker.py.j2 +41 -0
  310. fastapi_fastkit/fragments/test/factories.py.j2 +68 -0
  311. fastapi_fastkit/fragments/test/pytest_ini.j2 +16 -0
  312. fastapi_fastkit/fragments/test/test_factories.py.j2 +27 -0
  313. fastapi_fastkit/fragments/test/test_main.py.j2 +46 -0
  314. fastapi_fastkit/fragments/tooling/devcontainer.j2 +26 -0
  315. fastapi_fastkit/fragments/tooling/github_actions.j2 +37 -0
  316. fastapi_fastkit/fragments/tooling/makefile.j2 +40 -0
  317. fastapi_fastkit/fragments/tooling/pre_commit.j2 +36 -0
  318. fastapi_fastkit/fragments/tooling/ruff_toml.j2 +16 -0
  319. fastapi_fastkit/utils/config_file.py +263 -0
  320. fastapi_fastkit/utils/main.py +38 -0
  321. {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.0.dist-info}/METADATA +26 -2
  322. fastapi_fastkit-1.4.0.dist-info/RECORD +484 -0
  323. {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.0.dist-info}/WHEEL +1 -1
  324. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +0 -25
  325. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +0 -27
  326. fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +0 -22
  327. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +0 -22
  328. fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +0 -22
  329. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +0 -51
  330. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +0 -25
  331. fastapi_fastkit/fastapi_project_template/fastapi-single-module/setup.py-tpl +0 -17
  332. fastapi_fastkit-1.3.0.dist-info/RECORD +0 -287
  333. /fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/{mcp → mcp_server}/__init__.py-tpl +0 -0
  334. {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.0.dist-info}/entry_points.txt +0 -0
  335. {fastapi_fastkit-1.3.0.dist-info → fastapi_fastkit-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,347 @@
1
+ # --------------------------------------------------------------------------
2
+ # The TemplateInspector orchestrator.
3
+ #
4
+ # The inspector generates a real project from the template (copy + convert +
5
+ # metadata injection), runs every check against that generated project, and
6
+ # cleans the temporary directory up afterwards. Individual checks live in
7
+ # sibling modules; this class only sequences them and owns the lifecycle.
8
+ #
9
+ # @author bnbong
10
+ # --------------------------------------------------------------------------
11
+ import os
12
+ import shutil
13
+ import time
14
+ from pathlib import Path
15
+ from types import SimpleNamespace
16
+ from typing import Any, Callable, Dict, List, Optional, Tuple
17
+
18
+ from fastapi_fastkit.backend.scaffolder import ProjectScaffolder, ScaffoldOptions
19
+ from fastapi_fastkit.core.settings import settings
20
+ from fastapi_fastkit.utils.logging import debug_log
21
+
22
+ from . import checks, consistency, freshness, lint, smoke, strategies
23
+ from .context import InspectionContext, InspectionOptions
24
+ from .docker import DockerCompose
25
+ from .fsutils import force_cleanup_directory
26
+ from .report import build_report, print_report
27
+
28
+ #: Metadata used to generate the throwaway project that gets inspected.
29
+ DUMMY_METADATA = {
30
+ "project_name": "test-template",
31
+ "author": "Template Inspector",
32
+ "author_email": "inspector@fastapi-fastkit.dev",
33
+ "description": "Test project for template inspection",
34
+ }
35
+
36
+
37
+ class TemplateInspector:
38
+ """Validate a FastAPI template by generating and exercising a project.
39
+
40
+ Uses the context manager protocol so the temporary project directory is
41
+ always removed, even when a check raises.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ template_path: str,
47
+ temp_base_dir: Optional[str] = None,
48
+ options: Optional[InspectionOptions] = None,
49
+ ):
50
+ template_name = Path(template_path).name
51
+ base_dir = temp_base_dir or os.path.dirname(os.path.dirname(__file__))
52
+ self.ctx = InspectionContext(
53
+ template_path=Path(template_path),
54
+ temp_dir=os.path.join(base_dir, f"temp_{template_name}"),
55
+ options=options or InspectionOptions(),
56
+ )
57
+ self._cleanup_needed = False
58
+
59
+ # -- state exposed for callers and tests -------------------------------
60
+ @property
61
+ def template_path(self) -> Path:
62
+ """The on-disk template directory being inspected."""
63
+ return self.ctx.template_path
64
+
65
+ @property
66
+ def temp_dir(self) -> str:
67
+ """The generated project directory used for the inspection."""
68
+ return self.ctx.temp_dir
69
+
70
+ @property
71
+ def errors(self) -> List[str]:
72
+ """Fatal findings accumulated so far."""
73
+ return self.ctx.errors
74
+
75
+ @property
76
+ def warnings(self) -> List[str]:
77
+ """Non-fatal findings accumulated so far."""
78
+ return self.ctx.warnings
79
+
80
+ @property
81
+ def template_config(self) -> Optional[Dict[str, Any]]:
82
+ """Parsed ``template-config.yml`` contents, when the template ships one."""
83
+ return self.ctx.template_config
84
+
85
+ # -- lifecycle ---------------------------------------------------------
86
+ def __enter__(self) -> "TemplateInspector":
87
+ """Create the temporary directory and generate a project from the template."""
88
+ try:
89
+ if os.path.exists(self.ctx.temp_dir):
90
+ debug_log(
91
+ f"Cleaning up existing temp directory: {self.ctx.temp_dir}", "info"
92
+ )
93
+ try:
94
+ shutil.rmtree(self.ctx.temp_dir)
95
+ except OSError as e:
96
+ debug_log(
97
+ f"Failed to cleanup existing temp directory: {e}", "warning"
98
+ )
99
+
100
+ os.makedirs(self.ctx.temp_dir, exist_ok=True)
101
+ # Mark for cleanup immediately: a failure below must not leak the
102
+ # directory that was just created.
103
+ self._cleanup_needed = True
104
+ self._generate_project()
105
+
106
+ self.ctx.template_config = self._load_template_config()
107
+ debug_log(f"Created temporary directory at {self.ctx.temp_dir}", "debug")
108
+ return self
109
+ except Exception as e:
110
+ debug_log(f"Failed to setup template inspector: {e}", "error")
111
+ self._cleanup()
112
+ raise
113
+
114
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
115
+ """Remove the generated project directory."""
116
+ self._cleanup()
117
+
118
+ def _cleanup(self) -> None:
119
+ """Tear down Docker services and delete the temporary directory."""
120
+ if not (self._cleanup_needed and os.path.exists(self.ctx.temp_dir)):
121
+ return
122
+
123
+ try:
124
+ if self._cleanup_docker_services():
125
+ # Give the daemon a moment to release bind-mounted files.
126
+ time.sleep(3)
127
+ force_cleanup_directory(self.ctx.temp_dir)
128
+ except Exception as e:
129
+ debug_log(
130
+ f"Warning: Unexpected error during cleanup of "
131
+ f"{self.ctx.temp_dir}: {e}",
132
+ "warning",
133
+ )
134
+ try:
135
+ force_cleanup_directory(self.ctx.temp_dir)
136
+ except Exception:
137
+ pass
138
+ finally:
139
+ self._cleanup_needed = False
140
+
141
+ def _cleanup_docker_services(self) -> bool:
142
+ """Stop any Docker Compose stack started for this template.
143
+
144
+ Returns ``True`` when a teardown was actually attempted - templates
145
+ without a compose file skip the (slow) docker invocations entirely.
146
+ """
147
+ compose_files = [
148
+ name
149
+ for name in ("docker-compose.yml", "docker-compose.yaml", "compose.yml")
150
+ if os.path.exists(self.ctx.temp_path(name))
151
+ ]
152
+ if not compose_files:
153
+ return False
154
+
155
+ try:
156
+ DockerCompose(self.ctx.temp_dir, compose_files[0]).cleanup()
157
+ return True
158
+ except Exception as e:
159
+ debug_log(f"Failed to cleanup Docker services: {e}", "warning")
160
+ return False
161
+
162
+ def _generate_project(self) -> None:
163
+ """Produce the project to inspect through the real ``startdemo`` path.
164
+
165
+ Running the actual ``ProjectScaffolder`` (rather than re-implementing
166
+ copy + inject here) is the point: whatever generation does for a user
167
+ - placeholder substitution, the ``[tool.fastapi-fastkit]`` metadata
168
+ block, the template-only files it filters out - is exactly what the
169
+ checks then see. Virtualenv creation and dependency installation are
170
+ switched off, so no package manager is ever invoked.
171
+
172
+ A generation failure is propagated: there is nothing meaningful to
173
+ inspect when the template cannot even be deployed, and ``__enter__``
174
+ already removes the half-written directory.
175
+ """
176
+ template_path = self.ctx.template_path.resolve()
177
+ project_dir = Path(self.ctx.temp_dir).resolve()
178
+
179
+ scaffold_settings = SimpleNamespace(
180
+ FASTKIT_TEMPLATE_ROOT=str(template_path.parent),
181
+ USER_WORKSPACE=str(project_dir.parent),
182
+ PACKAGE_MANAGER_CONFIG=settings.PACKAGE_MANAGER_CONFIG,
183
+ )
184
+ options = ScaffoldOptions(
185
+ project_name=project_dir.name,
186
+ author=str(DUMMY_METADATA["author"]),
187
+ author_email=str(DUMMY_METADATA["author_email"]),
188
+ description=str(DUMMY_METADATA["description"]),
189
+ package_manager="uv",
190
+ template=template_path.name,
191
+ create_project_folder=True,
192
+ with_venv=False,
193
+ with_install=False,
194
+ )
195
+
196
+ ProjectScaffolder(scaffold_settings, options).run()
197
+
198
+ def _load_template_config(self) -> Optional[Dict[str, Any]]:
199
+ """Load ``template-config.yml`` from the template, if it ships one.
200
+
201
+ The file is read from the template directory rather than the generated
202
+ project: it is inspector-only metadata and is deliberately not copied
203
+ into a user's project by the transducer.
204
+ """
205
+ config_file = str(self.ctx.template_path / "template-config.yml-tpl")
206
+ if not os.path.exists(config_file):
207
+ # Legacy layout: some templates shipped the config without the
208
+ # ``-tpl`` marker, in which case it still lands in the copy.
209
+ config_file = self.ctx.temp_path("template-config.yml")
210
+ if not os.path.exists(config_file):
211
+ debug_log(
212
+ "No template-config.yml found, using default testing strategy", "info"
213
+ )
214
+ return None
215
+
216
+ try:
217
+ import yaml
218
+ except ImportError: # pragma: no cover - PyYAML is an optional extra
219
+ debug_log(
220
+ "PyYAML is not installed; ignoring template-config.yml", "warning"
221
+ )
222
+ return None
223
+
224
+ try:
225
+ with open(config_file, "r", encoding="utf-8") as f:
226
+ config = yaml.safe_load(f)
227
+ except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
228
+ debug_log(f"Failed to load template configuration: {e}", "warning")
229
+ return None
230
+
231
+ if not isinstance(config, dict):
232
+ debug_log("Invalid template configuration format", "warning")
233
+ return None
234
+
235
+ debug_log(
236
+ f"Loaded template configuration: {config.get('name', 'Unknown')}", "info"
237
+ )
238
+ return config
239
+
240
+ # -- checks ------------------------------------------------------------
241
+ def inspect_template(self) -> bool:
242
+ """Run every inspection step, stopping at the first failure.
243
+
244
+ :return: True if template is valid, False otherwise
245
+ """
246
+ check_list: List[Tuple[str, Callable[[], bool]]] = [
247
+ ("File Structure", self._check_file_structure),
248
+ ("File Extensions", self._check_file_extensions),
249
+ ("Test Suite Presence", self._check_tests_present),
250
+ ("Junk Files", self._check_no_junk_files),
251
+ ("Dependencies", self._check_dependencies),
252
+ ("Configuration Consistency", self._check_configuration_consistency),
253
+ ("FastAPI Implementation", self._check_fastapi_implementation),
254
+ ("Placeholder Substitution", self._check_no_placeholder_residue),
255
+ ("Template Tests", self._test_template),
256
+ ("Compile Check", self._check_compileall),
257
+ ("Type Check", self._check_mypy),
258
+ ("Smoke Test", self._check_smoke_test),
259
+ ("Dependency Freshness", self._check_dependency_freshness),
260
+ ]
261
+
262
+ for check_name, check_func in check_list:
263
+ debug_log(f"Running check: {check_name}", "info")
264
+ if not check_func():
265
+ debug_log(f"Check failed: {check_name}", "error")
266
+ return False
267
+ debug_log(f"Check passed: {check_name}", "info")
268
+
269
+ return True
270
+
271
+ def _check_file_structure(self) -> bool:
272
+ return checks.check_file_structure(self.ctx)
273
+
274
+ def _check_file_extensions(self) -> bool:
275
+ return checks.check_file_extensions(self.ctx)
276
+
277
+ def _check_dependencies(self) -> bool:
278
+ return checks.check_dependencies(self.ctx)
279
+
280
+ def _check_fastapi_implementation(self) -> bool:
281
+ return checks.check_fastapi_implementation(self.ctx)
282
+
283
+ def _check_tests_present(self) -> bool:
284
+ return checks.check_tests_present(self.ctx)
285
+
286
+ def _check_no_junk_files(self) -> bool:
287
+ return checks.check_no_junk_files(self.ctx)
288
+
289
+ def _check_no_placeholder_residue(self) -> bool:
290
+ return checks.check_no_placeholder_residue(self.ctx)
291
+
292
+ def _check_configuration_consistency(self) -> bool:
293
+ return consistency.check_configuration_consistency(self.ctx)
294
+
295
+ def _check_compileall(self) -> bool:
296
+ return lint.check_compileall(self.ctx)
297
+
298
+ def _check_mypy(self) -> bool:
299
+ return lint.check_mypy(self.ctx)
300
+
301
+ def _check_smoke_test(self) -> bool:
302
+ return smoke.check_smoke_test(self.ctx)
303
+
304
+ def _check_dependency_freshness(self) -> bool:
305
+ return freshness.check_dependency_freshness(self.ctx)
306
+
307
+ def _test_template(self) -> bool:
308
+ return strategies.run_template_tests(self.ctx)
309
+
310
+ def get_report(self) -> Dict[str, Any]:
311
+ """
312
+ Get inspection report with errors and warnings.
313
+
314
+ :return: Dictionary containing inspection results
315
+ """
316
+ return build_report(self.ctx)
317
+
318
+
319
+ def inspect_fastapi_template(
320
+ template_path: str,
321
+ temp_base_dir: Optional[str] = None,
322
+ options: Optional[InspectionOptions] = None,
323
+ ) -> Dict[str, Any]:
324
+ """
325
+ Convenience function to inspect a FastAPI template.
326
+
327
+ :param template_path: Path to the template to inspect
328
+ :param temp_base_dir: Base directory for temporary files (defaults to backend directory)
329
+ :param options: Optional switches (offline mode, smoke test, mypy, ...)
330
+ :return: Inspection report dictionary
331
+ """
332
+ template_name = Path(template_path).name
333
+ debug_log(
334
+ f"Starting template inspection for {template_name} at {template_path}", "info"
335
+ )
336
+
337
+ with TemplateInspector(template_path, temp_base_dir, options) as inspector:
338
+ is_valid = inspector.inspect_template()
339
+ report = inspector.get_report()
340
+ print_report(inspector.ctx, is_valid)
341
+
342
+ debug_log(
343
+ f"Template inspection completed for {template_name}. Valid: {is_valid}, "
344
+ f"Errors: {len(report['errors'])}, Warnings: {len(report['warnings'])}",
345
+ "info",
346
+ )
347
+ return report
@@ -0,0 +1,185 @@
1
+ # --------------------------------------------------------------------------
2
+ # Docker Compose orchestration for templates that declare requires_docker.
3
+ #
4
+ # ``DockerCompose`` owns every ``docker``/``docker-compose`` invocation so the
5
+ # test strategies stay free of process plumbing.
6
+ #
7
+ # @author bnbong
8
+ # --------------------------------------------------------------------------
9
+ import json
10
+ import subprocess
11
+ import time
12
+ from typing import Any, Dict, List, Optional, Sequence
13
+
14
+ from fastapi_fastkit.utils.logging import debug_log
15
+
16
+ COMPOSE_COMMAND = "docker-compose"
17
+ SHORT_TIMEOUT = 10
18
+ STATUS_TIMEOUT = 30
19
+ CLEANUP_TIMEOUT = 60
20
+ TEST_TIMEOUT = 300
21
+
22
+
23
+ class DockerCompose:
24
+ """Thin wrapper around the docker-compose CLI for one project directory."""
25
+
26
+ def __init__(self, project_dir: str, compose_file: str = "docker-compose.yml"):
27
+ self.project_dir = project_dir
28
+ self.compose_file = compose_file
29
+
30
+ def _run(
31
+ self, args: Sequence[str], timeout: int
32
+ ) -> Optional[subprocess.CompletedProcess[str]]:
33
+ """Run a command in the project directory, returning ``None`` on failure."""
34
+ try:
35
+ return subprocess.run(
36
+ list(args),
37
+ cwd=self.project_dir,
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=timeout,
41
+ )
42
+ except (subprocess.TimeoutExpired, OSError) as e:
43
+ debug_log(f"Command {' '.join(args)} failed: {e}", "warning")
44
+ return None
45
+
46
+ def _compose(
47
+ self, args: Sequence[str], timeout: int
48
+ ) -> Optional[subprocess.CompletedProcess[str]]:
49
+ """Run a docker-compose subcommand against the configured compose file."""
50
+ return self._run(
51
+ [COMPOSE_COMMAND, "-f", self.compose_file, *args], timeout=timeout
52
+ )
53
+
54
+ @staticmethod
55
+ def is_available() -> bool:
56
+ """Check that both docker and docker-compose respond."""
57
+ for command in (["docker", "--version"], [COMPOSE_COMMAND, "--version"]):
58
+ try:
59
+ result = subprocess.run(
60
+ command, capture_output=True, text=True, timeout=SHORT_TIMEOUT
61
+ )
62
+ except (subprocess.TimeoutExpired, OSError):
63
+ return False
64
+ if result.returncode != 0:
65
+ return False
66
+ return True
67
+
68
+ def _services(self, timeout: int) -> List[Dict[str, Any]]:
69
+ """Return the parsed ``docker-compose ps --format json`` entries."""
70
+ result = self._compose(["ps", "--format", "json"], timeout=timeout)
71
+ if result is None or result.returncode != 0:
72
+ return []
73
+
74
+ services: List[Dict[str, Any]] = []
75
+ for line in result.stdout.strip().split("\n"):
76
+ if not line.strip():
77
+ continue
78
+ try:
79
+ entry = json.loads(line)
80
+ except json.JSONDecodeError:
81
+ continue
82
+ if isinstance(entry, dict):
83
+ services.append(entry)
84
+ return services
85
+
86
+ def containers_running(self) -> bool:
87
+ """Check whether every container of the project is already up."""
88
+ result = self._compose(["ps", "-q"], timeout=STATUS_TIMEOUT)
89
+ if result is None or result.returncode != 0:
90
+ return False
91
+
92
+ container_ids = [line.strip() for line in result.stdout.strip().split("\n")]
93
+ container_ids = [cid for cid in container_ids if cid]
94
+ if not container_ids:
95
+ return False
96
+
97
+ for container_id in container_ids:
98
+ status = self._run(
99
+ ["docker", "inspect", "-f", "{{.State.Running}}", container_id],
100
+ timeout=SHORT_TIMEOUT,
101
+ )
102
+ if status is None or status.returncode != 0:
103
+ return False
104
+ if status.stdout.strip() != "true":
105
+ return False
106
+ return True
107
+
108
+ def up(self, timeout: int) -> subprocess.CompletedProcess[str]:
109
+ """Build and start the compose services in the background."""
110
+ result = self._compose(["up", "-d", "--build"], timeout=timeout)
111
+ if result is None:
112
+ raise subprocess.TimeoutExpired(COMPOSE_COMMAND, timeout)
113
+ return result
114
+
115
+ def wait_until_healthy(self, timeout: int) -> None:
116
+ """Block until every service reports ``running`` or ``timeout`` elapses."""
117
+ debug_log("Waiting for services to be healthy...", "info")
118
+ deadline = time.time() + timeout
119
+
120
+ while time.time() < deadline:
121
+ services = self._services(timeout=SHORT_TIMEOUT)
122
+ if services and all(
123
+ service.get("State") == "running" for service in services
124
+ ):
125
+ debug_log("All services are running", "info")
126
+ # Give the application a moment to finish booting.
127
+ time.sleep(10)
128
+ return
129
+
130
+ debug_log("Services not ready yet, waiting...", "info")
131
+ time.sleep(5)
132
+
133
+ debug_log(
134
+ f"Services did not become healthy within {timeout} seconds", "warning"
135
+ )
136
+
137
+ def verify_services_running(self) -> Optional[str]:
138
+ """Return ``None`` when db and app are up, else a descriptive error."""
139
+ services = self._services(timeout=STATUS_TIMEOUT)
140
+ if not services:
141
+ return "Failed to check service status"
142
+
143
+ db_running = False
144
+ app_running = False
145
+ for service in services:
146
+ name = str(service.get("Name", ""))
147
+ state = str(service.get("State", ""))
148
+ debug_log(f"Service {name}: {state}", "info")
149
+ if "db" in name and state == "running":
150
+ db_running = True
151
+ elif "app" in name and state == "running":
152
+ app_running = True
153
+
154
+ if not db_running:
155
+ return "Database service is not running"
156
+ if not app_running:
157
+ logs = self._compose(
158
+ ["logs", "--tail", "50", "app"], timeout=STATUS_TIMEOUT
159
+ )
160
+ if logs is not None and logs.returncode == 0:
161
+ return (
162
+ "Application service is not running. " f"Logs: {logs.stdout[-500:]}"
163
+ )
164
+ return "Application service is not running"
165
+
166
+ debug_log("All required services are running", "info")
167
+ return None
168
+
169
+ def exec_tests(
170
+ self, use_test_script: bool
171
+ ) -> Optional[subprocess.CompletedProcess[str]]:
172
+ """Run the template's tests inside the app container."""
173
+ if use_test_script:
174
+ command = ["exec", "-T", "app", "bash", "scripts/test.sh"]
175
+ else:
176
+ command = ["exec", "-T", "app", "python", "-m", "pytest", "tests/", "-v"]
177
+ return self._compose(command, timeout=TEST_TIMEOUT)
178
+
179
+ def cleanup(self) -> None:
180
+ """Tear down services and volumes, ignoring any failure."""
181
+ debug_log("Cleaning up Docker services", "info")
182
+ self._run(
183
+ [COMPOSE_COMMAND, "down", "-v", "--remove-orphans"], timeout=CLEANUP_TIMEOUT
184
+ )
185
+ self._run(["docker", "system", "prune", "-f"], timeout=STATUS_TIMEOUT)
@@ -0,0 +1,145 @@
1
+ # --------------------------------------------------------------------------
2
+ # Dependency freshness reporting.
3
+ #
4
+ # Each pinned dependency is compared against the latest release published on
5
+ # PyPI. Findings are warnings only - a template lagging behind upstream is
6
+ # worth surfacing but never a reason to fail an inspection. Network failures
7
+ # are skipped silently, and ``--offline`` disables the lookups entirely.
8
+ #
9
+ # @author bnbong
10
+ # --------------------------------------------------------------------------
11
+ import json
12
+ import re
13
+ import tomllib
14
+ import urllib.error
15
+ import urllib.request
16
+ from typing import Dict, Optional, Tuple
17
+
18
+ from fastapi_fastkit.backend.package_managers.poetry_manager import (
19
+ _parse_pip_requirement,
20
+ )
21
+ from fastapi_fastkit.utils.logging import debug_log
22
+
23
+ from .context import InspectionContext
24
+
25
+ PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
26
+ PYPI_TIMEOUT = 5
27
+ _VERSION_RE = re.compile(r"(\d+)\.(\d+)")
28
+ #: Only exact/lower-bound pins carry a meaningful "current version".
29
+ _PIN_OPERATORS = ("==", ">=", "~=")
30
+
31
+
32
+ def _pinned_version(requirement: str) -> Tuple[str, str]:
33
+ """Return ``(package name, pinned version)`` for a requirement string.
34
+
35
+ Version is empty when the requirement is unpinned or uses an operator
36
+ that does not identify a concrete current version (e.g. ``<2``).
37
+ """
38
+ name, _extras, version_spec, _marker = _parse_pip_requirement(requirement)
39
+ if not name or not version_spec:
40
+ return name.lower() if name else "", ""
41
+ for operator in _PIN_OPERATORS:
42
+ if version_spec.startswith(operator):
43
+ return name.lower(), version_spec[len(operator) :].strip()
44
+ return name.lower(), ""
45
+
46
+
47
+ def _parse_version(version: str) -> Optional[Tuple[int, int]]:
48
+ """Extract a (major, minor) pair from a version string."""
49
+ match = _VERSION_RE.match(version.strip())
50
+ if not match:
51
+ return None
52
+ return int(match.group(1)), int(match.group(2))
53
+
54
+
55
+ def fetch_latest_version(package: str) -> Optional[str]:
56
+ """Return the latest version of ``package`` on PyPI, or ``None`` on failure."""
57
+ url = PYPI_JSON_URL.format(package=package)
58
+ try:
59
+ with urllib.request.urlopen(url, timeout=PYPI_TIMEOUT) as response:
60
+ payload = json.loads(response.read().decode("utf-8"))
61
+ except (urllib.error.URLError, OSError, ValueError, TimeoutError) as e:
62
+ debug_log(f"PyPI lookup failed for {package}: {e}", "debug")
63
+ return None
64
+ version = payload.get("info", {}).get("version")
65
+ return str(version) if version else None
66
+
67
+
68
+ def _collect_pins(ctx: InspectionContext) -> Dict[str, str]:
69
+ """Map package name -> pinned version from the template's metadata files."""
70
+ pins: Dict[str, str] = {}
71
+
72
+ requirements_path = ctx.template_path / "requirements.txt-tpl"
73
+ if requirements_path.exists():
74
+ try:
75
+ with open(requirements_path, encoding="utf-8") as f:
76
+ lines = f.read().splitlines()
77
+ except (OSError, UnicodeDecodeError):
78
+ lines = []
79
+ for line in lines:
80
+ line = line.strip()
81
+ if not line or line.startswith("#"):
82
+ continue
83
+ name, version = _pinned_version(line)
84
+ if name and version:
85
+ pins.setdefault(name, version)
86
+
87
+ pyproject_path = ctx.template_path / "pyproject.toml-tpl"
88
+ if pyproject_path.exists():
89
+ try:
90
+ with open(pyproject_path, "rb") as f:
91
+ data = tomllib.load(f)
92
+ except (OSError, ValueError):
93
+ data = {}
94
+ for dep in data.get("project", {}).get("dependencies", []) or []:
95
+ if not isinstance(dep, str):
96
+ continue
97
+ name, version = _pinned_version(dep)
98
+ if name and version:
99
+ pins.setdefault(name, version)
100
+
101
+ return pins
102
+
103
+
104
+ def check_dependency_freshness(ctx: InspectionContext) -> bool:
105
+ """Warn about dependencies that lag behind their latest PyPI release.
106
+
107
+ Always returns ``True``: freshness is advisory.
108
+ """
109
+ if ctx.options.offline:
110
+ debug_log("Offline mode - skipping dependency freshness check", "info")
111
+ return True
112
+
113
+ pins = _collect_pins(ctx)
114
+ if not pins:
115
+ debug_log("No pinned dependencies found for freshness check", "info")
116
+ return True
117
+
118
+ checked = 0
119
+ for package, pinned in sorted(pins.items()):
120
+ latest = fetch_latest_version(package)
121
+ if latest is None:
122
+ continue
123
+ checked += 1
124
+ current_parts = _parse_version(pinned)
125
+ latest_parts = _parse_version(latest)
126
+ if current_parts is None or latest_parts is None:
127
+ continue
128
+ if latest_parts[0] > current_parts[0]:
129
+ ctx.add_warning(
130
+ f"{package} is {latest_parts[0] - current_parts[0]} major "
131
+ f"version(s) behind: pinned {pinned}, latest {latest}"
132
+ )
133
+ elif latest_parts[0] == current_parts[0] and latest_parts[1] > current_parts[1]:
134
+ ctx.add_warning(
135
+ f"{package} is {latest_parts[1] - current_parts[1]} minor "
136
+ f"version(s) behind: pinned {pinned}, latest {latest}"
137
+ )
138
+
139
+ if checked == 0:
140
+ debug_log(
141
+ "Dependency freshness check skipped - no PyPI responses received", "info"
142
+ )
143
+ else:
144
+ debug_log(f"Dependency freshness check completed ({checked} packages)", "info")
145
+ return True