Skip to content

Commit 41d0e52

Browse files
committed
✨ Add Typer CLI app
1 parent 6f5f5b9 commit 41d0e52

File tree

1 file changed

+244
-2
lines changed

1 file changed

+244
-2
lines changed

src/fastapi_cli/cli.py

Lines changed: 244 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,244 @@
1-
def main():
2-
print("Hello FastAPI CLI")
1+
from logging import getLogger
2+
from pathlib import Path
3+
from typing import Any, Union
4+
5+
import typer
6+
import uvicorn
7+
from rich import print
8+
from rich.padding import Padding
9+
from rich.panel import Panel
10+
from typing_extensions import Annotated
11+
12+
from fastapi_cli.discover import get_import_string
13+
from fastapi_cli.exceptions import FastAPICLIException
14+
15+
from . import __version__
16+
from .logging import setup_logging
17+
18+
app = typer.Typer(rich_markup_mode="rich")
19+
20+
setup_logging()
21+
logger = getLogger(__name__)
22+
23+
24+
def version_callback(value: bool) -> None:
25+
if value:
26+
print(f"FastAPI CLI version: [green]{__version__}[/green]")
27+
raise typer.Exit()
28+
29+
30+
@app.callback()
31+
def callback(
32+
version: Annotated[
33+
Union[bool, None],
34+
typer.Option(
35+
"--version", help="Show the version and exit.", callback=version_callback
36+
),
37+
] = None,
38+
) -> None:
39+
"""
40+
FastAPI CLI - The [bold]fastapi[/bold] command line app. 😎
41+
42+
Manage your [bold]FastAPI[/bold] projects, run your FastAPI apps, and more.
43+
44+
Read more in the docs: [link]https://fastapi.tiangolo.com/fastapi-cli/[/link].
45+
"""
46+
47+
48+
def _run(
49+
path: Union[Path, None] = None,
50+
*,
51+
host: str = "127.0.0.1",
52+
port: int = 8000,
53+
reload: bool = True,
54+
root_path: str = "",
55+
command: str,
56+
app: Union[str, None] = None,
57+
) -> None:
58+
try:
59+
use_uvicorn_app = get_import_string(path=path, app_name=app)
60+
except FastAPICLIException as e:
61+
logger.error(str(e))
62+
raise typer.Exit(code=1) from None
63+
serving_str = f"[dim]Serving at:[/dim] [link]http://{host}:{port}[/link]\n\n[dim]API docs:[/dim] [link]http://{host}:{port}/docs[/link]"
64+
65+
if command == "dev":
66+
panel = Panel(
67+
f"{serving_str}\n\n[dim]Running in development mode, for production use:[/dim] \n\n[b]fastapi run[/b]",
68+
title="FastAPI CLI - Development mode",
69+
expand=False,
70+
padding=(1, 2),
71+
style="black on yellow",
72+
)
73+
else:
74+
panel = Panel(
75+
f"{serving_str}\n\n[dim]Running in production mode, for development use:[/dim] \n\n[b]fastapi dev[/b]",
76+
title="FastAPI CLI - Production mode",
77+
expand=False,
78+
padding=(1, 2),
79+
style="green",
80+
)
81+
print(Padding(panel, 1))
82+
uvicorn.run(
83+
app=use_uvicorn_app,
84+
host=host,
85+
port=port,
86+
reload=reload,
87+
root_path=root_path,
88+
)
89+
90+
91+
@app.command()
92+
def dev(
93+
path: Annotated[
94+
Union[Path, None],
95+
typer.Argument(
96+
help="A path to a Python file or package directory (with [blue]__init__.py[/blue] files) containing a [bold]FastAPI[/bold] app. If not provided, a default set of paths will be tried."
97+
),
98+
] = None,
99+
*,
100+
host: Annotated[
101+
str,
102+
typer.Option(
103+
help="The host to serve on. For local development in localhost use [blue]127.0.0.1[/blue]. To enable public access, e.g. in a container, use all the IP addresses available with [blue]0.0.0.0[/blue]."
104+
),
105+
] = "127.0.0.1",
106+
port: Annotated[
107+
int,
108+
typer.Option(
109+
help="The port to serve on. You would normally have a termination proxy on top (another program) handling HTTPS on port [blue]443[/blue] and HTTP on port [blue]80[/blue], transferring the communication to your app."
110+
),
111+
] = 8000,
112+
reload: Annotated[
113+
bool,
114+
typer.Option(
115+
help="Enable auto-reload of the server when (code) files change. This is [bold]resource intensive[/bold], use it only during development."
116+
),
117+
] = True,
118+
root_path: Annotated[
119+
str,
120+
typer.Option(
121+
help="The root path is used to tell your app that it is being served to the outside world with some [bold]path prefix[/bold] set up in some termination proxy or similar."
122+
),
123+
] = "",
124+
app: Annotated[
125+
Union[str, None],
126+
typer.Option(
127+
help="The name of the variable that contains the [bold]FastAPI[/bold] app in the imported module or package. If not provided, it is detected automatically."
128+
),
129+
] = None,
130+
) -> Any:
131+
"""
132+
Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
133+
134+
This is equivalent to [bold]fastapi run[/bold] but with [bold]reload[/bold] enabled and listening on the [blue]127.0.0.1[/blue] address.
135+
136+
It automatically detects the Python module or package that needs to be imported based on the file or directory path passed.
137+
138+
If no path is passed, it tries with:
139+
140+
- [blue]main.py[/blue]
141+
- [blue]app.py[/blue]
142+
- [blue]api.py[/blue]
143+
- [blue]app/main.py[/blue]
144+
- [blue]app/app.py[/blue]
145+
- [blue]app/api.py[/blue]
146+
147+
It also detects the directory that needs to be added to the [bold]PYTHONPATH[/bold] to make the app importable and adds it.
148+
149+
It detects the [bold]FastAPI[/bold] app object to use. By default it looks in the module or package for an object named:
150+
151+
- [blue]app[/blue]
152+
- [blue]api[/blue]
153+
154+
Otherwise, it uses the first [bold]FastAPI[/bold] app found in the imported module or package.
155+
"""
156+
_run(
157+
path=path,
158+
host=host,
159+
port=port,
160+
reload=reload,
161+
root_path=root_path,
162+
app=app,
163+
command="dev",
164+
)
165+
166+
167+
@app.command()
168+
def run(
169+
path: Annotated[
170+
Union[Path, None],
171+
typer.Argument(
172+
help="A path to a Python file or package directory (with [blue]__init__.py[/blue] files) containing a [bold]FastAPI[/bold] app. If not provided, a default set of paths will be tried."
173+
),
174+
] = None,
175+
*,
176+
host: Annotated[
177+
str,
178+
typer.Option(
179+
help="The host to serve on. For local development in localhost use [blue]127.0.0.1[/blue]. To enable public access, e.g. in a container, use all the IP addresses available with [blue]0.0.0.0[/blue]."
180+
),
181+
] = "0.0.0.0",
182+
port: Annotated[
183+
int,
184+
typer.Option(
185+
help="The port to serve on. You would normally have a termination proxy on top (another program) handling HTTPS on port [blue]443[/blue] and HTTP on port [blue]80[/blue], transferring the communication to your app."
186+
),
187+
] = 8000,
188+
reload: Annotated[
189+
bool,
190+
typer.Option(
191+
help="Enable auto-reload of the server when (code) files change. This is [bold]resource intensive[/bold], use it only during development."
192+
),
193+
] = False,
194+
root_path: Annotated[
195+
str,
196+
typer.Option(
197+
help="The root path is used to tell your app that it is being served to the outside world with some [bold]path prefix[/bold] set up in some termination proxy or similar."
198+
),
199+
] = "",
200+
app: Annotated[
201+
Union[str, None],
202+
typer.Option(
203+
help="The name of the variable that contains the [bold]FastAPI[/bold] app in the imported module or package. If not provided, it is detected automatically."
204+
),
205+
] = None,
206+
) -> Any:
207+
"""
208+
Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
209+
210+
This is equivalent to [bold]fastapi dev[/bold] but with [bold]reload[/bold] disabled and listening on the [blue]0.0.0.0[/blue] address.
211+
212+
It automatically detects the Python module or package that needs to be imported based on the file or directory path passed.
213+
214+
If no path is passed, it tries with:
215+
216+
- [blue]main.py[/blue]
217+
- [blue]app.py[/blue]
218+
- [blue]api.py[/blue]
219+
- [blue]app/main.py[/blue]
220+
- [blue]app/app.py[/blue]
221+
- [blue]app/api.py[/blue]
222+
223+
It also detects the directory that needs to be added to the [bold]PYTHONPATH[/bold] to make the app importable and adds it.
224+
225+
It detects the [bold]FastAPI[/bold] app object to use. By default it looks in the module or package for an object named:
226+
227+
- [blue]app[/blue]
228+
- [blue]api[/blue]
229+
230+
Otherwise, it uses the first [bold]FastAPI[/bold] app found in the imported module or package.
231+
"""
232+
_run(
233+
path=path,
234+
host=host,
235+
port=port,
236+
reload=reload,
237+
root_path=root_path,
238+
app=app,
239+
command="run",
240+
)
241+
242+
243+
def main() -> None:
244+
app()

0 commit comments

Comments
 (0)