|
4 | 4 | # This module will be read by Github Action when contributor
|
5 | 5 | # makes a PR of adding new FastAPI template.
|
6 | 6 | #
|
| 7 | +# First, check a FastAPI template is formed a valid template form with .py-tpl extension |
| 8 | +# & dependencies requirements. |
| 9 | +# Second, check a FastAPI template has a proper FastAPI server implementation. |
| 10 | +# main.py module must have a FastAPI app creation. like `app = FastAPI()` |
| 11 | +# Third, check a FastAPI template has passed all the tests. |
| 12 | +# |
| 13 | +# This module create temporary named 'temp' directory at src/fastapi_fastkit/backend |
| 14 | +# and copy a template to Funtional FastAPI application into the temp directory. |
| 15 | +# After the inspection, it will be deleted. |
| 16 | +# |
| 17 | +# This module include virtual environment creation & installation of dependencies. |
| 18 | +# Depending on the volume in which the template is implemented and the number of dependencies, |
| 19 | +# it may take some time to complete the inspection. |
| 20 | +# |
7 | 21 | # @author bnbong
|
8 | 22 | # --------------------------------------------------------------------------
|
| 23 | +import os |
| 24 | +import shutil |
9 | 25 | import subprocess
|
10 | 26 | import sys
|
11 | 27 | from pathlib import Path
|
12 | 28 | from typing import Any, Dict, List
|
13 | 29 |
|
| 30 | +from fastapi_fastkit.backend.main import ( |
| 31 | + create_venv, |
| 32 | + find_template_core_modules, |
| 33 | + install_dependencies, |
| 34 | +) |
| 35 | +from fastapi_fastkit.backend.transducer import copy_and_convert_template |
| 36 | +from fastapi_fastkit.utils.main import print_error, print_success, print_warning |
| 37 | + |
14 | 38 |
|
15 | 39 | class TemplateInspector:
|
16 | 40 | def __init__(self, template_path: str):
|
17 | 41 | self.template_path = Path(template_path)
|
18 | 42 | self.errors: List[str] = []
|
19 | 43 | self.warnings: List[str] = []
|
| 44 | + self.temp_dir = os.path.join(os.path.dirname(__file__), "temp") |
| 45 | + |
| 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) |
| 49 | + |
| 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) |
20 | 54 |
|
21 | 55 | def inspect_template(self) -> bool:
|
22 | 56 | """Inspect the template is valid FastAPI application."""
|
@@ -57,81 +91,113 @@ def _check_file_extensions(self) -> bool:
|
57 | 91 | return True
|
58 | 92 |
|
59 | 93 | def _check_dependencies(self) -> bool:
|
60 |
| - """Check the dependencies.""" |
| 94 | + """Check the dependencies in both setup.py-tpl and requirements.txt-tpl.""" |
61 | 95 | req_path = self.template_path / "requirements.txt-tpl"
|
| 96 | + setup_path = self.template_path / "setup.py-tpl" |
| 97 | + |
62 | 98 | if not req_path.exists():
|
63 | 99 | self.errors.append("requirements.txt-tpl not found")
|
64 | 100 | return False
|
| 101 | + if not setup_path.exists(): |
| 102 | + self.errors.append("setup.py-tpl not found") |
| 103 | + return False |
65 | 104 |
|
66 | 105 | with open(req_path) as f:
|
67 | 106 | deps = f.read().splitlines()
|
68 | 107 | package_names = [dep.split("==")[0] for dep in deps if dep]
|
69 | 108 | if "fastapi" not in package_names:
|
70 |
| - self.errors.append("FastAPI dependency not found") |
| 109 | + self.errors.append( |
| 110 | + "FastAPI dependency not found in requirements.txt-tpl" |
| 111 | + ) |
71 | 112 | return False
|
72 | 113 | return True
|
73 | 114 |
|
74 | 115 | def _check_fastapi_implementation(self) -> bool:
|
75 | 116 | """Check if the template has a proper FastAPI server implementation."""
|
76 |
| - main_paths = [ |
77 |
| - self.template_path / "src/main.py-tpl", |
78 |
| - self.template_path / "main.py-tpl", |
79 |
| - ] |
| 117 | + core_modules = find_template_core_modules(self.temp_dir) |
80 | 118 |
|
81 |
| - main_file_found = False |
82 |
| - for main_path in main_paths: |
83 |
| - if main_path.exists(): |
84 |
| - main_file_found = True |
85 |
| - with open(main_path) as f: |
86 |
| - content = f.read() |
87 |
| - if "uvicorn.run" not in content: |
88 |
| - self.errors.append(f"Web server call not found in {main_path}") |
89 |
| - return False |
90 |
| - break |
91 |
| - |
92 |
| - if not main_file_found: |
93 |
| - self.errors.append("main.py-tpl not found in either src/ or root directory") |
| 119 | + if not core_modules["main"]: |
| 120 | + self.errors.append("main.py not found in template") |
94 | 121 | return False
|
95 | 122 |
|
| 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 |
96 | 128 | return True
|
97 | 129 |
|
98 | 130 | def _test_template(self) -> bool:
|
99 |
| - """Run the tests.""" |
100 |
| - test_dir = self.template_path / "tests" |
| 131 | + """Run the tests in the converted template directory.""" |
| 132 | + test_dir = Path(self.temp_dir) / "tests" |
101 | 133 | if not test_dir.exists():
|
102 | 134 | self.errors.append("Tests directory not found")
|
103 | 135 | return False
|
104 | 136 |
|
105 | 137 | try:
|
| 138 | + # Create virtual environment |
| 139 | + venv_path = create_venv(self.temp_dir) |
| 140 | + |
| 141 | + # Install dependencies |
| 142 | + install_dependencies(self.temp_dir, venv_path) |
| 143 | + |
| 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") |
| 149 | + |
106 | 150 | result = subprocess.run(
|
107 |
| - ["pytest", str(test_dir)], capture_output=True, text=True |
| 151 | + [pytest_path, str(test_dir)], |
| 152 | + capture_output=True, |
| 153 | + text=True, |
| 154 | + cwd=self.temp_dir, |
108 | 155 | )
|
| 156 | + |
109 | 157 | if result.returncode != 0:
|
110 | 158 | self.errors.append(f"Tests failed: {result.stderr}")
|
111 | 159 | return False
|
| 160 | + |
112 | 161 | except Exception as e:
|
113 | 162 | self.errors.append(f"Error running tests: {e}")
|
114 | 163 | return False
|
| 164 | + |
115 | 165 | return True
|
116 | 166 |
|
117 | 167 |
|
118 |
| -def inspect_template(template_path: str) -> Dict[str, Any | List[str]]: |
| 168 | +def inspect_template(template_path: str) -> Dict[str, Any]: |
119 | 169 | """Run the template inspection and return the result."""
|
120 | 170 | inspector = TemplateInspector(template_path)
|
121 | 171 | is_valid = inspector.inspect_template()
|
122 |
| - |
123 |
| - return { |
| 172 | + result: dict[str, Any] = { |
124 | 173 | "valid": is_valid,
|
125 | 174 | "errors": inspector.errors,
|
126 | 175 | "warnings": inspector.warnings,
|
127 | 176 | }
|
128 | 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 | + ) |
| 186 | + |
| 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 | + ) |
| 193 | + |
| 194 | + return result |
| 195 | + |
129 | 196 |
|
130 | 197 | if __name__ == "__main__":
|
131 | 198 | if len(sys.argv) != 2:
|
132 | 199 | print("Usage: python inspector.py <template_dir>")
|
133 | 200 | sys.exit(1)
|
134 | 201 |
|
135 | 202 | template_dir = sys.argv[1]
|
136 |
| - result = inspect_template(template_dir) |
137 |
| - print(result) |
| 203 | + inspect_template(template_dir) |
0 commit comments