fastapi-cli 0.0.29__tar.gz → 0.0.31__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.29 → fastapi_cli-0.0.31}/PKG-INFO +1 -1
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/pyproject.toml +1 -1
- fastapi_cli-0.0.31/src/fastapi_cli/__init__.py +1 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/cli.py +69 -49
- fastapi_cli-0.0.31/src/fastapi_cli/utils/cli.py +190 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_cli.py +91 -78
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_cli_pyproject.py +7 -7
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_cli.py +2 -1
- fastapi_cli-0.0.29/src/fastapi_cli/__init__.py +0 -1
- fastapi_cli-0.0.29/src/fastapi_cli/utils/cli.py +0 -91
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/LICENSE +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/README.md +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/add_latest_release_date.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/format.sh +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/lint.sh +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/prepare_release.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/test-cov-html.sh +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/test.sh +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/__main__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/config.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/discover.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/exceptions.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/logging.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/py.typed +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/utils/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/core/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/core/utils.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/other.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/pyproject_config/another_module.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/pyproject_config/my_module.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/pyproject_config/pyproject.toml +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/single_file_api.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/single_file_app.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/single_file_other.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/conftest.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_discover.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_prepare_release.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_requirements.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_default_dir.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_default_file.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_package.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_single_file.py +0 -0
- {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.31"
|
|
@@ -142,23 +142,25 @@ def _run(
|
|
|
142
142
|
proxy_headers: bool = False,
|
|
143
143
|
forwarded_allow_ips: str | None = None,
|
|
144
144
|
public_url: str | None = None,
|
|
145
|
+
verbose: bool = False,
|
|
145
146
|
) -> None:
|
|
146
147
|
with get_rich_toolkit() as toolkit:
|
|
147
148
|
server_type = "development" if command == "dev" else "production"
|
|
148
149
|
|
|
149
|
-
toolkit.
|
|
150
|
-
toolkit.print_line()
|
|
150
|
+
toolkit.print(f"Starting FastAPI in {server_type} mode", emoji="⚡️")
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
if verbose:
|
|
153
|
+
toolkit.print_line()
|
|
154
|
+
toolkit.print(
|
|
155
|
+
"Searching for package file structure from directories with [blue]__init__.py[/blue] files",
|
|
156
|
+
emoji="🔎",
|
|
157
|
+
)
|
|
155
158
|
|
|
156
159
|
if entrypoint and (path or app):
|
|
157
160
|
toolkit.print_line()
|
|
158
161
|
toolkit.print(
|
|
159
162
|
"[error]Cannot use --entrypoint together with path or --app arguments"
|
|
160
163
|
)
|
|
161
|
-
toolkit.print_line()
|
|
162
164
|
raise typer.Exit(code=1)
|
|
163
165
|
|
|
164
166
|
try:
|
|
@@ -172,8 +174,6 @@ def _run(
|
|
|
172
174
|
field = ".".join(str(loc) for loc in error["loc"])
|
|
173
175
|
toolkit.print(f" [red]•[/red] {field}: {error['msg']}")
|
|
174
176
|
|
|
175
|
-
toolkit.print_line()
|
|
176
|
-
|
|
177
177
|
raise typer.Exit(code=1) from None
|
|
178
178
|
|
|
179
179
|
try:
|
|
@@ -196,46 +196,58 @@ def _run(
|
|
|
196
196
|
|
|
197
197
|
module_data = import_data.module_data
|
|
198
198
|
import_string = import_data.import_string
|
|
199
|
+
is_auto_discovery = import_data.module_config_source == "auto-discovery"
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
if verbose:
|
|
202
|
+
toolkit.print_line()
|
|
203
|
+
toolkit.print(f"Importing from {module_data.extra_sys_path}", emoji="📂")
|
|
204
|
+
toolkit.print_line()
|
|
202
205
|
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
if module_data.module_paths:
|
|
207
|
+
root_tree = _get_module_tree(module_data.module_paths)
|
|
208
|
+
|
|
209
|
+
toolkit.print(root_tree)
|
|
205
210
|
|
|
206
|
-
toolkit.print(root_tree, tag="module")
|
|
207
211
|
toolkit.print_line()
|
|
208
212
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
toolkit.print(
|
|
214
|
+
"Importing the FastAPI app object from the module with the following code:",
|
|
215
|
+
)
|
|
216
|
+
toolkit.print_line()
|
|
217
|
+
toolkit.print(
|
|
218
|
+
f"[blue]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Outside --verbose, fold the resolution source into the import string
|
|
222
|
+
# line and point newcomers at --verbose for the full explanation
|
|
223
|
+
if is_auto_discovery and not verbose:
|
|
224
|
+
app_note = " [dim](auto-discovered, use --verbose to learn more)[/]"
|
|
225
|
+
else:
|
|
226
|
+
app_note = ""
|
|
218
227
|
|
|
228
|
+
toolkit.print_line()
|
|
219
229
|
toolkit.print(
|
|
220
|
-
f"Using import string: [blue]{import_string}[/]",
|
|
221
|
-
tag="app",
|
|
230
|
+
f"Using import string: [blue]{import_string}[/]{app_note}", emoji="🐍"
|
|
222
231
|
)
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
if verbose:
|
|
234
|
+
toolkit.print_line()
|
|
235
|
+
mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
|
|
236
|
+
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
|
|
237
|
+
toolkit.print("Configuration sources:", emoji="📋")
|
|
238
|
+
if mod_source_desc == app_source_desc:
|
|
239
|
+
toolkit.print(f"• Import string: {mod_source_desc}")
|
|
240
|
+
else:
|
|
241
|
+
toolkit.print(f"• Module: {mod_source_desc}")
|
|
242
|
+
toolkit.print(f"• App name: {app_source_desc}")
|
|
233
243
|
|
|
234
|
-
|
|
244
|
+
# Nudge to pin the entrypoint whenever it was auto-discovered, so it's
|
|
245
|
+
# explicit next time — shown in the default output, not just --verbose
|
|
246
|
+
if is_auto_discovery:
|
|
235
247
|
toolkit.print_line()
|
|
236
248
|
toolkit.print(
|
|
237
249
|
"You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
|
|
238
|
-
|
|
250
|
+
emoji="💡",
|
|
239
251
|
)
|
|
240
252
|
toolkit.print_line()
|
|
241
253
|
toolkit.print(
|
|
@@ -246,25 +258,15 @@ def _run(
|
|
|
246
258
|
),
|
|
247
259
|
"toml",
|
|
248
260
|
theme="ansi_light",
|
|
249
|
-
)
|
|
261
|
+
),
|
|
250
262
|
)
|
|
251
263
|
|
|
252
264
|
url = public_url.rstrip("/") if public_url else f"http://{host}:{port}"
|
|
253
265
|
url_docs = f"{url}/docs"
|
|
254
266
|
|
|
255
267
|
toolkit.print_line()
|
|
256
|
-
toolkit.print(
|
|
257
|
-
|
|
258
|
-
f"Documentation at [link={url_docs}]{url_docs}[/]",
|
|
259
|
-
tag="server",
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
if command == "dev":
|
|
263
|
-
toolkit.print_line()
|
|
264
|
-
toolkit.print(
|
|
265
|
-
"Running in development mode, for production use: [bold]fastapi run[/]",
|
|
266
|
-
tag="tip",
|
|
267
|
-
)
|
|
268
|
+
toolkit.print(f"Server started at [link={url}]{url}[/]", emoji="🌐")
|
|
269
|
+
toolkit.print(f"Documentation at [link={url_docs}]{url_docs}[/]")
|
|
268
270
|
|
|
269
271
|
if not uvicorn:
|
|
270
272
|
raise FastAPICLIException(
|
|
@@ -272,7 +274,7 @@ def _run(
|
|
|
272
274
|
) from None
|
|
273
275
|
|
|
274
276
|
toolkit.print_line()
|
|
275
|
-
toolkit.print("Logs:")
|
|
277
|
+
toolkit.print("Logs:", bullet=False)
|
|
276
278
|
toolkit.print_line()
|
|
277
279
|
|
|
278
280
|
extra_uvicorn_kwargs: dict[str, Any] = (
|
|
@@ -363,6 +365,14 @@ def dev(
|
|
|
363
365
|
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
|
|
364
366
|
),
|
|
365
367
|
] = None,
|
|
368
|
+
verbose: Annotated[
|
|
369
|
+
bool,
|
|
370
|
+
typer.Option(
|
|
371
|
+
"--verbose",
|
|
372
|
+
"-v",
|
|
373
|
+
help="Show detailed startup output and debug logs: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
|
|
374
|
+
),
|
|
375
|
+
] = False,
|
|
366
376
|
) -> Any:
|
|
367
377
|
"""
|
|
368
378
|
Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
|
|
@@ -402,6 +412,7 @@ def dev(
|
|
|
402
412
|
proxy_headers=proxy_headers,
|
|
403
413
|
forwarded_allow_ips=forwarded_allow_ips,
|
|
404
414
|
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
|
|
415
|
+
verbose=verbose,
|
|
405
416
|
)
|
|
406
417
|
|
|
407
418
|
|
|
@@ -471,6 +482,14 @@ def run(
|
|
|
471
482
|
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
|
|
472
483
|
),
|
|
473
484
|
] = None,
|
|
485
|
+
verbose: Annotated[
|
|
486
|
+
bool,
|
|
487
|
+
typer.Option(
|
|
488
|
+
"--verbose",
|
|
489
|
+
"-v",
|
|
490
|
+
help="Show detailed startup output and debug logs: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
|
|
491
|
+
),
|
|
492
|
+
] = False,
|
|
474
493
|
) -> Any:
|
|
475
494
|
"""
|
|
476
495
|
Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
|
|
@@ -510,6 +529,7 @@ def run(
|
|
|
510
529
|
proxy_headers=proxy_headers,
|
|
511
530
|
forwarded_allow_ips=forwarded_allow_ips,
|
|
512
531
|
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
|
|
532
|
+
verbose=verbose,
|
|
513
533
|
)
|
|
514
534
|
|
|
515
535
|
|
|
@@ -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())
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import subprocess
|
|
2
3
|
import sys
|
|
3
4
|
from pathlib import Path
|
|
@@ -40,20 +41,16 @@ def test_dev() -> None:
|
|
|
40
41
|
"forwarded_allow_ips": None,
|
|
41
42
|
"log_config": get_uvicorn_log_config(),
|
|
42
43
|
}
|
|
44
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
43
45
|
assert "Using import string: single_file_app:app" in result.output
|
|
44
|
-
assert "Configuration sources:" in result.output
|
|
45
|
-
assert "Module: path CLI argument" in result.output
|
|
46
|
-
assert "App name: auto-discovery" in result.output
|
|
47
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
48
|
-
assert "Starting development server 🚀" in result.output
|
|
49
46
|
assert "Server started at http://127.0.0.1:8000" in result.output
|
|
50
47
|
assert "Documentation at http://127.0.0.1:8000/docs" in result.output
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
assert "
|
|
48
|
+
# a passed path is explicit, so no source hint and no pyproject nudge
|
|
49
|
+
assert "(auto-discovered" not in result.output
|
|
50
|
+
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
51
|
+
# the step-by-step narration is --verbose only
|
|
52
|
+
assert "Searching for package file structure" not in result.output
|
|
53
|
+
assert "Configuration sources:" not in result.output
|
|
57
54
|
|
|
58
55
|
|
|
59
56
|
def test_run_uses_uvicorn_default_log_config_without_rich_logs(
|
|
@@ -83,12 +80,16 @@ def test_dev_no_args_auto_discovery() -> None:
|
|
|
83
80
|
assert mock_run.call_args.kwargs["host"] == "127.0.0.1"
|
|
84
81
|
assert mock_run.call_args.kwargs["port"] == 8000
|
|
85
82
|
assert mock_run.call_args.kwargs["reload"] is True
|
|
83
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
86
84
|
assert "Using import string: main:app" in result.output
|
|
87
|
-
|
|
88
|
-
assert "
|
|
85
|
+
# Rich can wrap the rest of the hint based on terminal and emoji width
|
|
86
|
+
assert "(auto-discovered, use --verbose" in result.output
|
|
87
|
+
# auto-discovery nudges you to pin the entrypoint
|
|
89
88
|
assert "You can configure an entrypoint in pyproject.toml" in result.output
|
|
90
89
|
assert "[tool.fastapi]" in result.output
|
|
91
90
|
assert 'entrypoint = "main:app"' in result.output
|
|
91
|
+
# the full breakdown stays behind --verbose
|
|
92
|
+
assert "Configuration sources:" not in result.output
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
def test_dev_package() -> None:
|
|
@@ -110,22 +111,14 @@ def test_dev_package() -> None:
|
|
|
110
111
|
"forwarded_allow_ips": None,
|
|
111
112
|
"log_config": get_uvicorn_log_config(),
|
|
112
113
|
}
|
|
114
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
113
115
|
assert "Using import string: nested_package.package:app" in result.output
|
|
114
|
-
assert "Module: path CLI argument" in result.output
|
|
115
|
-
assert "App name: auto-discovery" in result.output
|
|
116
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
117
|
-
assert "Starting development server 🚀" in result.output
|
|
118
116
|
assert "Server started at http://127.0.0.1:8000" in result.output
|
|
119
117
|
assert "Documentation at http://127.0.0.1:8000/docs" in result.output
|
|
120
|
-
assert (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
assert "📁 package" in result.output
|
|
126
|
-
assert "└── 🐍 __init__.py" in result.output
|
|
127
|
-
assert "└── 📁 package" in result.output
|
|
128
|
-
assert " └── 🐍 __init__.py" in result.output
|
|
118
|
+
assert "(auto-discovered" not in result.output
|
|
119
|
+
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
120
|
+
# the file tree is --verbose only
|
|
121
|
+
assert "📁 package" not in result.output
|
|
129
122
|
|
|
130
123
|
|
|
131
124
|
def test_dev_args() -> None:
|
|
@@ -163,17 +156,12 @@ def test_dev_args() -> None:
|
|
|
163
156
|
"forwarded_allow_ips": None,
|
|
164
157
|
"log_config": get_uvicorn_log_config(),
|
|
165
158
|
}
|
|
159
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
166
160
|
assert "Using import string: single_file_app:api" in result.output
|
|
167
|
-
assert "Module: path CLI argument" in result.output
|
|
168
|
-
assert "App name: --app CLI option" in result.output
|
|
169
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
170
|
-
assert "Starting development server 🚀" in result.output
|
|
171
161
|
assert "Server started at http://192.168.0.2:8080" in result.output
|
|
172
162
|
assert "Documentation at http://192.168.0.2:8080/docs" in result.output
|
|
173
|
-
assert (
|
|
174
|
-
|
|
175
|
-
in result.output
|
|
176
|
-
)
|
|
163
|
+
assert "(auto-discovered" not in result.output
|
|
164
|
+
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
177
165
|
|
|
178
166
|
|
|
179
167
|
def test_dev_env_vars() -> None:
|
|
@@ -197,17 +185,10 @@ def test_dev_env_vars() -> None:
|
|
|
197
185
|
"forwarded_allow_ips": None,
|
|
198
186
|
"log_config": get_uvicorn_log_config(),
|
|
199
187
|
}
|
|
188
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
200
189
|
assert "Using import string: single_file_app:app" in result.output
|
|
201
|
-
assert "Module: path CLI argument" in result.output
|
|
202
|
-
assert "App name: auto-discovery" in result.output
|
|
203
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
204
|
-
assert "Starting development server 🚀" in result.output
|
|
205
190
|
assert "Server started at http://127.0.0.1:8111" in result.output
|
|
206
191
|
assert "Documentation at http://127.0.0.1:8111/docs" in result.output
|
|
207
|
-
assert (
|
|
208
|
-
"Running in development mode, for production use: fastapi run"
|
|
209
|
-
in result.output
|
|
210
|
-
)
|
|
211
192
|
|
|
212
193
|
|
|
213
194
|
def test_dev_env_vars_and_args() -> None:
|
|
@@ -238,17 +219,68 @@ def test_dev_env_vars_and_args() -> None:
|
|
|
238
219
|
"forwarded_allow_ips": None,
|
|
239
220
|
"log_config": get_uvicorn_log_config(),
|
|
240
221
|
}
|
|
222
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
241
223
|
assert "Using import string: single_file_app:app" in result.output
|
|
242
|
-
assert "Module: path CLI argument" in result.output
|
|
243
|
-
assert "App name: auto-discovery" in result.output
|
|
244
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
245
|
-
assert "Starting development server 🚀" in result.output
|
|
246
224
|
assert "Server started at http://127.0.0.1:8080" in result.output
|
|
247
225
|
assert "Documentation at http://127.0.0.1:8080/docs" in result.output
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_dev_verbose() -> None:
|
|
229
|
+
"""--verbose restores the full narration and the module file tree."""
|
|
230
|
+
with changing_dir(assets_path):
|
|
231
|
+
with patch.object(uvicorn, "run") as mock_run:
|
|
232
|
+
result = runner.invoke(app, ["dev", "nested_package/package", "--verbose"])
|
|
233
|
+
assert result.exit_code == 0, result.output
|
|
234
|
+
assert mock_run.called
|
|
235
|
+
assert "Starting FastAPI in development mode" in result.output
|
|
236
|
+
assert "Searching for package file structure" in result.output
|
|
237
|
+
assert "Importing from" in result.output
|
|
238
|
+
assert "Importing the FastAPI app object" in result.output
|
|
239
|
+
assert "from nested_package.package import app" in result.output
|
|
240
|
+
assert "Using import string: nested_package.package:app" in result.output
|
|
241
|
+
# module and app come from different sources here
|
|
242
|
+
assert "Configuration sources:" in result.output
|
|
243
|
+
assert "Module: path CLI argument" in result.output
|
|
244
|
+
assert "App name: auto-discovery" in result.output
|
|
245
|
+
# the file tree
|
|
246
|
+
assert "📁 package" in result.output
|
|
247
|
+
assert "🐍 __init__.py" in result.output
|
|
248
|
+
# the source hint is folded away in verbose mode
|
|
249
|
+
assert "(auto-discovered, use --verbose to learn more)" not in result.output
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_dev_verbose_auto_discovery() -> None:
|
|
253
|
+
"""--verbose with auto-discovery shows the single 'import string' source."""
|
|
254
|
+
with changing_dir(assets_path / "default_files" / "default_main"):
|
|
255
|
+
with patch.object(uvicorn, "run") as mock_run:
|
|
256
|
+
result = runner.invoke(app, ["dev", "--verbose"])
|
|
257
|
+
assert result.exit_code == 0, result.output
|
|
258
|
+
assert mock_run.called
|
|
259
|
+
assert "Searching for package file structure" in result.output
|
|
260
|
+
assert "Importing the FastAPI app object" in result.output
|
|
261
|
+
assert "Using import string: main:app" in result.output
|
|
262
|
+
assert "Configuration sources:" in result.output
|
|
263
|
+
# module and app share the same source (auto-discovery)
|
|
264
|
+
assert "Import string: auto-discovery" in result.output
|
|
265
|
+
# auto-discovery still nudges to pin the entrypoint
|
|
266
|
+
assert "You can configure an entrypoint in pyproject.toml" in result.output
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_global_verbose_enables_debug_logging() -> None:
|
|
270
|
+
with changing_dir(assets_path):
|
|
271
|
+
with patch.object(uvicorn, "run"):
|
|
272
|
+
result = runner.invoke(app, ["--verbose", "dev", "single_file_app.py"])
|
|
273
|
+
assert result.exit_code == 0, result.output
|
|
274
|
+
|
|
275
|
+
assert logging.getLogger("fastapi_cli").level == logging.DEBUG
|
|
276
|
+
|
|
277
|
+
# a run without --verbose resets the level
|
|
278
|
+
with changing_dir(assets_path):
|
|
279
|
+
with patch.object(uvicorn, "run"):
|
|
280
|
+
result = runner.invoke(app, ["dev", "single_file_app.py"])
|
|
281
|
+
assert result.exit_code == 0, result.output
|
|
282
|
+
|
|
283
|
+
assert logging.getLogger("fastapi_cli").level == logging.INFO
|
|
252
284
|
|
|
253
285
|
|
|
254
286
|
def test_entrypoint_mutually_exclusive_with_path() -> None:
|
|
@@ -287,11 +319,8 @@ def test_run() -> None:
|
|
|
287
319
|
"forwarded_allow_ips": None,
|
|
288
320
|
"log_config": get_uvicorn_log_config(),
|
|
289
321
|
}
|
|
322
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
290
323
|
assert "Using import string: single_file_app:app" in result.output
|
|
291
|
-
assert "Module: path CLI argument" in result.output
|
|
292
|
-
assert "App name: auto-discovery" in result.output
|
|
293
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
294
|
-
assert "Starting production server 🚀" in result.output
|
|
295
324
|
assert "Server started at http://0.0.0.0:8000" in result.output
|
|
296
325
|
assert "Documentation at http://0.0.0.0:8000/docs" in result.output
|
|
297
326
|
|
|
@@ -317,14 +346,10 @@ def test_run_trust_proxy() -> None:
|
|
|
317
346
|
"forwarded_allow_ips": "*",
|
|
318
347
|
"log_config": get_uvicorn_log_config(),
|
|
319
348
|
}
|
|
349
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
320
350
|
assert "Using import string: single_file_app:app" in result.output
|
|
321
|
-
assert "Starting production server 🚀" in result.output
|
|
322
351
|
assert "Server started at http://0.0.0.0:8000" in result.output
|
|
323
352
|
assert "Documentation at http://0.0.0.0:8000/docs" in result.output
|
|
324
|
-
assert (
|
|
325
|
-
"Running in development mode, for production use: fastapi run"
|
|
326
|
-
not in result.output
|
|
327
|
-
)
|
|
328
353
|
|
|
329
354
|
|
|
330
355
|
def test_run_args() -> None:
|
|
@@ -365,17 +390,10 @@ def test_run_args() -> None:
|
|
|
365
390
|
"log_config": get_uvicorn_log_config(),
|
|
366
391
|
}
|
|
367
392
|
|
|
393
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
368
394
|
assert "Using import string: single_file_app:api" in result.output
|
|
369
|
-
assert "Module: path CLI argument" in result.output
|
|
370
|
-
assert "App name: --app CLI option" in result.output
|
|
371
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
372
|
-
assert "Starting production server 🚀" in result.output
|
|
373
395
|
assert "Server started at http://192.168.0.2:8080" in result.output
|
|
374
396
|
assert "Documentation at http://192.168.0.2:8080/docs" in result.output
|
|
375
|
-
assert (
|
|
376
|
-
"Running in development mode, for production use: fastapi run"
|
|
377
|
-
not in result.output
|
|
378
|
-
)
|
|
379
397
|
|
|
380
398
|
|
|
381
399
|
def test_run_env_vars() -> None:
|
|
@@ -399,11 +417,8 @@ def test_run_env_vars() -> None:
|
|
|
399
417
|
"forwarded_allow_ips": None,
|
|
400
418
|
"log_config": get_uvicorn_log_config(),
|
|
401
419
|
}
|
|
420
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
402
421
|
assert "Using import string: single_file_app:app" in result.output
|
|
403
|
-
assert "Module: path CLI argument" in result.output
|
|
404
|
-
assert "App name: auto-discovery" in result.output
|
|
405
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
406
|
-
assert "Starting production server 🚀" in result.output
|
|
407
422
|
assert "Server started at http://0.0.0.0:8111" in result.output
|
|
408
423
|
assert "Documentation at http://0.0.0.0:8111/docs" in result.output
|
|
409
424
|
|
|
@@ -436,11 +451,8 @@ def test_run_env_vars_and_args() -> None:
|
|
|
436
451
|
"forwarded_allow_ips": None,
|
|
437
452
|
"log_config": get_uvicorn_log_config(),
|
|
438
453
|
}
|
|
454
|
+
assert "Starting FastAPI in production mode" in result.output
|
|
439
455
|
assert "Using import string: single_file_app:app" in result.output
|
|
440
|
-
assert "Module: path CLI argument" in result.output
|
|
441
|
-
assert "App name: auto-discovery" in result.output
|
|
442
|
-
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
443
|
-
assert "Starting production server 🚀" in result.output
|
|
444
456
|
assert "Server started at http://0.0.0.0:8080" in result.output
|
|
445
457
|
assert "Documentation at http://0.0.0.0:8080/docs" in result.output
|
|
446
458
|
|
|
@@ -485,7 +497,7 @@ def test_public_url_env_var(command: str, public_url: str) -> None:
|
|
|
485
497
|
|
|
486
498
|
assert "Using import string: single_file_app:app" in result.output
|
|
487
499
|
assert (
|
|
488
|
-
f"Starting {'development' if command == 'dev' else 'production'}
|
|
500
|
+
f"Starting FastAPI in {'development' if command == 'dev' else 'production'} mode"
|
|
489
501
|
in result.output
|
|
490
502
|
)
|
|
491
503
|
expected_url_base = public_url.rstrip("/")
|
|
@@ -601,7 +613,8 @@ def test_dev_with_import_string() -> None:
|
|
|
601
613
|
"log_config": get_uvicorn_log_config(),
|
|
602
614
|
}
|
|
603
615
|
assert "Using import string: single_file_app:api" in result.output
|
|
604
|
-
|
|
616
|
+
# an explicit entrypoint is not auto-discovery
|
|
617
|
+
assert "(auto-discovered" not in result.output
|
|
605
618
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
606
619
|
|
|
607
620
|
|
|
@@ -625,7 +638,7 @@ def test_run_with_import_string() -> None:
|
|
|
625
638
|
"log_config": get_uvicorn_log_config(),
|
|
626
639
|
}
|
|
627
640
|
assert "Using import string: single_file_app:app" in result.output
|
|
628
|
-
assert "
|
|
641
|
+
assert "(auto-discovered" not in result.output
|
|
629
642
|
assert "You can configure an entrypoint in pyproject.toml" not in result.output
|
|
630
643
|
|
|
631
644
|
|
|
@@ -26,8 +26,9 @@ def test_dev_with_pyproject_app_config_uses() -> None:
|
|
|
26
26
|
assert mock_run.call_args.kwargs["reload"] is True
|
|
27
27
|
|
|
28
28
|
assert "Using import string: my_module:app" in result.output
|
|
29
|
-
|
|
30
|
-
assert "
|
|
29
|
+
# a configured entrypoint is not auto-discovery
|
|
30
|
+
assert "(auto-discovered" not in result.output
|
|
31
|
+
assert "Configuration sources:" not in result.output
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def test_run_with_pyproject_app_config() -> None:
|
|
@@ -44,8 +45,8 @@ def test_run_with_pyproject_app_config() -> None:
|
|
|
44
45
|
assert mock_run.call_args.kwargs["reload"] is False
|
|
45
46
|
|
|
46
47
|
assert "Using import string: my_module:app" in result.output
|
|
47
|
-
assert "
|
|
48
|
-
assert "
|
|
48
|
+
assert "(auto-discovered" not in result.output
|
|
49
|
+
assert "Configuration sources:" not in result.output
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def test_cli_arg_overrides_pyproject_config() -> None:
|
|
@@ -58,9 +59,8 @@ def test_cli_arg_overrides_pyproject_config() -> None:
|
|
|
58
59
|
assert result.exit_code == 0, result.output
|
|
59
60
|
|
|
60
61
|
assert mock_run.call_args.kwargs["app"] == "another_module:app"
|
|
61
|
-
assert "
|
|
62
|
-
assert "
|
|
63
|
-
assert "App name: auto-discovery" in result.output
|
|
62
|
+
assert "Using import string: another_module:app" in result.output
|
|
63
|
+
assert "Configuration sources:" not in result.output
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def test_pyproject_app_config_invalid_format() -> None:
|
|
@@ -47,7 +47,8 @@ def test_custom_formatter() -> None:
|
|
|
47
47
|
|
|
48
48
|
formatted = formatter.formatMessage(record)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
# the log level is shown as a colored bar bullet, like `fastapi cloud logs`
|
|
51
|
+
assert "▕" in formatted
|
|
51
52
|
assert "127.0.0.1" in formatted
|
|
52
53
|
assert "GET / HTTP/1.1" in formatted
|
|
53
54
|
assert "200" in formatted
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.29"
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import sys
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from rich_toolkit import RichToolkit, RichToolkitTheme
|
|
6
|
-
from rich_toolkit.styles import TaggedStyle
|
|
7
|
-
from uvicorn.logging import DefaultFormatter
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CustomFormatter(DefaultFormatter):
|
|
11
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
12
|
-
super().__init__(*args, **kwargs)
|
|
13
|
-
self.toolkit = get_rich_toolkit()
|
|
14
|
-
|
|
15
|
-
def formatMessage(self, record: logging.LogRecord) -> str:
|
|
16
|
-
message = record.getMessage()
|
|
17
|
-
result = self.toolkit.print_as_string(message, tag=record.levelname)
|
|
18
|
-
# Prepend newline to fix alignment after ^C is printed by the terminal
|
|
19
|
-
if message == "Shutting down":
|
|
20
|
-
result = "\n" + result
|
|
21
|
-
return result
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def should_use_rich_logs() -> bool:
|
|
25
|
-
"""Return True when stdout is a TTY and rich logs should be used, False otherwise."""
|
|
26
|
-
return sys.stdout.isatty()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_uvicorn_log_config() -> dict[str, Any]:
|
|
30
|
-
return {
|
|
31
|
-
"version": 1,
|
|
32
|
-
"disable_existing_loggers": False,
|
|
33
|
-
"formatters": {
|
|
34
|
-
"default": {
|
|
35
|
-
"()": CustomFormatter,
|
|
36
|
-
"fmt": "%(levelprefix)s %(message)s",
|
|
37
|
-
"use_colors": None,
|
|
38
|
-
},
|
|
39
|
-
"access": {
|
|
40
|
-
"()": CustomFormatter,
|
|
41
|
-
"fmt": "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s",
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
"handlers": {
|
|
45
|
-
"default": {
|
|
46
|
-
"formatter": "default",
|
|
47
|
-
"class": "logging.StreamHandler",
|
|
48
|
-
"stream": "ext://sys.stderr",
|
|
49
|
-
},
|
|
50
|
-
"access": {
|
|
51
|
-
"formatter": "access",
|
|
52
|
-
"class": "logging.StreamHandler",
|
|
53
|
-
"stream": "ext://sys.stdout",
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
"loggers": {
|
|
57
|
-
"uvicorn": {
|
|
58
|
-
"handlers": ["default"],
|
|
59
|
-
"level": "INFO",
|
|
60
|
-
"propagate": False,
|
|
61
|
-
},
|
|
62
|
-
"uvicorn.error": {"level": "INFO"},
|
|
63
|
-
"uvicorn.access": {
|
|
64
|
-
"handlers": ["access"],
|
|
65
|
-
"level": "INFO",
|
|
66
|
-
"propagate": False,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
logger = logging.getLogger(__name__)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_rich_toolkit() -> RichToolkit:
|
|
76
|
-
theme = RichToolkitTheme(
|
|
77
|
-
style=TaggedStyle(tag_width=11),
|
|
78
|
-
theme={
|
|
79
|
-
"tag.title": "white on #009485",
|
|
80
|
-
"tag": "white on #007166",
|
|
81
|
-
"placeholder": "grey85",
|
|
82
|
-
"text": "white",
|
|
83
|
-
"selected": "#007166",
|
|
84
|
-
"result": "grey85",
|
|
85
|
-
"progress": "on #007166",
|
|
86
|
-
"error": "red",
|
|
87
|
-
"log.info": "black on blue",
|
|
88
|
-
},
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
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.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_api/app/api.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/api.py
RENAMED
|
File without changes
|
{fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/app.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/api.py
RENAMED
|
File without changes
|
{fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/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.29 → fastapi_cli-0.0.31}/tests/assets/default_files/non_default/nonstandard.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/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
|