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.
- fastapi_fastkit/__init__.py +1 -10
- fastapi_fastkit/backend/inspector.py +933 -83
- fastapi_fastkit/backend/main.py +640 -482
- fastapi_fastkit/backend/package_managers/__init__.py +22 -0
- fastapi_fastkit/backend/package_managers/base.py +150 -0
- fastapi_fastkit/backend/package_managers/factory.py +151 -0
- fastapi_fastkit/backend/package_managers/pdm_manager.py +275 -0
- fastapi_fastkit/backend/package_managers/pip_manager.py +205 -0
- fastapi_fastkit/backend/package_managers/poetry_manager.py +378 -0
- fastapi_fastkit/backend/package_managers/uv_manager.py +327 -0
- fastapi_fastkit/backend/transducer.py +210 -106
- fastapi_fastkit/cli.py +702 -664
- fastapi_fastkit/core/exceptions.py +14 -3
- fastapi_fastkit/core/settings.py +181 -155
- fastapi_fastkit/fastapi_project_template/README.md +90 -90
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.gitignore-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +107 -107
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +36 -36
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/test.sh-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.cfg-tpl +14 -14
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +25 -25
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +9 -9
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +57 -57
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +21 -21
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +26 -26
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +42 -42
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/test_items.py-tpl +48 -48
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.gitignore-tpl +191 -191
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl +156 -156
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +36 -36
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/test.sh-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.cfg-tpl +12 -12
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +27 -27
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +9 -9
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +172 -172
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +21 -21
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl +103 -103
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +72 -72
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +39 -39
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +49 -49
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/utils/documents.py-tpl +20 -20
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +42 -42
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/test_items.py-tpl +61 -61
- fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl +101 -101
- fastapi_fastkit/fastapi_project_template/fastapi-default/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.gitignore-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +23 -23
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +119 -119
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +36 -36
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/test.sh-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.cfg-tpl +13 -13
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +54 -54
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +15 -15
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +26 -26
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/mocks/mock_items.json-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +19 -19
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/fastapi-empty/.env-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-empty/.gitignore-tpl +30 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +85 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.cfg-tpl +13 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +22 -22
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +75 -75
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +48 -36
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +16 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +37 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl +29 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +249 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +40 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/format.sh-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/lint.sh-tpl +18 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/run-server.sh-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/test.sh-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl +90 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +51 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/__init__.py-tpl +4 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +14 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +134 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +189 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +88 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +60 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +66 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/router.py-tpl +100 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +67 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/__init__.py-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +101 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +96 -0
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +81 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl +9 -9
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.gitignore-tpl +30 -30
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +23 -23
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl +144 -144
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/alembic.ini-tpl +119 -119
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/docker-compose.yml-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +41 -41
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/format.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/lint.sh-tpl +5 -5
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/pre-start.sh-tpl +16 -16
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/run-server.sh-tpl +8 -8
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/test.sh-tpl +10 -10
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.cfg-tpl +13 -13
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +25 -25
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/README-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +85 -85
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/script.py.mako-tpl +26 -26
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +37 -37
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +11 -11
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/deps.py-tpl +19 -19
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +109 -109
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +88 -88
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +53 -53
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +102 -102
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +45 -45
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +54 -54
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/backend_pre_start.py-tpl +39 -39
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/init_data.py-tpl +23 -23
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/tests_pre_start.py-tpl +39 -39
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/template-config.yml-tpl +24 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +63 -63
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +44 -44
- fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +64 -64
- fastapi_fastkit/fastapi_project_template/modules/crud/new_route.py-tpl +29 -29
- fastapi_fastkit/fastapi_project_template/modules/schemas/new_route.py-tpl +23 -23
- fastapi_fastkit/utils/logging.py +64 -28
- fastapi_fastkit/utils/main.py +180 -140
- fastapi_fastkit-1.1.0.dist-info/METADATA +182 -0
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/RECORD +165 -129
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/WHEEL +1 -1
- {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/licenses/LICENSE +21 -21
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/.DS_Store +0 -0
- fastapi_fastkit/logs/fastkit_debug_20250702_161423.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_162706.log +0 -20
- fastapi_fastkit/logs/fastkit_debug_20250702_163010.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_163526.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_164053.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_165220.log +0 -21
- fastapi_fastkit/logs/fastkit_debug_20250702_165647.log +0 -21
- fastapi_fastkit-1.0.2.dist-info/METADATA +0 -320
- {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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
self.
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
test_dir =
|
|
133
|
-
if not
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
[
|
|
714
|
+
["docker-compose", "-f", compose_file, "ps", "--format", "json"],
|
|
715
|
+
cwd=self.temp_dir,
|
|
152
716
|
capture_output=True,
|
|
153
717
|
text=True,
|
|
154
|
-
|
|
718
|
+
timeout=30,
|
|
155
719
|
)
|
|
156
720
|
|
|
157
721
|
if result.returncode != 0:
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1049
|
+
print_error("Usage: python inspector.py <template_dir>")
|
|
200
1050
|
sys.exit(1)
|
|
201
1051
|
|
|
202
1052
|
template_dir = sys.argv[1]
|
|
203
|
-
|
|
1053
|
+
inspect_fastapi_template(template_dir)
|