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.
Files changed (77) hide show
  1. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/PKG-INFO +1 -1
  2. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/pyproject.toml +1 -1
  3. fastapi_cli-0.0.31/src/fastapi_cli/__init__.py +1 -0
  4. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/cli.py +69 -49
  5. fastapi_cli-0.0.31/src/fastapi_cli/utils/cli.py +190 -0
  6. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_cli.py +91 -78
  7. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_cli_pyproject.py +7 -7
  8. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_cli.py +2 -1
  9. fastapi_cli-0.0.29/src/fastapi_cli/__init__.py +0 -1
  10. fastapi_cli-0.0.29/src/fastapi_cli/utils/cli.py +0 -91
  11. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/LICENSE +0 -0
  12. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/README.md +0 -0
  13. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/add_latest_release_date.py +0 -0
  14. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/format.sh +0 -0
  15. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/lint.sh +0 -0
  16. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/prepare_release.py +0 -0
  17. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/test-cov-html.sh +0 -0
  18. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/scripts/test.sh +0 -0
  19. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/__main__.py +0 -0
  20. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/config.py +0 -0
  21. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/discover.py +0 -0
  22. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/exceptions.py +0 -0
  23. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/logging.py +0 -0
  24. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/py.typed +0 -0
  25. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/src/fastapi_cli/utils/__init__.py +0 -0
  26. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/__init__.py +0 -0
  27. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/broken_package/mod/__init__.py +0 -0
  28. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/broken_package/mod/app.py +0 -0
  29. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/broken_package/utils.py +0 -0
  30. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_api/api.py +0 -0
  31. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app/api.py +0 -0
  32. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app/app.py +0 -0
  33. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  34. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  35. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  36. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  37. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  38. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  39. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  40. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  41. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  42. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  43. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  44. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_main/api.py +0 -0
  45. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_main/app.py +0 -0
  46. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/default_main/main.py +0 -0
  47. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  48. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/__init__.py +0 -0
  49. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/__init__.py +0 -0
  50. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/core/__init__.py +0 -0
  51. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/core/utils.py +0 -0
  52. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/__init__.py +0 -0
  53. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/api.py +0 -0
  54. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/app.py +0 -0
  55. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/nested_package/package/mod/other.py +0 -0
  56. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/__init__.py +0 -0
  57. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/core/__init__.py +0 -0
  58. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/core/utils.py +0 -0
  59. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/__init__.py +0 -0
  60. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/api.py +0 -0
  61. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/app.py +0 -0
  62. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/package/mod/other.py +0 -0
  63. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/pyproject_config/another_module.py +0 -0
  64. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/pyproject_config/my_module.py +0 -0
  65. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/pyproject_config/pyproject.toml +0 -0
  66. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/single_file_api.py +0 -0
  67. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/single_file_app.py +0 -0
  68. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/assets/single_file_other.py +0 -0
  69. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/conftest.py +0 -0
  70. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_discover.py +0 -0
  71. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_prepare_release.py +0 -0
  72. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_requirements.py +0 -0
  73. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_default_dir.py +0 -0
  74. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_default_file.py +0 -0
  75. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_package.py +0 -0
  76. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/test_utils_single_file.py +0 -0
  77. {fastapi_cli-0.0.29 → fastapi_cli-0.0.31}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cli
3
- Version: 0.0.29
3
+ Version: 0.0.31
4
4
  Summary: Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀
5
5
  Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <tiangolo@gmail.com>
6
6
  License-Expression: MIT
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "rich-toolkit >= 0.14.8",
39
39
  "tomli >= 2.0.0; python_version < '3.11'",
40
40
  ]
41
- version = "0.0.29"
41
+ version = "0.0.31"
42
42
 
43
43
  [project.optional-dependencies]
44
44
  standard = [
@@ -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.print_title(f"Starting {server_type} server 🚀", tag="FastAPI")
150
- toolkit.print_line()
150
+ toolkit.print(f"Starting FastAPI in {server_type} mode", emoji="⚡️")
151
151
 
152
- toolkit.print(
153
- "Searching for package file structure from directories with [blue]__init__.py[/blue] files"
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
- toolkit.print(f"Importing from {module_data.extra_sys_path}")
201
- toolkit.print_line()
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
- if module_data.module_paths:
204
- root_tree = _get_module_tree(module_data.module_paths)
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
- toolkit.print(
210
- "Importing the FastAPI app object from the module with the following code:",
211
- tag="code",
212
- )
213
- toolkit.print_line()
214
- toolkit.print(
215
- f"[underline]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]"
216
- )
217
- toolkit.print_line()
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
- mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
225
- app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
226
- toolkit.print_line()
227
- toolkit.print("Configuration sources:", tag="info")
228
- if mod_source_desc == app_source_desc:
229
- toolkit.print(f" • Import string: {mod_source_desc}")
230
- else:
231
- toolkit.print(f" • Module: {mod_source_desc}")
232
- toolkit.print(f" • App name: {app_source_desc}")
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
- if import_data.module_config_source == "auto-discovery":
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
- tag="tip",
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
- f"Server started at [link={url}]{url}[/]",
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
- assert (
52
- "Running in development mode, for production use: fastapi run"
53
- in result.output
54
- )
55
-
56
- assert "🐍 single_file_app.py" in result.output
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
- assert "Configuration sources:" in result.output
88
- assert "Import string: auto-discovery" in result.output
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
- "Running in development mode, for production use: fastapi run"
122
- in result.output
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
- "Running in development mode, for production use: fastapi run"
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
- assert (
249
- "Running in development mode, for production use: fastapi run"
250
- in result.output
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'} server 🚀"
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
- assert "Import string: --entrypoint CLI option" in result.output
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 "Import string: --entrypoint CLI option" in result.output
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
- assert "Configuration sources:" in result.output
30
- assert "Import string: entrypoint in pyproject.toml" in result.output
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 "Configuration sources:" in result.output
48
- assert "Import string: entrypoint in pyproject.toml" in result.output
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 "Configuration sources:" in result.output
62
- assert "Module: path CLI argument" in result.output
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
- assert "INFO" in formatted
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