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.
Files changed (77) hide show
  1. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/PKG-INFO +1 -1
  2. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/pyproject.toml +1 -1
  3. fastapi_cli-0.0.30/src/fastapi_cli/__init__.py +1 -0
  4. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/cli.py +33 -32
  5. fastapi_cli-0.0.30/src/fastapi_cli/utils/cli.py +190 -0
  6. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_cli.py +31 -11
  7. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_cli.py +18 -3
  8. fastapi_cli-0.0.28/src/fastapi_cli/__init__.py +0 -1
  9. fastapi_cli-0.0.28/src/fastapi_cli/utils/cli.py +0 -85
  10. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/LICENSE +0 -0
  11. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/README.md +0 -0
  12. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/add_latest_release_date.py +0 -0
  13. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/format.sh +0 -0
  14. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/lint.sh +0 -0
  15. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/prepare_release.py +0 -0
  16. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/test-cov-html.sh +0 -0
  17. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/scripts/test.sh +0 -0
  18. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/__main__.py +0 -0
  19. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/config.py +0 -0
  20. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/discover.py +0 -0
  21. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/exceptions.py +0 -0
  22. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/logging.py +0 -0
  23. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/py.typed +0 -0
  24. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/src/fastapi_cli/utils/__init__.py +0 -0
  25. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/__init__.py +0 -0
  26. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/broken_package/mod/__init__.py +0 -0
  27. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/broken_package/mod/app.py +0 -0
  28. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/broken_package/utils.py +0 -0
  29. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_api/api.py +0 -0
  30. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app/api.py +0 -0
  31. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app/app.py +0 -0
  32. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  33. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  34. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  35. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  36. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  37. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  38. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  39. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  40. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  41. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  42. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  43. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_main/api.py +0 -0
  44. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_main/app.py +0 -0
  45. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/default_main/main.py +0 -0
  46. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  47. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/__init__.py +0 -0
  48. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/__init__.py +0 -0
  49. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/core/__init__.py +0 -0
  50. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/core/utils.py +0 -0
  51. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/__init__.py +0 -0
  52. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/api.py +0 -0
  53. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/app.py +0 -0
  54. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/nested_package/package/mod/other.py +0 -0
  55. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/__init__.py +0 -0
  56. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/core/__init__.py +0 -0
  57. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/core/utils.py +0 -0
  58. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/__init__.py +0 -0
  59. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/api.py +0 -0
  60. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/app.py +0 -0
  61. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/package/mod/other.py +0 -0
  62. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/pyproject_config/another_module.py +0 -0
  63. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/pyproject_config/my_module.py +0 -0
  64. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/pyproject_config/pyproject.toml +0 -0
  65. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/single_file_api.py +0 -0
  66. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/single_file_app.py +0 -0
  67. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/assets/single_file_other.py +0 -0
  68. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/conftest.py +0 -0
  69. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_cli_pyproject.py +0 -0
  70. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_discover.py +0 -0
  71. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_prepare_release.py +0 -0
  72. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_requirements.py +0 -0
  73. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_default_dir.py +0 -0
  74. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_default_file.py +0 -0
  75. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_package.py +0 -0
  76. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/test_utils_single_file.py +0 -0
  77. {fastapi_cli-0.0.28 → fastapi_cli-0.0.30}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cli
3
- Version: 0.0.28
3
+ Version: 0.0.30
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.28"
41
+ version = "0.0.30"
42
42
 
43
43
  [project.optional-dependencies]
44
44
  standard = [
@@ -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 get_rich_toolkit, get_uvicorn_log_config
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.print_title(f"Starting {server_type} server 🚀", tag="FastAPI")
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.print(f"Importing from {module_data.extra_sys_path}")
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, tag="module")
203
- toolkit.print_line()
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"[underline]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]"
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.print(
216
- f"Using import string: [blue]{import_string}[/]",
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.print_line()
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" • Import string: {mod_source_desc}")
225
+ toolkit.print(f"• Import string: {mod_source_desc}")
226
226
  else:
227
- toolkit.print(f" • Module: {mod_source_desc}")
228
- toolkit.print(f" • App name: {app_source_desc}")
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
- tag="tip",
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
- f"Server started at [link={url}]{url}[/]",
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
- tag="tip",
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
- log_config=get_uvicorn_log_config(),
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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 server 🚀" in result.output
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'} server 🚀"
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 CustomFormatter, get_uvicorn_log_config
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
- assert "INFO" in formatted
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