Skip to content

Commit 82de5c4

Browse files
committed
[ADD] add basic cli operations
1 parent 4acfa59 commit 82de5c4

File tree

4 files changed

+277
-26
lines changed

4 files changed

+277
-26
lines changed

fastapi-project-template/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All source codes in demo projects must consist of **`.py-tpl`**, _not_ .py.
99
## Base structure of FastAPI template project
1010

1111
(manual will be added later)
12+
(TODO : add description about -> include "FastAPI-fastkit" in setup.py)
1213

1314
## Adding new FastAPI-based template project
1415

fastapi-project-template/fastapi-default/setup.py-tpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ install_requires = [
1818
"watchfiles==0.22.0",
1919
"pytest==8.2.2",
2020
"pytest-asyncio==0.23.8",
21+
"FastAPI-fastkit",
2122
]
2223

2324
# IDE will watch this setup config through your project src, and help you to set up your environment

src/fastapi_fastkit/cli.py

Lines changed: 153 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import click
99
import subprocess
10+
import shutil
1011

1112
from typing import Union
1213

@@ -95,12 +96,41 @@ def echo(ctx: Context) -> None:
9596

9697
@fastkit_cli.command()
9798
def list_templates() -> None:
98-
# TODO : impl this
9999
"""
100-
Get available templates list.
100+
Display the list of available templates.
101+
101102
:return: None
102103
"""
103-
pass
104+
settings = FastkitConfig()
105+
template_dir = settings.FASTKIT_TEMPLATE_ROOT
106+
107+
if not os.path.exists(template_dir):
108+
click.echo("Template directory not found.")
109+
return
110+
111+
templates = [
112+
d
113+
for d in os.listdir(template_dir)
114+
if os.path.isdir(os.path.join(template_dir, d))
115+
]
116+
117+
if not templates:
118+
click.echo("No available templates.")
119+
return
120+
121+
click.echo("\nAvailable templates:")
122+
for template in templates:
123+
template_path = os.path.join(template_dir, template)
124+
readme_path = os.path.join(template_path, "README.md-tpl")
125+
126+
description = "No description"
127+
if os.path.exists(readme_path):
128+
with open(readme_path, "r") as f:
129+
first_line = f.readline().strip()
130+
if first_line.startswith("# "):
131+
description = first_line[2:]
132+
133+
click.echo(f"- {template}: {description}")
104134

105135

