fastapi-cli 0.0.28__tar.gz → 0.0.30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/PKG-INFO +1 -1
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/pyproject.toml +1 -1
- fastapi_cli-0.0.30/src/fastapi_cli/__init__.py +1 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/cli.py +33 -32
- fastapi_cli-0.0.30/src/fastapi_cli/utils/cli.py +190 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_cli.py +31 -11
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_cli.py +18 -3
- fastapi_cli-0.0.28/src/fastapi_cli/__init__.py +0 -1
- fastapi_cli-0.0.28/src/fastapi_cli/utils/cli.py +0 -85
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/LICENSE +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/README.md +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/add_latest_release_date.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/format.sh +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/lint.sh +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/prepare_release.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/test-cov-html.sh +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/test.sh +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/__main__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/config.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/discover.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/exceptions.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/logging.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/py.typed +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/utils/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/core/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/core/utils.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/other.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/pyproject_config/another_module.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/pyproject_config/my_module.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/pyproject_config/pyproject.toml +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/single_file_api.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/single_file_app.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/single_file_other.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/conftest.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_cli_pyproject.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_discover.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_prepare_release.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_requirements.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_default_dir.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_default_file.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_package.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_single_file.py +0 -0
- {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.30"
|
|
@@ -20,7 +20,11 @@ from fastapi_cli.exceptions import FastAPICLIException
|
|
|
20
20
|
|
|
21
21
|
from . import __version__
|
|
22
22
|
from .logging import setup_logging
|
|
23
|
-
from .utils.cli import
|
|
23
|
+
from .utils.cli import (
|
|
24
|
+
get_rich_toolkit,
|
|
25
|
+
get_uvicorn_log_config,
|
|
26
|
+
should_use_rich_logs,
|
|
27
|
+
)
|
|
24
28
|
|
|
25
29
|
app = typer.Typer(
|
|
26
30
|
rich_markup_mode="rich", context_settings={"help_option_names": ["-h", "--help"]}
|
|
@@ -142,11 +146,12 @@ def _run(
|
|
|
142
146
|
with get_rich_toolkit() as toolkit:
|
|
143
147
|
server_type = "development" if command == "dev" else "production"
|
|
144
148
|
|
|
145
|
-
toolkit.
|
|
146
|
-
toolkit.print_line()
|
|
149
|
+
toolkit.print(f"Starting FastAPI in {server_type} mode", emoji="⚡️")
|
|
147
150
|
|
|
151
|
+
toolkit.print_line()
|
|
148
152
|
toolkit.print(
|
|
149
|
-
"Searching for package file structure from directories with [blue]__init__.py[/blue] files"
|
|
153
|
+
"Searching for package file structure from directories with [blue]__init__.py[/blue] files",
|
|
154
|
+
emoji="🔎",
|
|
150
155
|
)
|
|
151
156
|
|
|
152
157
|
if entrypoint and (path or app):
|
|
@@ -154,7 +159,6 @@ def _run(
|
|
|
154
159
|
toolkit.print(
|
|
155
160
|
"[error]Cannot use --entrypoint together with path or --app arguments"
|
|
156
161
|
)
|
|
157
|
-
toolkit.print_line()
|
|
158
162
|
raise typer.Exit(code=1)
|
|
159
163
|
|
|
160
164
|
try:
|
|
@@ -168,8 +172,6 @@ def _run(
|
|
|
168
172
|
field = ".".join(str(loc) for loc in error["loc"])
|
|
169
173
|
toolkit.print(f" [red]•[/red] {field}: {error['msg']}")
|
|
170
174
|
|
|
171
|
-
toolkit.print_line()
|
|
172
|
-
|
|
173
175
|
raise typer.Exit(code=1) from None
|
|
174
176
|
|
|
175
177
|
try:
|
|
@@ -193,45 +195,43 @@ def _run(
|
|
|
193
195
|
module_data = import_data.module_data
|
|
194
196
|
import_string = import_data.import_string
|
|
195
197
|
|
|
196
|
-
toolkit.
|
|
198
|
+
toolkit.print_line()
|
|
199
|
+
toolkit.print(f"Importing from {module_data.extra_sys_path}", emoji="📂")
|
|
197
200
|
toolkit.print_line()
|
|
198
201
|
|
|
199
202
|
if module_data.module_paths:
|
|
200
203
|
root_tree = _get_module_tree(module_data.module_paths)
|
|
201
204
|
|
|
202
|
-
toolkit.print(root_tree
|
|
203
|
-
|
|
205
|
+
toolkit.print(root_tree)
|
|
206
|
+
|
|
207
|
+
toolkit.print_line()
|
|
204
208
|
|
|
205
209
|
toolkit.print(
|
|
206
210
|
"Importing the FastAPI app object from the module with the following code:",
|
|
207
|
-
tag="code",
|
|
208
211
|
)
|
|
209
212
|
toolkit.print_line()
|
|
210
213
|
toolkit.print(
|
|
211
|
-
f"[
|
|
214
|
+
f"[blue]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]",
|
|
212
215
|
)
|
|
213
|
-
toolkit.print_line()
|
|
214
216
|
|
|
215
|
-
toolkit.
|
|
216
|
-
|
|
217
|
-
tag="app",
|
|
218
|
-
)
|
|
217
|
+
toolkit.print_line()
|
|
218
|
+
toolkit.print(f"Using import string: [blue]{import_string}[/]", emoji="🐍")
|
|
219
219
|
|
|
220
|
+
toolkit.print_line()
|
|
220
221
|
mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
|
|
221
222
|
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
|
|
222
|
-
toolkit.
|
|
223
|
-
toolkit.print("Configuration sources:", tag="info")
|
|
223
|
+
toolkit.print("Configuration sources:", emoji="📋")
|
|
224
224
|
if mod_source_desc == app_source_desc:
|
|
225
|
-
toolkit.print(f"
|
|
225
|
+
toolkit.print(f"• Import string: {mod_source_desc}")
|
|
226
226
|
else:
|
|
227
|
-
toolkit.print(f"
|
|
228
|
-
toolkit.print(f"
|
|
227
|
+
toolkit.print(f"• Module: {mod_source_desc}")
|
|
228
|
+
toolkit.print(f"• App name: {app_source_desc}")
|
|
229
229
|
|
|
230
230
|
if import_data.module_config_source == "auto-discovery":
|
|
231
231
|
toolkit.print_line()
|
|
232
232
|
toolkit.print(
|
|
233
233
|
"You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
|
|
234
|
-
|
|
234
|
+
emoji="💡",
|
|
235
235
|
)
|
|
236
236
|
toolkit.print_line()
|
|
237
237
|
toolkit.print(
|
|
@@ -242,24 +242,21 @@ def _run(
|
|
|
242
242
|
),
|
|
243
243
|
"toml",
|
|
244
244
|
theme="ansi_light",
|
|
245
|
-
)
|
|
245
|
+
),
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
url = public_url.rstrip("/") if public_url else f"http://{host}:{port}"
|
|
249
249
|
url_docs = f"{url}/docs"
|
|
250
250
|
|
|
251
251
|
toolkit.print_line()
|
|
252
|
-
toolkit.print(
|
|
253
|
-
|
|
254
|
-
f"Documentation at [link={url_docs}]{url_docs}[/]",
|
|
255
|
-
tag="server",
|
|
256
|
-
)
|
|
252
|
+
toolkit.print(f"Server started at [link={url}]{url}[/]", emoji="🌐")
|
|
253
|
+
toolkit.print(f"Documentation at [link={url_docs}]{url_docs}[/]")
|
|
257
254
|
|
|
258
255
|
if command == "dev":
|
|
259
256
|
toolkit.print_line()
|
|
260
257
|
toolkit.print(
|
|
261
258
|
"Running in development mode, for production use: [bold]fastapi run[/]",
|
|
262
|
-
|
|
259
|
+
emoji="💡",
|
|
263
260
|
)
|
|
264
261
|
|
|
265
262
|
if not uvicorn:
|
|
@@ -268,9 +265,13 @@ def _run(
|
|
|
268
265
|
) from None
|
|
269
266
|
|
|
270
267
|
toolkit.print_line()
|
|
271
|
-
toolkit.print("Logs:")
|
|
268
|
+
toolkit.print("Logs:", bullet=False)
|
|
272
269
|
toolkit.print_line()
|
|
273
270
|
|
|
271
|
+
extra_uvicorn_kwargs: dict[str, Any] = (
|
|
272
|
+
{"log_config": get_uvicorn_log_config()} if should_use_rich_logs() else {}
|
|
273
|
+
)
|
|
274
|
+
|
|
274
275
|
uvicorn.run(
|
|
275
276
|
app=import_string,
|
|
276
277
|
host=host,
|
|
@@ -285,7 +286,7 @@ def _run(
|
|
|
285
286
|
root_path=root_path,
|
|
286
287
|
proxy_headers=proxy_headers,
|
|
287
288
|
forwarded_allow_ips=forwarded_allow_ips,
|
|
288
|
-
|
|
289
|
+
**extra_uvicorn_kwargs,
|
|
289
290
|
)
|
|
290
291
|
|
|
291
292
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from rich._loop import loop_first
|
|
6
|
+
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
7
|
+
from rich.segment import Segment
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from rich_toolkit import RichToolkit
|
|
10
|
+
from rich_toolkit.element import Element
|
|
11
|
+
from rich_toolkit.styles import BaseStyle
|
|
12
|
+
from uvicorn.logging import DefaultFormatter
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def should_use_rich_logs() -> bool:
|
|
18
|
+
"""Return True when stdout is a TTY and rich logs should be used, False otherwise."""
|
|
19
|
+
return sys.stdout.isatty()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IndentedBlock:
|
|
23
|
+
"""Indent a renderable, hanging a prefix (e.g. an emoji bullet) on the
|
|
24
|
+
first line and aligning wrapped/extra lines under the text."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
renderable: RenderableType,
|
|
29
|
+
*,
|
|
30
|
+
first_prefix: Text,
|
|
31
|
+
prefix: Text,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.renderable = renderable
|
|
34
|
+
self.first_prefix = first_prefix
|
|
35
|
+
self.prefix = prefix
|
|
36
|
+
|
|
37
|
+
# Text renders its `end` ("\n" by default), which would break lines
|
|
38
|
+
self.first_prefix.end = ""
|
|
39
|
+
self.prefix.end = ""
|
|
40
|
+
|
|
41
|
+
def __rich_console__(
|
|
42
|
+
self, console: Console, options: ConsoleOptions
|
|
43
|
+
) -> RenderResult:
|
|
44
|
+
prefix_width = max(self.first_prefix.cell_len, self.prefix.cell_len)
|
|
45
|
+
lines = console.render_lines(
|
|
46
|
+
self.renderable,
|
|
47
|
+
options.update_width(options.max_width - prefix_width),
|
|
48
|
+
pad=False,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
new_line = Segment.line()
|
|
52
|
+
|
|
53
|
+
for first, line in loop_first(lines):
|
|
54
|
+
if any(segment.text.strip() for segment in line):
|
|
55
|
+
yield from console.render(
|
|
56
|
+
self.first_prefix if first else self.prefix, options
|
|
57
|
+
)
|
|
58
|
+
yield from line
|
|
59
|
+
|
|
60
|
+
yield new_line
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FastAPIStyle(BaseStyle):
|
|
64
|
+
"""Uniform left indent with emoji bullets hanging to the left of the text.
|
|
65
|
+
|
|
66
|
+
``emoji=`` renders as a bullet in a fixed column; ``bullet=False`` drops
|
|
67
|
+
the bullet column and just indents. This is a small, purpose-built subset
|
|
68
|
+
of the richer style in fastapi-cloud-cli.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
content_padding = 1
|
|
72
|
+
emoji_column_width = 3
|
|
73
|
+
|
|
74
|
+
def render_element(
|
|
75
|
+
self,
|
|
76
|
+
element: Any,
|
|
77
|
+
is_active: bool = False,
|
|
78
|
+
done: bool = False,
|
|
79
|
+
parent: Element | None = None,
|
|
80
|
+
**kwargs: Any,
|
|
81
|
+
) -> RenderableType:
|
|
82
|
+
rendered = super().render_element(
|
|
83
|
+
element=element, is_active=is_active, done=done, parent=parent, **kwargs
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
emoji = kwargs.get("emoji", "")
|
|
87
|
+
|
|
88
|
+
if not emoji and not kwargs.get("bullet", True):
|
|
89
|
+
indent = Text(" " * (self.content_padding + 1))
|
|
90
|
+
return IndentedBlock(rendered, first_prefix=indent, prefix=indent)
|
|
91
|
+
|
|
92
|
+
return IndentedBlock(
|
|
93
|
+
rendered,
|
|
94
|
+
first_prefix=self._get_bullet_prefix(emoji),
|
|
95
|
+
prefix=Text(" " * (self.content_padding + self.emoji_column_width)),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _get_bullet_prefix(self, emoji: str) -> Text:
|
|
99
|
+
prefix = Text(" " * self.content_padding)
|
|
100
|
+
|
|
101
|
+
if emoji:
|
|
102
|
+
prefix.append_text(Text.from_markup(emoji))
|
|
103
|
+
|
|
104
|
+
prefix.pad_right(
|
|
105
|
+
self.content_padding + self.emoji_column_width - prefix.cell_len
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return prefix
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
LOG_LEVEL_COLORS = {
|
|
112
|
+
"debug": "blue",
|
|
113
|
+
"info": "cyan",
|
|
114
|
+
"warning": "yellow",
|
|
115
|
+
"warn": "yellow",
|
|
116
|
+
"error": "red",
|
|
117
|
+
"critical": "magenta",
|
|
118
|
+
"fatal": "magenta",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_log_bullet(level: str) -> str:
|
|
123
|
+
"""Colored bar rendered in the emoji bullet column, matching the log
|
|
124
|
+
level, like the ``fastapi cloud logs`` output."""
|
|
125
|
+
color = LOG_LEVEL_COLORS.get(level.lower(), "dim")
|
|
126
|
+
|
|
127
|
+
return f"[{color}]▕[/{color}]"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CustomFormatter(DefaultFormatter):
|
|
131
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
132
|
+
super().__init__(*args, **kwargs)
|
|
133
|
+
self.toolkit = get_rich_toolkit()
|
|
134
|
+
|
|
135
|
+
def formatMessage(self, record: logging.LogRecord) -> str:
|
|
136
|
+
message = record.getMessage()
|
|
137
|
+
result = self.toolkit.print_as_string(
|
|
138
|
+
message, emoji=_get_log_bullet(record.levelname)
|
|
139
|
+
)
|
|
140
|
+
# Prepend newline to fix alignment after ^C is printed by the terminal
|
|
141
|
+
if message == "Shutting down":
|
|
142
|
+
result = "\n" + result
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_uvicorn_log_config() -> dict[str, Any]:
|
|
147
|
+
return {
|
|
148
|
+
"version": 1,
|
|
149
|
+
"disable_existing_loggers": False,
|
|
150
|
+
"formatters": {
|
|
151
|
+
"default": {
|
|
152
|
+
"()": CustomFormatter,
|
|
153
|
+
"fmt": "%(levelprefix)s %(message)s",
|
|
154
|
+
"use_colors": None,
|
|
155
|
+
},
|
|
156
|
+
"access": {
|
|
157
|
+
"()": CustomFormatter,
|
|
158
|
+
"fmt": "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
"handlers": {
|
|
162
|
+
"default": {
|
|
163
|
+
"formatter": "default",
|
|
164
|
+
"class": "logging.StreamHandler",
|
|
165
|
+
"stream": "ext://sys.stderr",
|
|
166
|
+
},
|
|
167
|
+
"access": {
|
|
168
|
+
"formatter": "access",
|
|
169
|
+
"class": "logging.StreamHandler",
|
|
170
|
+
"stream": "ext://sys.stdout",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
"loggers": {
|
|
174
|
+
"uvicorn": {
|
|
175
|
+
"handlers": ["default"],
|
|
176
|
+
"level": "INFO",
|
|
177
|
+
"propagate": False,
|
|
178
|
+
},
|
|
179
|
+
"uvicorn.error": {"level": "INFO"},
|
|
180
|
+
"uvicorn.access": {
|
|
181
|
+
"handlers": ["access"],
|
|
182
|
+
"level": "INFO",
|
|
183
|
+
"propagate": False,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_rich_toolkit() -> RichToolkit:
|
|
190
|
+
return RichToolkit(style=FastAPIStyle())
|
|
@@ -16,6 +16,11 @@ runner = CliRunner()
|
|
|
16
16
|
assets_path = Path(__file__).parent / "assets"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
@pytest.fixture(autouse=True)
|
|
20
|
+
def force_rich_logs(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
21
|
+
monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: True)
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
def test_dev() -> None:
|
|
20
25
|
with changing_dir(assets_path):
|
|
21
26
|
with patch.object(uvicorn, "run") as mock_run:
|
|
@@ -40,7 +45,7 @@ def test_dev() -> None:
|
|
|
40
45
|
assert "Module: path CLI argument" in result.output
|
|
41
46
|
assert "App name: auto-discovery" in result.output
|
|
42
47
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
43
|
-
assert "Starting development
|
|
48
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
44
49
|
assert "Server started at http://127.0.0.1:8000" in result.output
|
|
45
50
|
assert "Documentation at http://127.0.0.1:8000/docs" in result.output
|
|
46
51
|
assert (
|
|
@@ -51,6 +56,21 @@ def test_dev() -> None:
|
|
|
51
56
|
assert "🐍 single_file_app.py" in result.output
|
|
52
57
|
|
|
53
58
|
|
|
59
|
+
def test_run_uses_uvicorn_default_log_config_without_rich_logs(
|
|
60
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
61
|
+
) -> None:
|
|
62
|
+
monkeypatch.setattr("fastapi_cli.cli.should_use_rich_logs", lambda: False)
|
|
63
|
+
|
|
64
|
+
with changing_dir(assets_path):
|
|
65
|
+
with patch.object(uvicorn, "run") as mock_run:
|
|
66
|
+
result = runner.invoke(app, ["run", "single_file_app.py"])
|
|
67
|
+
assert result.exit_code == 0, result.output
|
|
68
|
+
assert mock_run.called
|
|
69
|
+
assert mock_run.call_args
|
|
70
|
+
|
|
71
|
+
assert "log_config" not in mock_run.call_args.kwargs
|
|
72
|
+
|
|
73
|
+
|
|
54
74
|
def test_dev_no_args_auto_discovery() -> None:
|
|
55
75
|
"""Test that auto-discovery works when no args and no pyproject.toml entrypoint"""
|
|
56
76
|
with changing_dir(assets_path / "default_files" / "default_main"):
|
|
@@ -94,7 +114,7 @@ def test_dev_package() -> None:
|
|
|
94
114
|
assert "Module: path CLI argument" in result.output
|
|
95
115
|
assert "App name: auto-discovery" in result.output
|
|
96
116
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
97
|
-
assert "Starting development
|
|
117
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
98
118
|
assert "Server started at http://127.0.0.1:8000" in result.output
|
|
99
119
|
assert "Documentation at http://127.0.0.1:8000/docs" in result.output
|
|
100
120
|
assert (
|
|
@@ -147,7 +167,7 @@ def test_dev_args() -> None:
|
|
|
147
167
|
assert "Module: path CLI argument" in result.output
|
|
148
168
|
assert "App name: --app CLI option" in result.output
|
|
149
169
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
150
|
-
assert "Starting development
|
|
170
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
151
171
|
assert "Server started at http://192.168.0.2:8080" in result.output
|
|
152
172
|
assert "Documentation at http://192.168.0.2:8080/docs" in result.output
|
|
153
173
|
assert (
|
|
@@ -181,7 +201,7 @@ def test_dev_env_vars() -> None:
|
|
|
181
201
|
assert "Module: path CLI argument" in result.output
|
|
182
202
|
assert "App name: auto-discovery" in result.output
|
|
183
203
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
184
|
-
assert "Starting development
|
|
204
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
185
205
|
assert "Server started at http://127.0.0.1:8111" in result.output
|
|
186
206
|
assert "Documentation at http://127.0.0.1:8111/docs" in result.output
|
|
187
207
|
assert (
|
|
@@ -222,7 +242,7 @@ def test_dev_env_vars_and_args() -> None:
|
|
|
222
242
|
assert "Module: path CLI argument" in result.output
|
|
223
243
|
assert "App name: auto-discovery" in result.output
|
|
224
244
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
225
|
-
assert "Starting development
|
|
245
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
226
246
|
assert "Server started at http://127.0.0.1:8080" in result.output
|
|
227
247
|
assert "Documentation at http://127.0.0.1:8080/docs" in result.output
|
|
228
248
|
assert (
|
|
@@ -271,7 +291,7 @@ def test_run() -> None:
|
|
|
271
291
|
assert "Module: path CLI argument" in result.output
|
|
272
292
|
assert "App name: auto-discovery" in result.output
|
|
273
293
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
274
|
-
assert "Starting production
|
|
294
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
275
295
|
assert "Server started at http://0.0.0.0:8000" in result.output
|
|
276
296
|
assert "Documentation at http://0.0.0.0:8000/docs" in result.output
|
|
277
297
|
|
|
@@ -298,7 +318,7 @@ def test_run_trust_proxy() -> None:
|
|
|
298
318
|
"log_config": get_uvicorn_log_config(),
|
|
299
319
|
}
|
|
300
320
|
assert "Using import string: single_file_app:app" in result.output
|
|
301
|
-
assert "Starting production
|
|
321
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
302
322
|
assert "Server started at http://0.0.0.0:8000" in result.output
|
|
303
323
|
assert "Documentation at http://0.0.0.0:8000/docs" in result.output
|
|
304
324
|
assert (
|
|
@@ -349,7 +369,7 @@ def test_run_args() -> None:
|
|
|
349
369
|
assert "Module: path CLI argument" in result.output
|
|
350
370
|
assert "App name: --app CLI option" in result.output
|
|
351
371
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
352
|
-
assert "Starting production
|
|
372
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
353
373
|
assert "Server started at http://192.168.0.2:8080" in result.output
|
|
354
374
|
assert "Documentation at http://192.168.0.2:8080/docs" in result.output
|
|
355
375
|
assert (
|
|
@@ -383,7 +403,7 @@ def test_run_env_vars() -> None:
|
|
|
383
403
|
assert "Module: path CLI argument" in result.output
|
|
384
404
|
assert "App name: auto-discovery" in result.output
|
|
385
405
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
386
|
-
assert "Starting production
|
|
406
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
387
407
|
assert "Server started at http://0.0.0.0:8111" in result.output
|
|
388
408
|
assert "Documentation at http://0.0.0.0:8111/docs" in result.output
|
|
389
409
|
|
|
@@ -420,7 +440,7 @@ def test_run_env_vars_and_args() -> None:
|
|
|
420
440
|
assert "Module: path CLI argument" in result.output
|
|
421
441
|
assert "App name: auto-discovery" in result.output
|
|
422
442
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
423
|
-
assert "Starting production
|
|
443
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
424
444
|
assert "Server started at http://0.0.0.0:8080" in result.output
|
|
425
445
|
assert "Documentation at http://0.0.0.0:8080/docs" in result.output
|
|
426
446
|
|
|
@@ -465,7 +485,7 @@ def test_public_url_env_var(command: str, public_url: str) -> None:
|
|
|
465
485
|
|
|
466
486
|
assert "Using import string: single_file_app:app" in result.output
|
|
467
487
|
assert (
|
|
468
|
-
f"Starting {'development' if command == 'dev' else 'production'}
|
|
488
|
+
f"Starting FastAPI in {'development' if command == 'dev' else 'production'} mode"
|
|
469
489
|
in result.output
|
|
470
490
|
)
|
|
471
491
|
expected_url_base = public_url.rstrip("/")
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import logging
|
|
3
|
+
import sys
|
|
2
4
|
from logging.config import dictConfig
|
|
3
5
|
|
|
4
|
-
from pytest import LogCaptureFixture
|
|
6
|
+
from pytest import LogCaptureFixture, MonkeyPatch
|
|
5
7
|
|
|
6
|
-
from fastapi_cli.utils.cli import
|
|
8
|
+
from fastapi_cli.utils.cli import (
|
|
9
|
+
CustomFormatter,
|
|
10
|
+
get_uvicorn_log_config,
|
|
11
|
+
should_use_rich_logs,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
def test_get_uvicorn_config_uses_custom_formatter() -> None:
|
|
@@ -14,6 +20,14 @@ def test_get_uvicorn_config_uses_custom_formatter() -> None:
|
|
|
14
20
|
assert config["loggers"]["uvicorn"]["propagate"] is False
|
|
15
21
|
|
|
16
22
|
|
|
23
|
+
def test_should_use_rich_logs_is_false_without_tty(
|
|
24
|
+
monkeypatch: MonkeyPatch,
|
|
25
|
+
) -> None:
|
|
26
|
+
monkeypatch.setattr(sys, "stdout", io.StringIO())
|
|
27
|
+
|
|
28
|
+
assert should_use_rich_logs() is False
|
|
29
|
+
|
|
30
|
+
|
|
17
31
|
def test_custom_formatter() -> None:
|
|
18
32
|
formatter = CustomFormatter()
|
|
19
33
|
|
|
@@ -33,7 +47,8 @@ def test_custom_formatter() -> None:
|
|
|
33
47
|
|
|
34
48
|
formatted = formatter.formatMessage(record)
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
# the log level is shown as a colored bar bullet, like `fastapi cloud logs`
|
|
51
|
+
assert "▕" in formatted
|
|
37
52
|
assert "127.0.0.1" in formatted
|
|
38
53
|
assert "GET / HTTP/1.1" in formatted
|
|
39
54
|
assert "200" in formatted
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.28"
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from rich_toolkit import RichToolkit, RichToolkitTheme
|
|
5
|
-
from rich_toolkit.styles import TaggedStyle
|
|
6
|
-
from uvicorn.logging import DefaultFormatter
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class CustomFormatter(DefaultFormatter):
|
|
10
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
11
|
-
super().__init__(*args, **kwargs)
|
|
12
|
-
self.toolkit = get_rich_toolkit()
|
|
13
|
-
|
|
14
|
-
def formatMessage(self, record: logging.LogRecord) -> str:
|
|
15
|
-
message = record.getMessage()
|
|
16
|
-
result = self.toolkit.print_as_string(message, tag=record.levelname)
|
|
17
|
-
# Prepend newline to fix alignment after ^C is printed by the terminal
|
|
18
|
-
if message == "Shutting down":
|
|
19
|
-
result = "\n" + result
|
|
20
|
-
return result
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def get_uvicorn_log_config() -> dict[str, Any]:
|
|
24
|
-
return {
|
|
25
|
-
"version": 1,
|
|
26
|
-
"disable_existing_loggers": False,
|
|
27
|
-
"formatters": {
|
|
28
|
-
"default": {
|
|
29
|
-
"()": CustomFormatter,
|
|
30
|
-
"fmt": "%(levelprefix)s %(message)s",
|
|
31
|
-
"use_colors": None,
|
|
32
|
-
},
|
|
33
|
-
"access": {
|
|
34
|
-
"()": CustomFormatter,
|
|
35
|
-
"fmt": "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s",
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
"handlers": {
|
|
39
|
-
"default": {
|
|
40
|
-
"formatter": "default",
|
|
41
|
-
"class": "logging.StreamHandler",
|
|
42
|
-
"stream": "ext://sys.stderr",
|
|
43
|
-
},
|
|
44
|
-
"access": {
|
|
45
|
-
"formatter": "access",
|
|
46
|
-
"class": "logging.StreamHandler",
|
|
47
|
-
"stream": "ext://sys.stdout",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
"loggers": {
|
|
51
|
-
"uvicorn": {
|
|
52
|
-
"handlers": ["default"],
|
|
53
|
-
"level": "INFO",
|
|
54
|
-
"propagate": False,
|
|
55
|
-
},
|
|
56
|
-
"uvicorn.error": {"level": "INFO"},
|
|
57
|
-
"uvicorn.access": {
|
|
58
|
-
"handlers": ["access"],
|
|
59
|
-
"level": "INFO",
|
|
60
|
-
"propagate": False,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
logger = logging.getLogger(__name__)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def get_rich_toolkit() -> RichToolkit:
|
|
70
|
-
theme = RichToolkitTheme(
|
|
71
|
-
style=TaggedStyle(tag_width=11),
|
|
72
|
-
theme={
|
|
73
|
-
"tag.title": "white on #009485",
|
|
74
|
-
"tag": "white on #007166",
|
|
75
|
-
"placeholder": "grey85",
|
|
76
|
-
"text": "white",
|
|
77
|
-
"selected": "#007166",
|
|
78
|
-
"result": "grey85",
|
|
79
|
-
"progress": "on #007166",
|
|
80
|
-
"error": "red",
|
|
81
|
-
"log.info": "black on blue",
|
|
82
|
-
},
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
return RichToolkit(theme=theme)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_api/app/api.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/api.py
RENAMED
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/app.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/api.py
RENAMED
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/non_default/nonstandard.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|