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