106136
@fastkit_cli.command(context_settings={"ignore_unknown_options": True})
@@ -190,39 +220,118 @@ def startup(
190220

191221

192222
@fastkit_cli.command(context_settings={"ignore_unknown_options": True})
193-
def startproject() -> None:
194-
# TODO : impl this. this method includes a stack selecting process. when user select a stack, it will be auto installed at venv environment, and make list of installed dependencies at requirements.txt file.
223+
@click.option(
224+
"--project-name",
225+
prompt="Enter project name",
226+
help="Name of the new FastAPI project",
227+
)
228+
@click.option(
229+
"--stack",
230+
type=click.Choice(["minimal", "standard", "full"]),
231+
prompt="Select stack",
232+
help="Project stack configuration",
233+
)
234+
def startproject(project_name: str, stack: str) -> None:
235+
"""
236+
Start a new FastAPI project.
237+
Dependencies will be automatically installed based on the selected stack.
238+
239+
:param project_name: Project name
240+
:param stack: Project stack configuration
241+
:return: None
242+
"""
243+
settings = FastkitConfig()
244+
project_dir = os.path.join(settings.USER_WORKSPACE, project_name)
245+
246+
if os.path.exists(project_dir):
247+
click.echo(f"Error: Project '{project_name}' already exists.")
248+
return
249+
250+
try:
251+
os.makedirs(project_dir)
252+
253+
dependencies = {
254+
"minimal": ["fastapi", "uvicorn"],
255+
"standard": ["fastapi", "uvicorn", "sqlalchemy", "alembic", "pytest"],
256+
"full": [
257+
"fastapi",
258+
"uvicorn",
259+
"sqlalchemy",
260+
"alembic",
261+
"pytest",
262+
"redis",
263+
"celery",
264+
"docker-compose",
265+
],
266+
}
267+
268+
with open(os.path.join(project_dir, "requirements.txt"), "w") as f:
269+
for dep in dependencies[stack]:
270+
f.write(f"{dep}\n")
271+
272+
click.echo("Creating virtual environment and installing dependencies...")
273+
subprocess.run(["python", "-m", "venv", os.path.join(project_dir, "venv")])
274+
subprocess.run(["pip", "install", "-r", "requirements.txt"], cwd=project_dir)
275+
276+
click.echo(f"Project '{project_name}' has been created successfully!")
277+
278+
except Exception as e:
279+
click.echo(f"Error during project creation: {e}")
280+
shutil.rmtree(project_dir, ignore_errors=True)
281+
282+
283+
def is_fastkit_project(project_dir: str) -> bool:
195284
"""
196-
Start a empty FastAPI project.
197-
:return:
285+
Check if the project was created with fastkit.
286+
Inspects the contents of the setup.py file.
287+
288+
:param project_dir: Project directory
289+
:return: True if the project was created with fastkit, False otherwise
198290
"""
199-
pass
291+
setup_py = os.path.join(project_dir, "setup.py")
292+
if not os.path.exists(setup_py):
293+
return False
294+
295+
try:
296+
with open(setup_py, "r") as f:
297+
content = f.read()
298+
return "FastAPI-fastkit" in content
299+
except:
300+
return False
200301

201302

202303
@fastkit_cli.command()
203304
@click.argument("project_name")
204305
@click.pass_context
205306
def deleteproject(ctx: Context, project_name: str) -> None:
206-
# TODO : add checking step - if target project is not from fastkit, discard the attempt.
207-
settings = ctx.obj["settings"]
307+
"""
308+
Delete a FastAPI project.
208309
310+
:param ctx: Click context object
311+
:param project_name: Project name
312+
:return: None
313+
"""
314+
settings = ctx.obj["settings"]
209315
user_local = settings.USER_WORKSPACE
210316
project_dir = os.path.join(user_local, project_name)
211317

212318
if not os.path.exists(project_dir):
213-
click.echo(f"Error: Project '{project_name}' does not exist at '{user_local}'.")
319+
click.echo(f"Error: Project '{project_name}' does not exist in '{user_local}'.")
320+
return
321+
322+
if not is_fastkit_project(project_dir):
323+
click.echo(f"Error: '{project_name}' is not a FastAPI-fastkit project.")
214324
return
215325

216326
confirm = click.confirm(
217-
f"\nAre you sure you want to delete the project '{project_name}' at '{project_dir}'?",
327+
f"\nDo you want to delete project '{project_name}' at '{project_dir}'?",
218328
default=False,
219329
)
220330
if not confirm:
221-
click.echo("Project deletion aborted!")
331+
click.echo("Project deletion cancelled!")
222332
return
223333

224334
try:
225-
# TODO : adjust this
226335
delete_project(project_dir)
227336
click.echo(
228337
f"Project '{project_name}' has been successfully deleted from '{user_local}'."
@@ -236,28 +345,37 @@ def deleteproject(ctx: Context, project_name: str) -> None:
236345
"--host",
237346
default="127.0.0.1",
238347
show_default=True,
239-
help="The host to bind the server to.",
348+
help="Host to bind the server",
240349
)
241350
@click.option(
242351
"--port",
243352
default=8000,
244353
show_default=True,
245-
help="The port to bind the server to.",
354+
help="Port to bind the server",
246355
)
247356
@click.option(
248357
"--reload/--no-reload",
249358
default=True,
250359
show_default=True,
251-
help="Enable or disable auto-reloading on code changes.",
360+
help="Enable/disable auto-reload on code changes",
361+
)
362+
@click.option(
363+
"--workers",
364+
default=1,
365+
show_default=True,
366+
help="Number of worker processes",
252367
)
253368
@click.pass_context
254369
def runserver(
255-
ctx: Context, host: str = "127.0.0.1", port: int = 8000, reload: bool = True
370+
ctx: Context,
371+
host: str = "127.0.0.1",
372+
port: int = 8000,
373+
reload: bool = True,
374+
workers: int = 1,
256375
) -> None:
257-
# TODO : add & apply click option
258-
# TODO : edit template 'fastapi-default'. fix modules
259376
"""
260377
Run the FastAPI server for the current project.
378+
[TODO] Alternative Point : using FastAPI-fastkit's 'fastapi dev' command
261379
262380
:param ctx: Click context object
263381
:param host: Host address to bind the server to
@@ -270,16 +388,25 @@ def runserver(
270388

271389
app_path = os.path.join(project_dir, "main.py")
272390
if not os.path.exists(app_path):
273-
click.echo(
274-
f"Error: No 'main.py' found in the project directory '{project_dir}'."
275-
)
391+
click.echo(f"Error: Could not find 'main.py' in '{project_dir}'.")
276392
return
277393

278-
# TODO : edit this - add click's params
279-
command = ["fastapi", "dev", "main.py"]
394+
command = [
395+
"uvicorn",
396+
"main:app",
397+
"--host",
398+
host,
399+
"--port",
400+
str(port),
401+
"--workers",
402+
str(workers),
403+
]
404+
405+
if reload:
406+
command.append("--reload")
280407

281408
try:
282409
click.echo(f"Starting FastAPI server at {host}:{port}...")
283410
subprocess.run(command, check=True)
284411
except subprocess.CalledProcessError as e:
285-
click.echo(f"Error: Failed to start the FastAPI server.\n{e}")
412+
click.echo(f"Error: Failed to start FastAPI server.\n{e}")

0 commit comments

Comments
 (0)