FastAPI-fastkit 1.0.2__py3-none-any.whl → 1.1.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 (174) hide show
  1. fastapi_fastkit/__init__.py +1 -10
  2. fastapi_fastkit/backend/inspector.py +933 -83
  3. fastapi_fastkit/backend/main.py +640 -482
  4. fastapi_fastkit/backend/package_managers/__init__.py +22 -0
  5. fastapi_fastkit/backend/package_managers/base.py +150 -0
  6. fastapi_fastkit/backend/package_managers/factory.py +151 -0
  7. fastapi_fastkit/backend/package_managers/pdm_manager.py +275 -0
  8. fastapi_fastkit/backend/package_managers/pip_manager.py +205 -0
  9. fastapi_fastkit/backend/package_managers/poetry_manager.py +378 -0
  10. fastapi_fastkit/backend/package_managers/uv_manager.py +327 -0
  11. fastapi_fastkit/backend/transducer.py +210 -106
  12. fastapi_fastkit/cli.py +702 -664
  13. fastapi_fastkit/core/exceptions.py +14 -3
  14. fastapi_fastkit/core/settings.py +181 -155
  15. fastapi_fastkit/fastapi_project_template/README.md +90 -90
  16. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.env-tpl +1 -1
  17. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.gitignore-tpl +30 -30
  18. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +107 -107
  19. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +36 -36
  20. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/format.sh-tpl +5 -5
  21. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/lint.sh-tpl +5 -5
  22. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/run-server.sh-tpl +8 -8
  23. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/test.sh-tpl +6 -6
  24. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.cfg-tpl +14 -14
  25. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +25 -25
  26. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +9 -9
  27. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +57 -57
  28. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +64 -64
  29. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +21 -21
  30. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +26 -26
  31. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/mocks/mock_items.json-tpl +8 -8
  32. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +22 -22
  33. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +42 -42
  34. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/test_items.py-tpl +48 -48
  35. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.env-tpl +1 -1
  36. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.gitignore-tpl +191 -191
  37. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl +156 -156
  38. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +36 -36
  39. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/format.sh-tpl +5 -5
  40. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/lint.sh-tpl +5 -5
  41. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/run-server.sh-tpl +8 -8
  42. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/test.sh-tpl +6 -6
  43. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.cfg-tpl +12 -12
  44. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +27 -27
  45. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +9 -9
  46. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +172 -172
  47. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +64 -64
  48. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +21 -21
  49. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl +103 -103
  50. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +72 -72
  51. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +39 -39
  52. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/mocks/mock_items.json-tpl +8 -8
  53. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +49 -49
  54. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +22 -22
  55. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/utils/documents.py-tpl +20 -20
  56. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +42 -42
  57. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/test_items.py-tpl +61 -61
  58. fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl +101 -101
  59. fastapi_fastkit/fastapi_project_template/fastapi-default/scripts/run-server.sh-tpl +8 -8
  60. fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +22 -22
  61. fastapi_fastkit/fastapi_project_template/fastapi-default/src/mocks/mock_items.json-tpl +8 -8
  62. fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +30 -30
  63. fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +44 -44
  64. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.env-tpl +1 -1
  65. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.gitignore-tpl +30 -30
  66. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +23 -23
  67. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +119 -119
  68. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +36 -36
  69. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/format.sh-tpl +5 -5
  70. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/lint.sh-tpl +5 -5
  71. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/run-server.sh-tpl +8 -8
  72. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/test.sh-tpl +6 -6
  73. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.cfg-tpl +13 -13
  74. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +22 -22
  75. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +6 -6
  76. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +54 -54
  77. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +64 -64
  78. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +15 -15
  79. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +26 -26
  80. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/mocks/mock_items.json-tpl +8 -8
  81. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +19 -19
  82. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +30 -30
  83. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +44 -44
  84. fastapi_fastkit/fastapi_project_template/fastapi-empty/.env-tpl +1 -1
  85. fastapi_fastkit/fastapi_project_template/fastapi-empty/.gitignore-tpl +30 -0
  86. fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +85 -0
  87. fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +9 -0
  88. fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.cfg-tpl +13 -0
  89. fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +22 -22
  90. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +75 -75
  91. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +48 -36
  92. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/__init__.py-tpl +0 -0
  93. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +16 -0
  94. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +37 -0
  95. fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl +29 -0
  96. fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +249 -0
  97. fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +40 -0
  98. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/format.sh-tpl +12 -0
  99. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/lint.sh-tpl +18 -0
  100. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/run-server.sh-tpl +9 -0
  101. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/test.sh-tpl +9 -0
  102. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl +90 -0
  103. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +51 -0
  104. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/__init__.py-tpl +4 -0
  105. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/__init__.py-tpl +3 -0
  106. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +14 -0
  107. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/__init__.py-tpl +3 -0
  108. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +134 -0
  109. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +189 -0
  110. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/__init__.py-tpl +3 -0
  111. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +88 -0
  112. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/__init__.py-tpl +3 -0
  113. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +60 -0
  114. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +66 -0
  115. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/__init__.py-tpl +3 -0
  116. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/router.py-tpl +100 -0
  117. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/__init__.py-tpl +3 -0
  118. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +67 -0
  119. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/__init__.py-tpl +3 -0
  120. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +49 -0
  121. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +101 -0
  122. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +96 -0
  123. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +81 -0
  124. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl +9 -9
  125. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.gitignore-tpl +30 -30
  126. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +23 -23
  127. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl +144 -144
  128. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/alembic.ini-tpl +119 -119
  129. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/docker-compose.yml-tpl +44 -44
  130. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +41 -41
  131. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/format.sh-tpl +5 -5
  132. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/lint.sh-tpl +5 -5
  133. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/pre-start.sh-tpl +16 -16
  134. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/run-server.sh-tpl +8 -8
  135. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/test.sh-tpl +10 -10
  136. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.cfg-tpl +13 -13
  137. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +25 -25
  138. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/README-tpl +1 -1
  139. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +85 -85
  140. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/script.py.mako-tpl +26 -26
  141. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +37 -37
  142. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +11 -11
  143. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/deps.py-tpl +19 -19
  144. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +109 -109
  145. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +88 -88
  146. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +53 -53
  147. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +102 -102
  148. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +45 -45
  149. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +54 -54
  150. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/backend_pre_start.py-tpl +39 -39
  151. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/init_data.py-tpl +23 -23
  152. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/tests_pre_start.py-tpl +39 -39
  153. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/template-config.yml-tpl +24 -0
  154. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +63 -63
  155. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +44 -44
  156. fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +64 -64
  157. fastapi_fastkit/fastapi_project_template/modules/crud/new_route.py-tpl +29 -29
  158. fastapi_fastkit/fastapi_project_template/modules/schemas/new_route.py-tpl +23 -23
  159. fastapi_fastkit/utils/logging.py +64 -28
  160. fastapi_fastkit/utils/main.py +180 -140
  161. fastapi_fastkit-1.1.0.dist-info/METADATA +182 -0
  162. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/RECORD +165 -129
  163. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/WHEEL +1 -1
  164. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/licenses/LICENSE +21 -21
  165. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/.DS_Store +0 -0
  166. fastapi_fastkit/logs/fastkit_debug_20250702_161423.log +0 -21
  167. fastapi_fastkit/logs/fastkit_debug_20250702_162706.log +0 -20
  168. fastapi_fastkit/logs/fastkit_debug_20250702_163010.log +0 -21
  169. fastapi_fastkit/logs/fastkit_debug_20250702_163526.log +0 -21
  170. fastapi_fastkit/logs/fastkit_debug_20250702_164053.log +0 -21
  171. fastapi_fastkit/logs/fastkit_debug_20250702_165220.log +0 -21
  172. fastapi_fastkit/logs/fastkit_debug_20250702_165647.log +0 -21
  173. fastapi_fastkit-1.0.2.dist-info/METADATA +0 -320
  174. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,6 @@
