|
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