Skip to content

Commit 8d34e8a

Browse files
committed
[ADD, REFACTOR] refactor cli modules, adjust template metadata injection logic
1 parent f56691e commit 8d34e8a

File tree

7 files changed

+196
-56
lines changed

7 files changed

+196
-56
lines changed

.github/workflows/distribute.yml

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
name: Distribute source to pypi package
22

33
on:
4-
release:
5-
types: [published]
4+
push:
5+
tags:
6+
- '*'
67

78

89
jobs:
@@ -19,5 +20,19 @@ jobs:
1920

2021
- uses: pdm-project/setup-pdm@v4
2122

23+
- name: Install dependencies
24+
run: pdm install
25+
26+
- name: Extract tag name
27+
id: tag
28+
run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
29+
30+
- name: Update version in __init__.py
31+
run: |
32+
echo "__version__ = '${{ steps.tag.outputs.TAG_NAME }}'" > src/fastapi_fastkit/__init__.py
33+
34+
- name: Build package
35+
run: pdm build
36+
2237
- name: Publish package distributions to PyPI
23-
run: pdm publish
38+
uses: pypa/gh-action-pypi-publish@release/v1

README.md

-8
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,12 @@ This project was created to speed up the configuration of the development enviro
1919

2020
This project was inspired by the `SpringBoot initializer` & Python Django's `django-admin` cli operation.
2121

22-
---
23-
2422
## Key Features
2523