1
1
  # --------------------------------------------------------------------------
2
- # The Module inspect FastAPI template is appropriate
3
- #
4
- # This module will be read by Github Action when contributor
2
+ # The Module defines FastAPI template inspector for template validation.
3
+ # This module will be used by maintainers of FastAPI-fastkit when anyone
5
4
  # makes a PR of adding new FastAPI template.
6
5
  #
7
6
  # First, check a FastAPI template is formed a valid template form with .py-tpl extension
@@ -25,7 +24,9 @@ import shutil
25
24
  import subprocess
26
25
  import sys
27
26
  from pathlib import Path
28
- from typing import Any, Dict, List
27
+ from typing import Any, Callable, Dict, List, Optional, Tuple
28
+
29
+ import yaml # type: ignore
29
30
 
30
31
  from fastapi_fastkit.backend.main import (
31
32
  create_venv,
@@ -33,38 +34,178 @@ from fastapi_fastkit.backend.main import (
33
34
  install_dependencies,
34
35
  )
35
36
  from fastapi_fastkit.backend.transducer import copy_and_convert_template
37
+ from fastapi_fastkit.core.settings import settings
38
+ from fastapi_fastkit.utils.logging import debug_log, get_logger
36
39
  from fastapi_fastkit.utils.main import print_error, print_success, print_warning
37
40
 
41
+ logger = get_logger(__name__)
42
+
38
43
 
39
44
  class TemplateInspector:
45
+ """
46
+ Template inspector for validating FastAPI templates.
47
+
48
+ Uses context manager protocol for proper resource cleanup.
49
+ """
50
+
40
51
  def __init__(self, template_path: str):
41
52
  self.template_path = Path(template_path)
42
53
  self.errors: List[str] = []
43
54
  self.warnings: List[str] = []
44
55
  self.temp_dir = os.path.join(os.path.dirname(__file__), "temp")
56
+ self._cleanup_needed = False
57
+ self.template_config: Optional[Dict[str, Any]] = None
58
+
59
+ def __enter__(self) -> "TemplateInspector":
60
+ """Enter context manager - create temp directory and copy template."""
61
+ try:
62
+ os.makedirs(self.temp_dir, exist_ok=True)
63
+ copy_and_convert_template(str(self.template_path), self.temp_dir)
64
+ self._cleanup_needed = True
65
+ self.template_config = self._load_template_config()
66
+ debug_log(f"Created temporary directory at {self.temp_dir}", "debug")
67
+ return self
68
+ except Exception as e:
69
+ debug_log(f"Failed to setup template inspector: {e}", "error")
70
+ self._cleanup()
71
+ raise
72
+
73
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
74
+ """Exit context manager - cleanup temp directory."""
75
+ self._cleanup()
76
+
77
+ def _cleanup(self) -> None:
78
+ """Cleanup temp directory if it exists and cleanup is needed."""
79
+ if self._cleanup_needed and os.path.exists(self.temp_dir):
80
+ try:
81
+ shutil.rmtree(self.temp_dir)
82
+ debug_log(f"Cleaned up temp directory {self.temp_dir}", "debug")
83
+ except OSError as e:
84
+ debug_log(
85
+ f"Failed to cleanup temp directory {self.temp_dir}: {e}", "warning"
86
+ )
87
+ finally:
88
+ self._cleanup_needed = False
89
+
90
+ def _load_template_config(self) -> Optional[Dict[str, Any]]:
91
+ """Load template configuration from template-config.yml if available."""
92
+ config_file = os.path.join(self.temp_dir, "template-config.yml")
93
+ if not os.path.exists(config_file):
94
+ debug_log(
95
+ "No template-config.yml found, using default testing strategy", "info"
96
+ )
97
+ return None
98
+
99
+ try:
100
+ with open(config_file, "r", encoding="utf-8") as f:
101
+ config = yaml.safe_load(f)
102
+ if isinstance(config, dict):
103
+ debug_log(
104
+ f"Loaded template configuration: {config.get('name', 'Unknown')}",
105
+ "info",
106
+ )
107
+ return config
108
+ else:
109
+ debug_log("Invalid template configuration format", "warning")
110
+ return None
111
+ except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
112
+ debug_log(f"Failed to load template configuration: {e}", "warning")
113
+ return None
114
+
115
+ def _check_docker_available(self) -> bool:
116
+ """Check if Docker and Docker Compose are available."""
117
+ try:
118
+ # Check Docker
119
+ result = subprocess.run(
120
+ ["docker", "--version"], capture_output=True, text=True, timeout=10
121
+ )
122
+ if result.returncode != 0:
123
+ return False
124
+
125
+ # Check Docker Compose
126
+ result = subprocess.run(
127
+ ["docker-compose", "--version"],
128
+ capture_output=True,
129
+ text=True,
130
+ timeout=10,
131
+ )
132
+ return result.returncode == 0
133
+ except (
134
+ subprocess.CalledProcessError,
135
+ subprocess.TimeoutExpired,
136
+ FileNotFoundError,
137
+ ):
138
+ return False
45
139
 
46
- # Create temp directory and copy template
47
- os.makedirs(self.temp_dir, exist_ok=True)
48
- copy_and_convert_template(str(self.template_path), self.temp_dir)
140
+ def _check_containers_running(self, compose_file: str) -> bool:
141
+ """Check if Docker Compose containers are already running."""
142
+ try:
143
+ result = subprocess.run(
144
+ ["docker-compose", "-f", compose_file, "ps", "-q"],
145
+ cwd=self.temp_dir,
146
+ capture_output=True,
147
+ text=True,
148
+ timeout=30,
149
+ )
150
+
151
+ if result.returncode != 0:
152
+ return False
153
+
154
+ # Check if any containers are running
155
+ container_ids = result.stdout.strip().split("\n")
156
+ if not container_ids or container_ids == [""]:
157
+ return False
49
158
 
50
- def __del__(self) -> None:
51
- """Cleanup temp directory when inspector is destroyed."""
52
- if os.path.exists(self.temp_dir):
53
- shutil.rmtree(self.temp_dir)
159
+ # Check if containers are actually running (not just exist)
160
+ for container_id in container_ids:
161
+ if container_id.strip():
162
+ status_result = subprocess.run(
163
+ [
164
+ "docker",
165
+ "inspect",
166
+ "-f",
167
+ "{{.State.Running}}",
168
+ container_id.strip(),
169
+ ],
170
+ capture_output=True,
171
+ text=True,
172
+ timeout=10,
173
+ )
174
+ if (
175
+ status_result.returncode != 0
176
+ or status_result.stdout.strip() != "true"
177
+ ):
178
+ return False
179
+
180
+ return True
181
+ except (
182
+ subprocess.CalledProcessError,
183
+ subprocess.TimeoutExpired,
184
+ FileNotFoundError,
185
+ ):
186
+ return False
54
187
 
55
188
  def inspect_template(self) -> bool:
56
- """Inspect the template is valid FastAPI application."""
57
- checks = [
58
- self._check_file_structure,
59
- self._check_file_extensions,
60
- self._check_dependencies,
61
- self._check_fastapi_implementation,
62
- self._test_template,
189
+ """
190
+ Inspect the template to validate it's a proper FastAPI application.
191
+
192
+ :return: True if template is valid, False otherwise
193
+ """
194
+ checks: List[Tuple[str, Callable[[], bool]]] = [
195
+ ("File Structure", self._check_file_structure),
196
+ ("File Extensions", self._check_file_extensions),
197
+ ("Dependencies", self._check_dependencies),
198
+ ("FastAPI Implementation", self._check_fastapi_implementation),
199
+ ("Template Tests", self._test_template),
63
200
  ]
64
201
 
65
- for check in checks:
66
- if not check():
202
+ for check_name, check_func in checks:
203
+ debug_log(f"Running check: {check_name}", "info")
204
+ if not check_func():
205
+ debug_log(f"Check failed: {check_name}", "error")
67
206
  return False
207
+ debug_log(f"Check passed: {check_name}", "info")
208
+
68
209
  return True
69
210
 
70
211
  def _check_file_structure(self) -> bool:
@@ -78,16 +219,24 @@ class TemplateInspector:
78
219
 
79
220
  for path in required_paths:
80
221
  if not (self.template_path / path).exists():
81
- self.errors.append(f"Missing required path: {path}")
222
+ error_msg = f"Missing required path: {path}"
223
+ self.errors.append(error_msg)
224
+ debug_log(f"File structure check failed: {error_msg}", "error")
82
225
  return False
226
+
227
+ debug_log("File structure check passed", "info")
83
228
  return True
84
229
 
85
230
  def _check_file_extensions(self) -> bool:
86
231
  """Check all Python files have .py-tpl extension."""
87
232
  for path in self.template_path.rglob("*"):
88
233
  if path.is_file() and path.suffix == ".py":
89
- self.errors.append(f"Found .py file instead of .py-tpl: {path}")
234
+ error_msg = f"Found .py file instead of .py-tpl: {path}"
235
+ self.errors.append(error_msg)
236
+ debug_log(f"File extension check failed: {error_msg}", "error")
90
237
  return False
238
+
239
+ debug_log("File extension check passed", "info")
91
240
  return True
92
241
 
93
242
  def _check_dependencies(self) -> bool:
@@ -96,108 +245,809 @@ class TemplateInspector:
96
245
  setup_path = self.template_path / "setup.py-tpl"
97
246
 
98
247
  if not req_path.exists():
99
- self.errors.append("requirements.txt-tpl not found")
248
+ error_msg = "requirements.txt-tpl not found"
249
+ self.errors.append(error_msg)
250
+ debug_log(f"Dependencies check failed: {error_msg}", "error")
100
251
  return False
101
252
  if not setup_path.exists():
102
- self.errors.append("setup.py-tpl not found")
253
+ error_msg = "setup.py-tpl not found"
254
+ self.errors.append(error_msg)
255
+ debug_log(f"Dependencies check failed: {error_msg}", "error")
103
256
  return False
104
257
 
105
- with open(req_path) as f:
106
- deps = f.read().splitlines()
107
- package_names = [dep.split("==")[0] for dep in deps if dep]
108
- if "fastapi" not in package_names:
109
- self.errors.append(
110
- "FastAPI dependency not found in requirements.txt-tpl"
111
- )
112
- return False
258
+ try:
259
+ with open(req_path, encoding="utf-8") as f:
260
+ deps = f.read().splitlines()
261
+ package_names = [dep.split("==")[0] for dep in deps if dep]
262
+ debug_log(f"Found dependencies: {package_names}", "debug")
263
+ if "fastapi" not in package_names:
264
+ error_msg = "FastAPI dependency not found in requirements.txt-tpl"
265
+ self.errors.append(error_msg)
266
+ debug_log(f"Dependencies check failed: {error_msg}", "error")
267
+ return False
268
+ except (OSError, UnicodeDecodeError) as e:
269
+ error_msg = f"Error reading requirements.txt-tpl: {e}"
270
+ self.errors.append(error_msg)
271
+ debug_log(f"Dependencies check failed: {error_msg}", "error")
272
+ return False
273
+
274
+ debug_log("Dependencies check passed", "info")
113
275
  return True
114
276
 
115
277
  def _check_fastapi_implementation(self) -> bool:
116
278
  """Check if the template has a proper FastAPI server implementation."""
117
- core_modules = find_template_core_modules(self.temp_dir)
279
+ try:
280
+ core_modules = find_template_core_modules(self.temp_dir)
281
+ debug_log(f"Found core modules: {core_modules}", "debug")
282
+
283
+ if not core_modules["main"]:
284
+ error_msg = "main.py not found in template"
285
+ self.errors.append(error_msg)
286
+ debug_log(f"FastAPI implementation check failed: {error_msg}", "error")
287
+ return False
118
288
 
119
- if not core_modules["main"]:
120
- self.errors.append("main.py not found in template")
289
+ with open(core_modules["main"], encoding="utf-8") as f:
290
+ content = f.read()
291
+ if "FastAPI" not in content or "app" not in content:
292
+ error_msg = "FastAPI app creation not found in main.py"
293
+ self.errors.append(error_msg)
294
+ debug_log(
295
+ f"FastAPI implementation check failed: {error_msg}", "error"
296
+ )
297
+ debug_log(f"main.py content preview: {content[:200]}...", "debug")
298
+ return False
299
+ except (OSError, UnicodeDecodeError) as e:
300
+ error_msg = f"Error checking FastAPI implementation: {e}"
301
+ self.errors.append(error_msg)
302
+ debug_log(f"FastAPI implementation check failed: {error_msg}", "error")
121
303
  return False
122
304
 
123
- with open(core_modules["main"]) as f:
124
- content = f.read()
125
- if "FastAPI" not in content or "app" not in content:
126
- self.errors.append("FastAPI app creation not found in main.py")
127
- return False
305
+ debug_log("FastAPI implementation check passed", "info")
128
306
  return True
129
307
 
130
308
  def _test_template(self) -> bool:
131
- """Run the tests in the converted template directory."""
132
- test_dir = Path(self.temp_dir) / "tests"
133
- if not test_dir.exists():
134
- self.errors.append("Tests directory not found")
309
+ """Run tests on the template using appropriate strategy based on configuration."""
310
+ test_dir = os.path.join(self.temp_dir, "tests")
311
+ if not os.path.exists(test_dir):
312
+ warning_msg = "No tests directory found"
313
+ self.warnings.append(warning_msg)
314
+ debug_log(f"Template warning: {warning_msg}", "warning")
315
+ return True
316
+
317
+ # Determine test strategy based on template configuration
318
+ if self.template_config and self.template_config.get("requires_docker", False):
319
+ return self._test_with_docker_strategy()
320
+ else:
321
+ return self._test_with_standard_strategy()
322
+
323
+ def _test_with_docker_strategy(self) -> bool:
324
+ """Run tests using Docker Compose strategy."""
325
+ docker_available = self._check_docker_available()
326
+
327
+ if not docker_available:
328
+ debug_log("Docker not available, trying fallback strategy", "warning")
329
+ warning_msg = "Docker not available, using fallback testing strategy"
330
+ self.warnings.append(warning_msg)
331
+ debug_log(f"Template warning: {warning_msg}", "warning")
332
+ return self._test_with_fallback_strategy()
333
+
334
+ try:
335
+ # Set up environment variables
336
+ self._setup_test_environment()
337
+
338
+ # Run Docker Compose based tests
339
+ testing_config = (
340
+ self.template_config.get("testing", {}) if self.template_config else {}
341
+ )
342
+ compose_file = testing_config.get("compose_file", "docker-compose.yml")
343
+ timeout = testing_config.get("health_check_timeout", 120)
344
+
345
+ # Check if containers are already running
346
+ if not self._check_containers_running(compose_file):
347
+ debug_log("Starting Docker Compose services for testing", "info")
348
+
349
+ # Start services
350
+ result = subprocess.run(
351
+ ["docker-compose", "-f", compose_file, "up", "-d", "--build"],
352
+ cwd=self.temp_dir,
353
+ capture_output=True,
354
+ text=True,
355
+ timeout=timeout,
356
+ )
357
+
358
+ if result.returncode != 0:
359
+ self.errors.append(
360
+ f"Failed to start Docker services: {result.stderr}"
361
+ )
362
+ return False
363
+
364
+ # Wait for services to be healthy
365
+ self._wait_for_services_healthy(compose_file, timeout)
366
+ else:
367
+ debug_log(
368
+ "Docker Compose services already running, skipping startup", "info"
369
+ )
370
+
371
+ # Verify services are actually running before attempting tests
372
+ if not self._verify_services_running(compose_file):
373
+ self.errors.append("Services failed to start properly")
374
+ return False
375
+
376
+ # Run tests using docker exec
377
+ test_result = self._run_docker_exec_tests(compose_file)
378
+
379
+ return test_result
380
+
381
+ except subprocess.TimeoutExpired:
382
+ self.errors.append("Docker Compose setup timed out")
135
383
  return False
384
+ except Exception as e:
385
+ self.errors.append(f"Unexpected error during Docker testing: {e}")
386
+ return False
387
+ finally:
388
+ # Clean up Docker services
389
+ self._cleanup_docker_services()
136
390
 
391
+ def _test_with_standard_strategy(self) -> bool:
392
+ """Run tests using standard virtual environment strategy."""
137
393
  try:
138
- # Create virtual environment
394
+ # Create virtual environment for testing
139
395
  venv_path = create_venv(self.temp_dir)
396
+ install_dependencies(self.temp_dir, venv_path)
397
+
398
+ # Check if scripts/test.sh exists
399
+ test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
400
+ if os.path.exists(test_script_path):
401
+ debug_log("Found scripts/test.sh, using template test script", "info")
402
+ result = self._run_test_script(test_script_path, venv_path)
403
+ else:
404
+ debug_log("No scripts/test.sh found, running pytest directly", "info")
405
+ result = self._run_pytest_directly(venv_path)
406
+
407
+ if result.returncode != 0:
408
+ error_msg = f"Tests failed with return code {result.returncode}\n"
409
+ if result.stderr:
410
+ error_msg += f"STDERR:\n{result.stderr}\n"
411
+ if result.stdout:
412
+ error_msg += f"STDOUT:\n{result.stdout}\n"
413
+ self.errors.append(error_msg)
414
+ debug_log(f"Standard strategy tests failed: {error_msg}", "error")
415
+ return False
416
+
417
+ debug_log("All tests passed successfully", "info")
418
+ return True
419
+
420
+ except subprocess.TimeoutExpired:
421
+ self.errors.append("Tests timed out after 5 minutes")
422
+ return False
423
+ except subprocess.CalledProcessError as e:
424
+ self.errors.append(f"Error running tests: {e}")
425
+ return False
426
+ except Exception as e:
427
+ self.errors.append(f"Unexpected error during testing: {e}")
428
+ return False
429
+
430
+ def _fix_script_line_endings(self, script_path: str) -> None:
431
+ """Fix line endings in script file (convert Windows \r\n to Unix \n)."""
432
+ try:
433
+ with open(script_path, "rb") as f:
434
+ content = f.read()
435
+
436
+ # Convert Windows line endings to Unix
437
+ content = content.replace(b"\r\n", b"\n")
438
+ content = content.replace(b"\r", b"\n")
439
+
440
+ with open(script_path, "wb") as f:
441
+ f.write(content)
442
+
443
+ debug_log(f"Fixed line endings in {script_path}", "debug")
444
+ except Exception as e:
445
+ debug_log(f"Failed to fix line endings in {script_path}: {e}", "warning")
446
+
447
+ def _fix_all_script_line_endings(self) -> None:
448
+ """Fix line endings in all shell scripts in the temp directory."""
449
+ script_patterns = ["*.sh", "*.bash"]
450
+
451
+ for pattern in script_patterns:
452
+ for root, dirs, files in os.walk(self.temp_dir):
453
+ for file in files:
454
+ if file.endswith((".sh", ".bash")):
455
+ script_path = os.path.join(root, file)
456
+ self._fix_script_line_endings(script_path)
457
+
458
+ debug_log("Fixed line endings for all shell scripts", "info")
459
+
460
+ def _run_test_script(
461
+ self, test_script_path: str, venv_path: str
462
+ ) -> subprocess.CompletedProcess[str]:
463
+ """Run the template's test script."""
464
+ # Fix line endings (convert Windows \r\n to Unix \n)
465
+ self._fix_script_line_endings(test_script_path)
466
+
467
+ # Make script executable
468
+ os.chmod(test_script_path, 0o755)
469
+
470
+ # Set up environment to use virtual environment
471
+ env = os.environ.copy()
472
+ if os.name == "nt": # Windows
473
+ env["PATH"] = f"{os.path.join(venv_path, 'Scripts')};{env.get('PATH', '')}"
474
+ else: # Unix-based
475
+ env["PATH"] = f"{os.path.join(venv_path, 'bin')}:{env.get('PATH', '')}"
476
+
477
+ # Run the test script
478
+ return subprocess.run(
479
+ [test_script_path],
480
+ cwd=self.temp_dir,
481
+ capture_output=True,
482
+ text=True,
483
+ env=env,
484
+ timeout=300, # 5 minutes timeout
485
+ )
486
+
487
+ def _run_pytest_directly(self, venv_path: str) -> subprocess.CompletedProcess[str]:
488
+ """Run pytest directly using virtual environment."""
489
+ if os.name == "nt": # Windows
490
+ python_executable = os.path.join(venv_path, "Scripts", "python")
491
+ else: # Unix-based
492
+ python_executable = os.path.join(venv_path, "bin", "python")
493
+
494
+ test_dir = os.path.join(self.temp_dir, "tests")
495
+ return subprocess.run(
496
+ [python_executable, "-m", "pytest", test_dir, "-v"],
497
+ cwd=self.temp_dir,
498
+ capture_output=True,
499
+ text=True,
500
+ timeout=300, # 5 minutes timeout
501
+ )
502
+
503
+ def _run_test_script_with_env(
504
+ self, test_script_path: str, venv_path: str, env: Dict[str, str]
505
+ ) -> subprocess.CompletedProcess[str]:
506
+ """Run the template's test script with custom environment variables."""
507
+ # Fix line endings (convert Windows \r\n to Unix \n)
508
+ self._fix_script_line_endings(test_script_path)
509
+
510
+ # Make script executable
511
+ os.chmod(test_script_path, 0o755)
512
+
513
+ # Set up environment to use virtual environment
514
+ env = env.copy()
515
+ if os.name == "nt": # Windows
516
+ env["PATH"] = f"{os.path.join(venv_path, 'Scripts')};{env.get('PATH', '')}"
517
+ else: # Unix-based
518
+ env["PATH"] = f"{os.path.join(venv_path, 'bin')}:{env.get('PATH', '')}"
519
+
520
+ # Run the test script
521
+ return subprocess.run(
522
+ [test_script_path],
523
+ cwd=self.temp_dir,
524
+ capture_output=True,
525
+ text=True,
526
+ env=env,
527
+ timeout=300, # 5 minutes timeout
528
+ )
529
+
530
+ def _run_pytest_with_env(
531
+ self, venv_path: str, env: Dict[str, str], fallback_config: Dict[str, Any]
532
+ ) -> subprocess.CompletedProcess[str]:
533
+ """Run pytest directly with custom environment variables."""
534
+ if os.name == "nt": # Windows
535
+ python_executable = os.path.join(venv_path, "Scripts", "python")
536
+ else: # Unix-based
537
+ python_executable = os.path.join(venv_path, "bin", "python")
140
538
 
141
- # Install dependencies
539
+ test_command = fallback_config.get("test_command", "pytest tests/ -v").split()
540
+ return subprocess.run(
541
+ [python_executable, "-m"] + test_command,
542
+ cwd=self.temp_dir,
543
+ capture_output=True,
544
+ text=True,
545
+ env=env,
546
+ timeout=300,
547
+ )
548
+
549
+ def _test_with_fallback_strategy(self) -> bool:
550
+ """Run tests using fallback strategy (e.g., SQLite instead of PostgreSQL)."""
551
+ if not self.template_config or "fallback_testing" not in self.template_config:
552
+ debug_log(
553
+ "No fallback strategy configured, using standard strategy", "info"
554
+ )
555
+ return self._test_with_standard_strategy()
556
+
557
+ try:
558
+ # Create virtual environment for testing
559
+ venv_path = create_venv(self.temp_dir)
142
560
  install_dependencies(self.temp_dir, venv_path)
143
561
 
144
- # Run tests using the venv's pytest
145
- if os.name == "nt": # Windows
146
- pytest_path = os.path.join(venv_path, "Scripts", "pytest")
147
- else: # Linux/Mac
148
- pytest_path = os.path.join(venv_path, "bin", "pytest")
562
+ # Set up fallback environment (e.g., SQLite database)
563
+ fallback_config = self.template_config["fallback_testing"]
564
+ database_url = fallback_config.get("database_url", "sqlite:///:memory:")
565
+
566
+ # Set environment variable for fallback database
567
+ env = os.environ.copy()
568
+ env["DATABASE_URL"] = database_url
569
+ env["SQLALCHEMY_DATABASE_URI"] = database_url
570
+
571
+ # Check if scripts/test.sh exists
572
+ test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
573
+ if os.path.exists(test_script_path):
574
+ debug_log(
575
+ "Found scripts/test.sh, using template test script with fallback environment",
576
+ "info",
577
+ )
578
+ result = self._run_test_script_with_env(
579
+ test_script_path, venv_path, env
580
+ )
581
+ else:
582
+ debug_log(
583
+ "No scripts/test.sh found, running pytest directly with fallback environment",
584
+ "info",
585
+ )
586
+ result = self._run_pytest_with_env(venv_path, env, fallback_config)
587
+
588
+ if result.returncode != 0:
589
+ error_msg = (
590
+ f"Fallback tests failed with return code {result.returncode}\n"
591
+ )
592
+ if result.stderr:
593
+ error_msg += f"STDERR:\n{result.stderr}\n"
594
+ if result.stdout:
595
+ error_msg += f"STDOUT:\n{result.stdout}\n"
596
+ self.errors.append(error_msg)
597
+ debug_log(f"Fallback strategy tests failed: {error_msg}", "error")
598
+ return False
599
+
600
+ debug_log("Fallback tests passed successfully", "info")
601
+ warning_msg = (
602
+ "Tests passed using fallback strategy (SQLite instead of PostgreSQL)"
603
+ )
604
+ self.warnings.append(warning_msg)
605
+ debug_log(f"Template warning: {warning_msg}", "warning")
606
+ return True
607
+
608
+ except Exception as e:
609
+ self.errors.append(f"Unexpected error during fallback testing: {e}")
610
+ return False
611
+
612
+ def _setup_test_environment(self) -> None:
613
+ """Set up environment variables for testing."""
614
+ if not self.template_config:
615
+ return
616
+
617
+ env_defaults = self.template_config.get("test_env_defaults", {})
618
+ env_file_path = os.path.join(self.temp_dir, ".env")
619
+
620
+ # Check if .env file already exists
621
+ if os.path.exists(env_file_path):
622
+ debug_log(f".env file already exists at {env_file_path}", "info")
623
+ # Read existing content and merge with defaults
624
+ existing_vars = {}
625
+ try:
626
+ with open(env_file_path, "r", encoding="utf-8") as f:
627
+ for line in f:
628
+ line = line.strip()
629
+ if line and "=" in line and not line.startswith("#"):
630
+ key, value = line.split("=", 1)
631
+ existing_vars[key] = value
632
+ except Exception as e:
633
+ debug_log(f"Error reading existing .env file: {e}", "warning")
634
+ else:
635
+ existing_vars = {}
636
+
637
+ # Merge defaults with existing variables (defaults take precedence for missing vars)
638
+ final_vars = {**env_defaults, **existing_vars}
639
+
640
+ # Create .env file with merged variables
641
+ with open(env_file_path, "w", encoding="utf-8") as f:
642
+ for key, value in final_vars.items():
643
+ f.write(f"{key}={value}\n")
644
+
645
+ debug_log(
646
+ f"Set up environment file: {env_file_path} with variables: {list(final_vars.keys())}",
647
+ "info",
648
+ )
649
+
650
+ # Fix line endings in all shell scripts
651
+ self._fix_all_script_line_endings()
652
+
653
+ def _wait_for_services_healthy(self, compose_file: str, timeout: int) -> None:
654
+ """Wait for Docker Compose services to be healthy."""
655
+ import time
656
+
657
+ debug_log("Waiting for services to be healthy...", "info")
658
+ start_time = time.time()
659
+
660
+ while time.time() - start_time < timeout:
661
+ try:
662
+ # Check if all services are running and healthy
663
+ result = subprocess.run(
664
+ ["docker-compose", "-f", compose_file, "ps", "--format", "json"],
665
+ cwd=self.temp_dir,
666
+ capture_output=True,
667
+ text=True,
668
+ timeout=10,
669
+ )
670
+
671
+ if result.returncode == 0:
672
+ import json
673
+
674
+ services = []
675
+ for line in result.stdout.strip().split("\n"):
676
+ if line.strip():
677
+ try:
678
+ service_info = json.loads(line)
679
+ services.append(service_info)
680
+ except json.JSONDecodeError:
681
+ continue
149
682
 
683
+ # Check if all services are running
684
+ all_running = True
685
+ for service in services:
686
+ if service.get("State") != "running":
687
+ all_running = False
688
+ debug_log(
689
+ f"Service {service.get('Name')} is {service.get('State')}",
690
+ "info",
691
+ )
692
+ break
693
+
694
+ if all_running and len(services) >= 2: # db and app services
695
+ debug_log("All services are running", "info")
696
+ # Additional wait for app to be fully ready
697
+ time.sleep(10)
698
+ return
699
+
700
+ except Exception as e:
701
+ debug_log(f"Error checking service health: {e}", "warning")
702
+
703
+ debug_log("Services not ready yet, waiting...", "info")
704
+ time.sleep(5)
705
+
706
+ debug_log(
707
+ f"Services did not become healthy within {timeout} seconds", "warning"
708
+ )
709
+
710
+ def _verify_services_running(self, compose_file: str) -> bool:
711
+ """Verify that all required services are running."""
712
+ try:
150
713
  result = subprocess.run(
151
- [pytest_path, str(test_dir)],
714
+ ["docker-compose", "-f", compose_file, "ps", "--format", "json"],
715
+ cwd=self.temp_dir,
152
716
  capture_output=True,
153
717
  text=True,
154
- cwd=self.temp_dir,
718
+ timeout=30,
155
719
  )
156
720
 
157
721
  if result.returncode != 0:
158
- self.errors.append(f"Tests failed: {result.stderr}")
722
+ debug_log(f"Failed to check service status: {result.stderr}", "error")
159
723
  return False
160
724
 
725
+ import json
726
+
727
+ services = []
728
+ for line in result.stdout.strip().split("\n"):
729
+ if line.strip():
730
+ try:
731
+ service_info = json.loads(line)
732
+ services.append(service_info)
733
+ except json.JSONDecodeError:
734
+ continue
735
+
736
+ # Check for required services (db and app)
737
+ db_running = False
738
+ app_running = False
739
+
740
+ for service in services:
741
+ service_name = service.get("Name", "")
742
+ service_state = service.get("State", "")
743
+
744
+ debug_log(f"Service {service_name}: {service_state}", "info")
745
+
746
+ if "db" in service_name and service_state == "running":
747
+ db_running = True
748
+ elif "app" in service_name and service_state == "running":
749
+ app_running = True
750
+
751
+ if not db_running:
752
+ self.errors.append("Database service is not running")
753
+ return False
754
+
755
+ if not app_running:
756
+ # Get logs from failed app service for debugging
757
+ try:
758
+ logs_result = subprocess.run(
759
+ [
760
+ "docker-compose",
761
+ "-f",
762
+ compose_file,
763
+ "logs",
764
+ "--tail",
765
+ "50",
766
+ "app",
767
+ ],
768
+ cwd=self.temp_dir,
769
+ capture_output=True,
770
+ text=True,
771
+ timeout=30,
772
+ )
773
+ if logs_result.returncode == 0:
774
+ debug_log(f"App service logs: {logs_result.stdout}", "error")
775
+ self.errors.append(
776
+ f"Application service is not running. Logs: {logs_result.stdout[-500:]}"
777
+ )
778
+ else:
779
+ self.errors.append("Application service is not running")
780
+ except Exception:
781
+ self.errors.append("Application service is not running")
782
+ return False
783
+
784
+ debug_log("All required services are running", "info")
785
+ return True
786
+
161
787
  except Exception as e:
162
- self.errors.append(f"Error running tests: {e}")
788
+ debug_log(f"Error verifying services: {e}", "error")
789
+ self.errors.append(f"Failed to verify service status: {e}")
163
790
  return False
164
791
 
165
- return True
792
+ def _run_docker_tests(self, compose_file: str) -> bool:
793
+ """Run tests in Docker environment."""
794
+ try:
795
+ # Check if scripts/test.sh exists in temp directory
796
+ test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
797
+ if os.path.exists(test_script_path):
798
+ debug_log(
799
+ "Found scripts/test.sh, using template test script in Docker",
800
+ "info",
801
+ )
802
+ # Fix line endings before running in Docker
803
+ self._fix_script_line_endings(test_script_path)
804
+ # Run the test script inside Docker container
805
+ result = subprocess.run(
806
+ [
807
+ "docker-compose",
808
+ "-f",
809
+ compose_file,
810
+ "exec",
811
+ "-T",
812
+ "app",
813
+ "bash",
814
+ "-c",
815
+ "chmod +x ./scripts/test.sh && ./scripts/test.sh",
816
+ ],
817
+ cwd=self.temp_dir,
818
+ capture_output=True,
819
+ text=True,
820
+ timeout=300,
821
+ )
822
+ else:
823
+ debug_log(
824
+ "No scripts/test.sh found, running pytest directly in Docker",
825
+ "info",
826
+ )
827
+ # Run pytest directly using docker-compose exec
828
+ result = subprocess.run(
829
+ [
830
+ "docker-compose",
831
+ "-f",
832
+ compose_file,
833
+ "exec",
834
+ "-T",
835
+ "app",
836
+ "python",
837
+ "-m",
838
+ "pytest",
839
+ "tests/",
840
+ "-v",
841
+ ],
842
+ cwd=self.temp_dir,
843
+ capture_output=True,
844
+ text=True,
845
+ timeout=300,
846
+ )
166
847
 
848
+ if result.returncode != 0:
849
+ error_msg = (
850
+ f"Docker tests failed with return code {result.returncode}\n"
851
+ )
852
+ if result.stderr:
853
+ error_msg += f"STDERR:\n{result.stderr}\n"
854
+ if result.stdout:
855
+ error_msg += f"STDOUT:\n{result.stdout}\n"
856
+ self.errors.append(error_msg)
857
+ debug_log(f"Docker strategy tests failed: {error_msg}", "error")
858
+ return False
167
859
 
168
- def inspect_template(template_path: str) -> Dict[str, Any]:
169
- """Run the template inspection and return the result."""
170
- inspector = TemplateInspector(template_path)
171
- is_valid = inspector.inspect_template()
172
- result: dict[str, Any] = {
173
- "valid": is_valid,
174
- "errors": inspector.errors,
175
- "warnings": inspector.warnings,
176
- }
177
-
178
- if result["valid"]:
179
- print_success("Template inspection passed successfully! ✨")
180
- elif result["errors"]:
181
- error_messages = [str(error) for error in result["errors"]]
182
- print_error(
183
- "Template inspection failed with following errors:\n"
184
- + "\n".join(f"- {error}" for error in error_messages)
185
- )
860
+ debug_log("Docker tests passed successfully", "info")
861
+ return True
186
862
 
187
- if result["warnings"]:
188
- warning_messages = [str(warning) for warning in result["warnings"]]
189
- print_warning(
190
- "Template has following warnings:\n"
191
- + "\n".join(f"- {warning}" for warning in warning_messages)
192
- )
863
+ except subprocess.TimeoutExpired:
864
+ self.errors.append("Docker tests timed out")
865
+ return False
866
+ except Exception as e:
867
+ self.errors.append(f"Error running Docker tests: {e}")
868
+ return False
869
+
870
+ def _run_docker_exec_tests(self, compose_file: str) -> bool:
871
+ """Run tests using docker-compose exec command."""
872
+ try:
873
+ # Check if scripts/test.sh exists in temp directory
874
+ test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
875
+ if os.path.exists(test_script_path):
876
+ debug_log(
877
+ "Found scripts/test.sh, using template test script with docker-compose exec",
878
+ "info",
879
+ )
880
+ # Fix line endings before running in Docker
881
+ self._fix_script_line_endings(test_script_path)
882
+ # Run the test script inside Docker container using docker-compose exec
883
+ result = subprocess.run(
884
+ [
885
+ "docker-compose",
886
+ "-f",
887
+ compose_file,
888
+ "exec",
889
+ "-T",
890
+ "app",
891
+ "bash",
892
+ "scripts/test.sh",
893
+ ],
894
+ cwd=self.temp_dir,
895
+ capture_output=True,
896
+ text=True,
897
+ timeout=300,
898
+ )
899
+ else:
900
+ debug_log(
901
+ "No scripts/test.sh found, running pytest directly with docker-compose exec",
902
+ "info",
903
+ )
904
+ # Run pytest directly using docker-compose exec
905
+ result = subprocess.run(
906
+ [
907
+ "docker-compose",
908
+ "-f",
909
+ compose_file,
910
+ "exec",
911
+ "-T",
912
+ "app",
913
+ "python",
914
+ "-m",
915
+ "pytest",
916
+ "tests/",
917
+ "-v",
918
+ ],
919
+ cwd=self.temp_dir,
920
+ capture_output=True,
921
+ text=True,
922
+ timeout=300,
923
+ )
924
+
925
+ if result.returncode != 0:
926
+ error_msg = (
927
+ f"Docker exec tests failed with return code {result.returncode}\n"
928
+ )
929
+ if result.stderr:
930
+ error_msg += f"STDERR:\n{result.stderr}\n"
931
+ if result.stdout:
932
+ error_msg += f"STDOUT:\n{result.stdout}\n"
933
+ self.errors.append(error_msg)
934
+ debug_log(f"Docker exec strategy tests failed: {error_msg}", "error")
935
+ debug_log(f"Docker exec test stderr: {result.stderr}", "error")
936
+ debug_log(f"Docker exec test stdout: {result.stdout}", "info")
937
+ return False
938
+
939
+ debug_log("Docker exec tests passed successfully", "info")
940
+ debug_log(f"Docker exec test stdout: {result.stdout}", "info")
941
+ return True
942
+
943
+ except subprocess.TimeoutExpired:
944
+ self.errors.append("Docker exec tests timed out")
945
+ return False
946
+ except Exception as e:
947
+ self.errors.append(f"Error running Docker exec tests: {e}")
948
+ return False
949
+
950
+ def _cleanup_docker_services(self) -> None:
951
+ """Clean up Docker Compose services."""
952
+ try:
953
+ debug_log("Cleaning up Docker services", "info")
954
+ subprocess.run(
955
+ ["docker-compose", "down", "-v"],
956
+ cwd=self.temp_dir,
957
+ capture_output=True,
958
+ text=True,
959
+ timeout=60,
960
+ )
961
+ except Exception as e:
962
+ debug_log(f"Failed to cleanup Docker services: {e}", "warning")
963
+
964
+ def get_report(self) -> Dict[str, Any]:
965
+ """
966
+ Get inspection report with errors and warnings.
967
+
968
+ :return: Dictionary containing inspection results
969
+ """
970
+ is_valid = len(self.errors) == 0
971
+ template_name = self.template_path.name
972
+
973
+ # Log final inspection results
974
+ if is_valid:
975
+ debug_log(
976
+ f"Template inspection completed successfully for {template_name}",
977
+ "info",
978
+ )
979
+ if self.warnings:
980
+ debug_log(
981
+ f"Template {template_name} has {len(self.warnings)} warnings: {self.warnings}",
982
+ "warning",
983
+ )
984
+ else:
985
+ debug_log(
986
+ f"Template inspection failed for {template_name} with {len(self.errors)} errors",
987
+ "error",
988
+ )
989
+ for i, error in enumerate(self.errors, 1):
990
+ debug_log(f"Error {i}: {error}", "error")
991
+ if self.warnings:
992
+ debug_log(
993
+ f"Template {template_name} also has {len(self.warnings)} warnings: {self.warnings}",
994
+ "warning",
995
+ )
996
+
997
+ return {
998
+ "template_path": str(self.template_path),
999
+ "errors": self.errors,
1000
+ "warnings": self.warnings,
1001
+ "is_valid": is_valid,
1002
+ }
1003
+
1004
+
1005
+ def inspect_fastapi_template(template_path: str) -> Dict[str, Any]:
1006
+ """
1007
+ Convenience function to inspect a FastAPI template.
1008
+
1009
+ :param template_path: Path to the template to inspect
1010
+ :return: Inspection report dictionary
1011
+ """
1012
+ template_name = Path(template_path).name
1013
+ debug_log(
1014
+ f"Starting template inspection for {template_name} at {template_path}", "info"
1015
+ )
1016
+
1017
+ with TemplateInspector(template_path) as inspector:
1018
+ is_valid = inspector.inspect_template()
1019
+ report = inspector.get_report()
1020
+
1021
+ if is_valid:
1022
+ print_success(f"Template {template_path} is valid!")
1023
+ debug_log(
1024
+ f"Template inspection completed successfully for {template_name}",
1025
+ "info",
1026
+ )
1027
+ else:
1028
+ print_error(f"Template {template_path} validation failed")
1029
+ debug_log(f"Template inspection failed for {template_name}", "error")
1030
+ for error in inspector.errors:
1031
+ print_error(f" - {error}")
1032
+
1033
+ if inspector.warnings:
1034
+ debug_log(
1035
+ f"Template inspection for {template_name} has warnings", "warning"
1036
+ )
1037
+ for warning in inspector.warnings:
1038
+ print_warning(f" - {warning}")
193
1039
 
194
- return result
1040
+ debug_log(
1041
+ f"Template inspection completed for {template_name}. Valid: {is_valid}, Errors: {len(inspector.errors)}, Warnings: {len(inspector.warnings)}",
1042
+ "info",
1043
+ )
1044
+ return report
195
1045
 
196
1046
 
197
1047
  if __name__ == "__main__":
198
1048
  if len(sys.argv) != 2:
199
- print("Usage: python inspector.py <template_dir>")
1049
+ print_error("Usage: python inspector.py <template_dir>")
200
1050
  sys.exit(1)
201
1051
 
202
1052
  template_dir = sys.argv[1]
203
- inspect_template(template_dir)
1053
+ inspect_fastapi_template(template_dir)