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