2624
- **Immediate FastAPI project creation** : Super-fast FastAPI workspace & project creation via CLI, inspired by `django-admin`feature of [Python Django](https://github.com/django/django)
2725
- **Prettier CLI outputs** : beautiful CLI experiment ([rich library](https://github.com/Textualize/rich) feature)
2826
- **Standards-based FastAPI project template** : All FastAPI-fastkit templates are based on Python standards and FastAPI's common use
2927

30-
---
31-
3228
## Installation
3329

3430
Install `FastAPI-fastkit` at your Python environment.
@@ -141,8 +137,6 @@ FastAPI template project will deploy at '<your-project-path>'
141137
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
142138
```
143139

144-
---
145-
146140
## Significance of FastAPI-fastkit
147141

148142
FastAPI-fastkit aims to provide a fast and easy-to-use starter kit for new users of Python and FastAPI.
@@ -151,8 +145,6 @@ This idea was initiated with the aim of full fill to help FastAPI newcomers to l
151145

152146
As one person who has been using and loving FastAPI for a long time, I wanted to develop a project that could help me a little bit to practice [the wonderful motivation](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417) that FastAPI developer [tiangolo](https://github.com/tiangolo) has.
153147

154-
---
155-
156148
## License
157149

158150
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

src/fastapi_fastkit/backend/inspector.py

+93-27
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,53 @@
44
# This module will be read by Github Action when contributor
55
# makes a PR of adding new FastAPI template.
66
#
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+
#
721
# @author bnbong
822
# --------------------------------------------------------------------------
23+
import os
24+
import shutil
925
import subprocess
1026
import sys
1127
from pathlib import Path
1228
from typing import Any, Dict, List
1329

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+
1438

1539
class TemplateInspector:
1640
def __init__(self, template_path: str):
1741
self.template_path = Path(template_path)
1842
self.errors: List[str] = []
1943
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)
2054

2155
def inspect_template(self) -> bool:
2256
"""Inspect the template is valid FastAPI application."""
@@ -57,81 +91,113 @@ def _check_file_extensions(self) -> bool:
5791
return True
5892

5993
def _check_dependencies(self) -> bool:
60-
"""Check the dependencies."""
94+
"""Check the dependencies in both setup.py-tpl and requirements.txt-tpl."""
6195
req_path = self.template_path / "requirements.txt-tpl"
96+
setup_path = self.template_path / "setup.py-tpl"
97+
6298
if not req_path.exists():
6399
self.errors.append("requirements.txt-tpl not found")
64100
return False
101+
if not setup_path.exists():
102+
self.errors.append("setup.py-tpl not found")
103+
return False
65104

66105
with open(req_path) as f:
67106
deps = f.read().splitlines()
68107
package_names = [dep.split("==")[0] for dep in deps if dep]
69108
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+
)
71112
return False
72113
return True
73114

74115
def _check_fastapi_implementation(self) -> bool:
75116
"""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)
80118

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")
94121
return False
95122

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
96128
return True
97129

98130
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"
101133
if not test_dir.exists():
102134
self.errors.append("Tests directory not found")
103135
return False
104136

105137
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+
106150
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,
108155
)
156+
109157
if result.returncode != 0:
110158
self.errors.append(f"Tests failed: {result.stderr}")
111159
return False
160+
112161
except Exception as e:
113162
self.errors.append(f"Error running tests: {e}")
114163
return False
164+
115165
return True
116166

117167

118-
def inspect_template(template_path: str) -> Dict[str, Any | List[str]]:
168+
def inspect_template(template_path: str) -> Dict[str, Any]:
119169
"""Run the template inspection and return the result."""
120170
inspector = TemplateInspector(template_path)
121171
is_valid = inspector.inspect_template()
122-
123-
return {
172+
result: dict[str, Any] = {
124173
"valid": is_valid,
125174
"errors": inspector.errors,
126175
"warnings": inspector.warnings,
127176
}
128177

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+
129196

130197
if __name__ == "__main__":
131198
if len(sys.argv) != 2:
132199
print("Usage: python inspector.py <template_dir>")
133200
sys.exit(1)
134201

135202
template_dir = sys.argv[1]
136-
result = inspect_template(template_dir)
137-
print(result)
203+
inspect_template(template_dir)

src/fastapi_fastkit/backend/main.py

+53-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from fastapi_fastkit import console
1010
from fastapi_fastkit.core.exceptions import BackendExceptions, TemplateExceptions
1111
from fastapi_fastkit.core.settings import settings
12-
from fastapi_fastkit.utils.main import print_error, print_info, print_success
12+
from fastapi_fastkit.utils.main import print_error, print_info
1313

1414

1515
def find_template_core_modules(project_dir: str) -> dict[str, str]:
@@ -110,12 +110,13 @@ def create_venv(project_dir: str) -> str:
110110
activate_venv = f" {os.path.join(venv_path, 'Scripts', 'activate.bat')}"
111111
else:
112112
activate_venv = f" source {os.path.join(venv_path, 'bin', 'activate')}"
113-
print_info(
114-
"venv created at "
115-
+ venv_path
116-
+ "\nTo activate the virtual environment, run:\n\n"
117-
+ activate_venv,
118-
)
113+
114+
print_info(
115+
"venv created at "
116+
+ venv_path
117+
+ "\nTo activate the virtual environment, run:\n\n"
118+
+ activate_venv,
119+
)
119120
return venv_path
120121

121122
except Exception as e:
@@ -137,12 +138,54 @@ def install_dependencies(project_dir: str, venv_path: str) -> None:
137138
cwd=project_dir,
138139
check=True,
139140
)
141+
if os.name == "nt":
142+
activate_venv = f" {os.path.join(venv_path, 'Scripts', 'activate.bat')}"
143+
else:
144+
activate_venv = f" source {os.path.join(venv_path, 'bin', 'activate')}"
145+
146+
print_info(
147+
"Dependencies installed successfully."
148+
+ "\nTo activate the virtual environment, run:\n\n"
149+
+ activate_venv,
150+
)
140151

141152
except Exception as e:
142153
print_error(f"Error during dependency installation: {e}")
143154
raise BackendExceptions("Failed to install dependencies")
144155

145156

146-
# TODO : modify this function
147-
# def read_template_stack() -> Union[list, None]:
148-
# pass
157+
def read_template_stack(template_path: str) -> list[str]:
158+
"""
159+
Read the install_requires from setup.py-tpl in the template directory.
160+
Returns a list of required packages.
161+
162+
:param template_path: Path to the template directory
163+
:return: List of required packages
164+
"""
165+
setup_path = os.path.join(template_path, "setup.py-tpl")
166+
if not os.path.exists(setup_path):
167+
return []
168+
169+
try:
170+
with open(setup_path, "r") as f:
171+
content = f.read()
172+
# Find the install_requires section using proper string matching
173+
if "install_requires: list[str] = [" in content:
174+
start_idx = content.find("install_requires: list[str] = [") + len(
175+
"install_requires: list[str] = ["
176+
)
177+
end_idx = content.find("]", start_idx)
178+
if start_idx != -1 and end_idx != -1:
179+
deps_str = content[start_idx:end_idx]
180+
# Clean and parse the dependencies
181+
deps = [
182+
dep.strip().strip("'").strip('"')
183+
for dep in deps_str.split(",")
184+
if dep.strip() and not dep.isspace()
185+
]
186+
return [dep for dep in deps if dep] # Remove empty strings
187+
except Exception as e:
188+
print_error(f"Error reading template dependencies: {e}")
189+
return []
190+
191+
return []

0 commit comments

Comments
 (0)