fastapi-cli 0.0.24__tar.gz → 0.0.26__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 (76) hide show
  1. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/PKG-INFO +1 -1
  2. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/pyproject.toml +20 -3
  3. fastapi_cli-0.0.26/scripts/add_latest_release_date.py +40 -0
  4. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/lint.sh +1 -0
  5. fastapi_cli-0.0.26/scripts/prepare_release.py +216 -0
  6. fastapi_cli-0.0.26/src/fastapi_cli/__init__.py +1 -0
  7. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/cli.py +58 -4
  8. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/config.py +5 -2
  9. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/discover.py +33 -3
  10. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_cli.py +38 -0
  11. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_cli_pyproject.py +7 -0
  12. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_discover.py +20 -6
  13. fastapi_cli-0.0.26/tests/test_prepare_release.py +296 -0
  14. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_requirements.py +3 -3
  15. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/utils.py +1 -2
  16. fastapi_cli-0.0.24/src/fastapi_cli/__init__.py +0 -1
  17. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/LICENSE +0 -0
  18. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/README.md +0 -0
  19. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/format.sh +0 -0
  20. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/test-cov-html.sh +0 -0
  21. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/test.sh +0 -0
  22. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/__main__.py +0 -0
  23. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/exceptions.py +0 -0
  24. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/logging.py +0 -0
  25. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/py.typed +0 -0
  26. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/utils/__init__.py +0 -0
  27. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/utils/cli.py +0 -0
  28. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/__init__.py +0 -0
  29. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/broken_package/mod/__init__.py +0 -0
  30. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/broken_package/mod/app.py +0 -0
  31. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/broken_package/utils.py +0 -0
  32. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_api/api.py +0 -0
  33. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app/api.py +0 -0
  34. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app/app.py +0 -0
  35. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  36. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  37. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  38. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  39. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  40. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  41. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  42. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  43. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  44. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  45. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  46. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_main/api.py +0 -0
  47. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_main/app.py +0 -0
  48. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_main/main.py +0 -0
  49. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  50. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/__init__.py +0 -0
  51. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/__init__.py +0 -0
  52. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/core/__init__.py +0 -0
  53. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/core/utils.py +0 -0
  54. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/__init__.py +0 -0
  55. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/api.py +0 -0
  56. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/app.py +0 -0
  57. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/other.py +0 -0
  58. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/__init__.py +0 -0
  59. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/core/__init__.py +0 -0
  60. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/core/utils.py +0 -0
  61. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/__init__.py +0 -0
  62. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/api.py +0 -0
  63. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/app.py +0 -0
  64. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/other.py +0 -0
  65. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/pyproject_config/another_module.py +0 -0
  66. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/pyproject_config/my_module.py +0 -0
  67. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/pyproject_config/pyproject.toml +0 -0
  68. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/single_file_api.py +0 -0
  69. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/single_file_app.py +0 -0
  70. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/single_file_other.py +0 -0
  71. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/conftest.py +0 -0
  72. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_cli.py +0 -0
  73. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_default_dir.py +0 -0
  74. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_default_file.py +0 -0
  75. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_package.py +0 -0
  76. {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_single_file.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cli
3
- Version: 0.0.24
3
+ Version: 0.0.26
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.24"
41
+ version = "0.0.26"
42
42
 
43
43
  [project.optional-dependencies]
44
44
  standard = [
@@ -63,6 +63,7 @@ Changelog = "https://github.com/fastapi/fastapi-cli/blob/main/release-notes.md"
63
63
  dev = [
64
64
  { include-group = "tests" },
65
65
  "prek>=0.2.24,<1.0.0",
66
+ "zizmor>=1.24.1",
66
67
  ]
67
68
  github-actions = [
68
69
  "smokeshow>=0.5.0",
@@ -70,10 +71,11 @@ github-actions = [
70
71
  tests = [
71
72
  "coverage[toml]>=6.2,<8.0",
72
73
  "fastapi>=0.128.0",
73
- "mypy==1.19.1",
74
+ "mypy==2.1.0",
74
75
  "pytest>=7.4.0,<10.0.0",
75
- "ruff==0.15.2",
76
+ "ruff>=0.15.15",
76
77
  "uvicorn>=0.39.0",
78
+ "ty>=0.0.25",
77
79
  ]
78
80
 
79
81
  [build-system]
@@ -161,3 +163,18 @@ known-third-party = [
161
163
 
162
164
  [tool.ruff.lint.pyupgrade]
163
165
  keep-runtime-typing = true
166
+
167
+ [tool.ty.terminal]
168
+ error-on-warning = true
169
+
170
+ [tool.typos.files]
171
+ extend-exclude = [
172
+ "coverage/",
173
+ "dist/",
174
+ "release-notes.md",
175
+ "htmlcov/",
176
+ "uv.lock",
177
+ ]
178
+
179
+ [tool.typos.default.extend-identifiers]
180
+ alls = "alls"
@@ -0,0 +1,40 @@
1
+ """Check release-notes.md and add today's date to the latest release header if missing."""
2
+
3
+ import re
4
+ import sys
5
+ from datetime import date
6
+
7
+ RELEASE_NOTES_FILE = "release-notes.md"
8
+ RELEASE_HEADER_PATTERN = re.compile(r"^## (\d+\.\d+\.\d+)\s*(\(.*\))?\s*$")
9
+
10
+
11
+ def main() -> None:
12
+ with open(RELEASE_NOTES_FILE) as f:
13
+ lines = f.readlines()
14
+
15
+ for i, line in enumerate(lines):
16
+ match = RELEASE_HEADER_PATTERN.match(line)
17
+ if not match:
18
+ continue
19
+
20
+ version = match.group(1)
21
+ date_part = match.group(2)
22
+
23
+ if date_part:
24
+ print(f"Latest release {version} already has a date: {date_part}")
25
+ sys.exit(0)
26
+
27
+ today = date.today().isoformat()
28
+ lines[i] = f"## {version} ({today})\n"
29
+ print(f"Added date: {version} ({today})")
30
+
31
+ with open(RELEASE_NOTES_FILE, "w") as f:
32
+ f.writelines(lines)
33
+ sys.exit(0)
34
+
35
+ print("No release header found")
36
+ sys.exit(1)
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()
@@ -4,5 +4,6 @@ set -e
4
4
  set -x
5
5
 
6
6
  mypy src tests
7
+ ty check src tests
7
8
  ruff check src tests scripts
8
9
  ruff format src tests --check
@@ -0,0 +1,216 @@
1
+ """Prepare a release by updating the package version and release notes."""
2
+
3
+ import re
4
+ from datetime import date
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ VERSION_PATTERN = re.compile(r'(?m)^__version__ = "(\d+\.\d+\.\d+)"$')
12
+ VERSION_HEADING_PATTERN = re.compile(r"(?m)^## (\d+\.\d+\.\d+)(?: \([^)]+\))?$")
13
+ RELEASE_NOTES_HEADER = "# Release Notes\n\n"
14
+ LATEST_CHANGES_HEADER = "## Latest Changes"
15
+
16
+
17
+ class BumpType(str, Enum):
18
+ major = "major"
19
+ minor = "minor"
20
+ patch = "patch"
21
+
22
+
23
+ app = typer.Typer()
24
+
25
+
26
+ def parse_version(version: str) -> tuple[int, int, int]:
27
+ match = re.fullmatch(r"\d+\.\d+\.\d+", version)
28
+ if not match:
29
+ raise ValueError(f"Invalid version: {version!r}. Expected format: X.Y.Z")
30
+ major, minor, patch = version.split(".")
31
+ return int(major), int(minor), int(patch)
32
+
33
+
34
+ def get_current_version(content: str, version_file: Path) -> str:
35
+ matches = list(VERSION_PATTERN.finditer(content))
36
+ if len(matches) != 1:
37
+ raise RuntimeError(
38
+ f"Expected exactly one __version__ assignment in {version_file}, "
39
+ f"found {len(matches)}"
40
+ )
41
+ return matches[0].group(1)
42
+
43
+
44
+ def bump_version(version: str, bump: BumpType) -> str:
45
+ major, minor, patch = parse_version(version)
46
+ if bump is BumpType.major:
47
+ return f"{major + 1}.0.0"
48
+ if bump is BumpType.minor:
49
+ return f"{major}.{minor + 1}.0"
50
+ return f"{major}.{minor}.{patch + 1}"
51
+
52
+
53
+ def update_version_file(content: str, version: str, version_file: Path) -> str:
54
+ current_version = get_current_version(content, version_file)
55
+ if parse_version(version) <= parse_version(current_version):
56
+ raise RuntimeError(
57
+ f"New version {version} must be greater than current version {current_version}"
58
+ )
59
+ return VERSION_PATTERN.sub(f'__version__ = "{version}"', content, count=1)
60
+
61
+
62
+ def update_release_notes(
63
+ content: str, version: str, release_date: date, release_notes_file: Path
64
+ ) -> str:
65
+ if not content.startswith(RELEASE_NOTES_HEADER):
66
+ raise RuntimeError(
67
+ f"{release_notes_file} must start with {RELEASE_NOTES_HEADER!r}"
68
+ )
69
+ if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M):
70
+ raise RuntimeError(f"Release notes already contain a section for {version}")
71
+
72
+ latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n"
73
+ if not content.startswith(latest_header):
74
+ raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}")
75
+
76
+ release_header = f"## {version} ({release_date.isoformat()})"
77
+ return content.replace(
78
+ latest_header,
79
+ f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n",
80
+ 1,
81
+ )
82
+
83
+
84
+ def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:
85
+ version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$")
86
+ match = version_heading.search(content)
87
+ if not match:
88
+ raise RuntimeError(
89
+ f"Could not find release notes section for {version} in {release_notes_file}"
90
+ )
91
+
92
+ next_match = VERSION_HEADING_PATTERN.search(content, match.end())
93
+ end = next_match.start() if next_match else len(content)
94
+ body = content[match.end() : end].strip()
95
+ if not body:
96
+ raise RuntimeError(
97
+ f"Release notes section for {version} in {release_notes_file} is empty"
98
+ )
99
+ return f"{body}\n"
100
+
101
+
102
+ @app.command()
103
+ def prepare(
104
+ bump: Annotated[
105
+ BumpType,
106
+ typer.Argument(
107
+ envvar="PREPARE_RELEASE_BUMP",
108
+ help="The release bump to make: major, minor, or patch.",
109
+ ),
110
+ ],
111
+ version_file: Annotated[
112
+ Path,
113
+ typer.Option(
114
+ envvar="PREPARE_RELEASE_VERSION_FILE",
115
+ exists=True,
116
+ file_okay=True,
117
+ dir_okay=False,
118
+ readable=True,
119
+ writable=True,
120
+ help="Path to the Python file containing the __version__ assignment.",
121
+ ),
122
+ ],
123
+ release_notes_file: Annotated[
124
+ Path,
125
+ typer.Option(
126
+ envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",
127
+ exists=True,
128
+ file_okay=True,
129
+ dir_okay=False,
130
+ readable=True,
131
+ writable=True,
132
+ help="Path to the release notes Markdown file.",
133
+ ),
134
+ ],
135
+ release_date: Annotated[
136
+ str,
137
+ typer.Option(
138
+ "--date",
139
+ envvar="PREPARE_RELEASE_DATE",
140
+ help="Release date in YYYY-MM-DD format. Defaults to today.",
141
+ ),
142
+ ] = date.today().isoformat(),
143
+ ) -> None:
144
+ parsed_release_date = date.fromisoformat(release_date or date.today().isoformat())
145
+
146
+ version_file_content = version_file.read_text()
147
+ release_notes_content = release_notes_file.read_text()
148
+ version = bump_version(
149
+ get_current_version(version_file_content, version_file), bump
150
+ )
151
+
152
+ version_file.write_text(
153
+ update_version_file(version_file_content, version, version_file)
154
+ )
155
+ release_notes_file.write_text(
156
+ update_release_notes(
157
+ release_notes_content, version, parsed_release_date, release_notes_file
158
+ )
159
+ )
160
+
161
+ typer.echo(f"Prepared release {version} ({parsed_release_date.isoformat()})")
162
+
163
+
164
+ @app.command()
165
+ def current_version(
166
+ version_file: Annotated[
167
+ Path,
168
+ typer.Option(
169
+ envvar="PREPARE_RELEASE_VERSION_FILE",
170
+ exists=True,
171
+ file_okay=True,
172
+ dir_okay=False,
173
+ readable=True,
174
+ help="Path to the Python file containing the __version__ assignment.",
175
+ ),
176
+ ],
177
+ ) -> None:
178
+ typer.echo(get_current_version(version_file.read_text(), version_file))
179
+
180
+
181
+ @app.command()
182
+ def release_notes(
183
+ version_file: Annotated[
184
+ Path,
185
+ typer.Option(
186
+ envvar="PREPARE_RELEASE_VERSION_FILE",
187
+ exists=True,
188
+ file_okay=True,
189
+ dir_okay=False,
190
+ readable=True,
191
+ help="Path to the Python file containing the __version__ assignment.",
192
+ ),
193
+ ],
194
+ release_notes_file: Annotated[
195
+ Path,
196
+ typer.Option(
197
+ envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",
198
+ exists=True,
199
+ file_okay=True,
200
+ dir_okay=False,
201
+ readable=True,
202
+ help="Path to the release notes Markdown file.",
203
+ ),
204
+ ],
205
+ ) -> None:
206
+ version = get_current_version(version_file.read_text(), version_file)
207
+ typer.echo(
208
+ get_release_notes_body(
209
+ release_notes_file.read_text(), version, release_notes_file
210
+ ),
211
+ nl=False,
212
+ )
213
+
214
+
215
+ if __name__ == "__main__":
216
+ app()
@@ -0,0 +1 @@
1
+ __version__ = "0.0.26"
@@ -5,10 +5,16 @@ from typing import Annotated, Any
5
5
  import typer
6
6
  from pydantic import ValidationError
7
7
  from rich import print
8
+ from rich.syntax import Syntax
8
9
  from rich.tree import Tree
9
10
 
10
11
  from fastapi_cli.config import FastAPIConfig
11
- from fastapi_cli.discover import get_import_data, get_import_data_from_import_string
12
+ from fastapi_cli.discover import (
13
+ AppConfigSource,
14
+ ModuleConfigSource,
15
+ get_import_data,
16
+ get_import_data_from_import_string,
17
+ )
12
18
  from fastapi_cli.exceptions import FastAPICLIException
13
19
 
14
20
  from . import __version__
@@ -22,10 +28,19 @@ app = typer.Typer(
22
28
  logger = logging.getLogger(__name__)
23
29
 
24
30
 
31
+ SOURCE_DESCRIPTIONS: dict[ModuleConfigSource | AppConfigSource, str] = {
32
+ "entrypoint-option": "[blue]--entrypoint[/] CLI option",
33
+ "entrypoint-pyproject": "[blue]entrypoint[/] in [blue]pyproject.toml[/]",
34
+ "path-argument": "[blue]path[/] CLI argument",
35
+ "app-option": "[blue]--app[/] CLI option",
36
+ "auto-discovery": "auto-discovery",
37
+ }
38
+
39
+
25
40
  try:
26
41
  import uvicorn
27
42
  except ImportError: # pragma: no cover
28
- uvicorn = None # type: ignore[assignment]
43
+ uvicorn = None # type: ignore[assignment] # ty: ignore[invalid-assignment]
29
44
 
30
45
 
31
46
  try:
@@ -39,7 +54,7 @@ except ImportError: # pragma: no cover
39
54
 
40
55
 
41
56
  try:
42
- from fastapi_new.cli import ( # type: ignore[import-not-found]
57
+ from fastapi_new.cli import ( # type: ignore[import-not-found] # ty: ignore[unresolved-import]
43
58
  app as fastapi_new_cli,
44
59
  )
45
60
 
@@ -51,6 +66,15 @@ except ImportError: # pragma: no cover
51
66
  def version_callback(value: bool) -> None:
52
67
  if value:
53
68
  print(f"FastAPI CLI version: [green]{__version__}[/green]")
69
+ try:
70
+ from fastapi_cloud_cli import (
71
+ __version__ as cloud_cli_version,
72
+ )
73
+
74
+ print(f"FastAPI Cloud CLI version: [green]{cloud_cli_version}[/green]")
75
+ except ImportError: # pragma: no cover
76
+ pass
77
+
54
78
  raise typer.Exit()
55
79
 
56
80
 
@@ -151,7 +175,9 @@ def _run(
151
175
  if path or app:
152
176
  import_data = get_import_data(path=path, app_name=app)
153
177
  elif config.entrypoint:
154
- import_data = get_import_data_from_import_string(config.entrypoint)
178
+ import_data = get_import_data_from_import_string(
179
+ config.entrypoint, config.from_pyproject
180
+ )
155
181
  else:
156
182
  import_data = get_import_data()
157
183
  except FastAPICLIException as e:
@@ -189,6 +215,34 @@ def _run(
189
215
  tag="app",
190
216
  )
191
217
 
218
+ mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
219
+ app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
220
+ toolkit.print_line()
221
+ toolkit.print("Configuration sources:", tag="info")
222
+ if mod_source_desc == app_source_desc:
223
+ toolkit.print(f" • Import string: {mod_source_desc}")
224
+ else:
225
+ toolkit.print(f" • Module: {mod_source_desc}")
226
+ toolkit.print(f" • App name: {app_source_desc}")
227
+
228
+ if import_data.module_config_source == "auto-discovery":
229
+ toolkit.print_line()
230
+ toolkit.print(
231
+ "You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
232
+ tag="tip",
233
+ )
234
+ toolkit.print_line()
235
+ toolkit.print(
236
+ Syntax(
237
+ (
238
+ "[tool.fastapi]\n"
239
+ f'entrypoint = "{import_data.module_data.module_import_str}:{import_data.app_name}"'
240
+ ),
241
+ "toml",
242
+ theme="ansi_light",
243
+ )
244
+ )
245
+
192
246
  url = f"http://{host}:{port}"
193
247
  url_docs = f"{url}/docs"
194
248
 
@@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
9
9
 
10
10
  class FastAPIConfig(BaseModel):
11
11
  entrypoint: StrictStr | None = None
12
+ from_pyproject: bool = False
12
13
 
13
14
  @classmethod
14
15
  def _read_pyproject_toml(cls) -> dict[str, Any]:
@@ -19,7 +20,7 @@ class FastAPIConfig(BaseModel):
19
20
  return {}
20
21
 
21
22
  try:
22
- import tomllib # type: ignore[import-not-found, unused-ignore]
23
+ import tomllib # type: ignore[import-not-found, unused-ignore] # ty: ignore[unresolved-import]
23
24
  except ImportError:
24
25
  try:
25
26
  import tomli as tomllib # type: ignore[no-redef, import-not-found, unused-ignore]
@@ -30,7 +31,7 @@ class FastAPIConfig(BaseModel):
30
31
  with open(pyproject_path, "rb") as f:
31
32
  data = tomllib.load(f)
32
33
 
33
- return data.get("tool", {}).get("fastapi", {}) # type: ignore
34
+ return data.get("tool", {}).get("fastapi", {}) # type: ignore[no-any-return]
34
35
 
35
36
  @classmethod
36
37
  def resolve(cls, entrypoint: str | None = None) -> "FastAPIConfig":
@@ -39,4 +40,6 @@ class FastAPIConfig(BaseModel):
39
40
  if entrypoint is not None:
40
41
  config["entrypoint"] = entrypoint
41
42
 
43
+ config["from_pyproject"] = ("entrypoint" in config) and (entrypoint is None)
44
+
42
45
  return cls.model_validate(config)
@@ -3,6 +3,7 @@ import sys
3
3
  from dataclasses import dataclass
4
4
  from logging import getLogger
5
5
  from pathlib import Path
6
+ from typing import Literal, TypeAlias
6
7
 
7
8
  from fastapi_cli.exceptions import FastAPICLIException
8
9
 
@@ -11,7 +12,7 @@ logger = getLogger(__name__)
11
12
  try:
12
13
  from fastapi import FastAPI
13
14
  except ImportError: # pragma: no cover
14
- FastAPI = None # type: ignore[misc, assignment]
15
+ FastAPI = None # type: ignore[misc, assignment] # ty: ignore[invalid-assignment]
15
16
 
16
17
 
17
18
  def get_default_path() -> Path:
@@ -102,18 +103,35 @@ def get_app_name(*, mod_data: ModuleData, app_name: str | None = None) -> str:
102
103
  raise FastAPICLIException("Could not find FastAPI app in module, try using --app")
103
104
 
104
105
 
106
+ ModuleConfigSource: TypeAlias = Literal[
107
+ "entrypoint-option",
108
+ "entrypoint-pyproject",
109
+ "path-argument",
110
+ "auto-discovery",
111
+ ]
112
+
113
+ AppConfigSource: TypeAlias = Literal[
114
+ "entrypoint-option", "entrypoint-pyproject", "app-option", "auto-discovery"
115
+ ]
116
+
117
+
105
118
  @dataclass
106
119
  class ImportData:
107
120
  app_name: str
108
121
  module_data: ModuleData
109
122
  import_string: str
110
123
 
124
+ module_config_source: ModuleConfigSource
125
+ app_name_config_source: AppConfigSource
126
+
111
127
 
112
128
  def get_import_data(
113
129
  *, path: Path | None = None, app_name: str | None = None
114
130
  ) -> ImportData:
131
+ path_config_source: ModuleConfigSource = "path-argument"
115
132
  if not path:
116
133
  path = get_default_path()
134
+ path_config_source = "auto-discovery"
117
135
 
118
136
  logger.debug(f"Using path [blue]{path}[/blue]")
119
137
  logger.debug(f"Resolved absolute path {path.resolve()}")
@@ -127,11 +145,17 @@ def get_import_data(
127
145
  import_string = f"{mod_data.module_import_str}:{use_app_name}"
128
146
 
129
147
  return ImportData(
130
- app_name=use_app_name, module_data=mod_data, import_string=import_string
148
+ app_name=use_app_name,
149
+ module_data=mod_data,
150
+ import_string=import_string,
151
+ module_config_source=path_config_source,
152
+ app_name_config_source="app-option" if app_name else "auto-discovery",
131
153
  )
132
154
 
133
155
 
134
- def get_import_data_from_import_string(import_string: str) -> ImportData:
156
+ def get_import_data_from_import_string(
157
+ import_string: str, from_pyproject: bool
158
+ ) -> ImportData:
135
159
  module_str, _, app_name = import_string.partition(":")
136
160
 
137
161
  if not module_str or not app_name:
@@ -151,4 +175,10 @@ def get_import_data_from_import_string(import_string: str) -> ImportData:
151
175
  module_paths=[],
152
176
  ),
153
177
  import_string=import_string,
178
+ module_config_source=(
179
+ "entrypoint-pyproject" if from_pyproject else "entrypoint-option"
180
+ ),
181
+ app_name_config_source=(
182
+ "entrypoint-pyproject" if from_pyproject else "entrypoint-option"
183
+ ),
154
184
  )
@@ -35,6 +35,10 @@ def test_dev() -> None:
35
35
  "log_config": get_uvicorn_log_config(),
36
36
  }
37
37
  assert "Using import string: single_file_app:app" in result.output
38
+ assert "Configuration sources:" in result.output
39
+ assert "Module: path CLI argument" in result.output
40
+ assert "App name: auto-discovery" in result.output
41
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
38
42
  assert "Starting development server 🚀" in result.output
39
43
  assert "Server started at http://127.0.0.1:8000" in result.output
40
44
  assert "Documentation at http://127.0.0.1:8000/docs" in result.output
@@ -59,6 +63,11 @@ def test_dev_no_args_auto_discovery() -> None:
59
63
  assert mock_run.call_args.kwargs["port"] == 8000
60
64
  assert mock_run.call_args.kwargs["reload"] is True
61
65
  assert "Using import string: main:app" in result.output
66
+ assert "Configuration sources:" in result.output
67
+ assert "Import string: auto-discovery" in result.output
68
+ assert "You can configure an entrypoint in pyproject.toml" in result.output
69
+ assert "[tool.fastapi]" in result.output
70
+ assert 'entrypoint = "main:app"' in result.output
62
71
 
63
72
 
64
73
  def test_dev_package() -> None:
@@ -81,6 +90,9 @@ def test_dev_package() -> None:
81
90
  "log_config": get_uvicorn_log_config(),
82
91
  }
83
92
  assert "Using import string: nested_package.package:app" in result.output
93
+ assert "Module: path CLI argument" in result.output
94
+ assert "App name: auto-discovery" in result.output
95
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
84
96
  assert "Starting development server 🚀" in result.output
85
97
  assert "Server started at http://127.0.0.1:8000" in result.output
86
98
  assert "Documentation at http://127.0.0.1:8000/docs" in result.output
@@ -131,6 +143,9 @@ def test_dev_args() -> None:
131
143
  "log_config": get_uvicorn_log_config(),
132
144
  }
133
145
  assert "Using import string: single_file_app:api" in result.output
146
+ assert "Module: path CLI argument" in result.output
147
+ assert "App name: --app CLI option" in result.output
148
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
134
149
  assert "Starting development server 🚀" in result.output
135
150
  assert "Server started at http://192.168.0.2:8080" in result.output
136
151
  assert "Documentation at http://192.168.0.2:8080/docs" in result.output
@@ -162,6 +177,9 @@ def test_dev_env_vars() -> None:
162
177
  "log_config": get_uvicorn_log_config(),
163
178
  }
164
179
  assert "Using import string: single_file_app:app" in result.output
180
+ assert "Module: path CLI argument" in result.output
181
+ assert "App name: auto-discovery" in result.output
182
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
165
183
  assert "Starting development server 🚀" in result.output
166
184
  assert "Server started at http://127.0.0.1:8111" in result.output
167
185
  assert "Documentation at http://127.0.0.1:8111/docs" in result.output
@@ -200,6 +218,9 @@ def test_dev_env_vars_and_args() -> None:
200
218
  "log_config": get_uvicorn_log_config(),
201
219
  }
202
220
  assert "Using import string: single_file_app:app" in result.output
221
+ assert "Module: path CLI argument" in result.output
222
+ assert "App name: auto-discovery" in result.output
223
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
203
224
  assert "Starting development server 🚀" in result.output
204
225
  assert "Server started at http://127.0.0.1:8080" in result.output
205
226
  assert "Documentation at http://127.0.0.1:8080/docs" in result.output
@@ -246,6 +267,9 @@ def test_run() -> None:
246
267
  "log_config": get_uvicorn_log_config(),
247
268
  }
248
269
  assert "Using import string: single_file_app:app" in result.output
270
+ assert "Module: path CLI argument" in result.output
271
+ assert "App name: auto-discovery" in result.output
272
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
249
273
  assert "Starting production server 🚀" in result.output
250
274
  assert "Server started at http://0.0.0.0:8000" in result.output
251
275
  assert "Documentation at http://0.0.0.0:8000/docs" in result.output
@@ -321,6 +345,9 @@ def test_run_args() -> None:
321
345
  }
322
346
 
323
347
  assert "Using import string: single_file_app:api" in result.output
348
+ assert "Module: path CLI argument" in result.output
349
+ assert "App name: --app CLI option" in result.output
350
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
324
351
  assert "Starting production server 🚀" in result.output
325
352
  assert "Server started at http://192.168.0.2:8080" in result.output
326
353
  assert "Documentation at http://192.168.0.2:8080/docs" in result.output
@@ -352,6 +379,9 @@ def test_run_env_vars() -> None:
352
379
  "log_config": get_uvicorn_log_config(),
353
380
  }
354
381
  assert "Using import string: single_file_app:app" in result.output
382
+ assert "Module: path CLI argument" in result.output
383
+ assert "App name: auto-discovery" in result.output
384
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
355
385
  assert "Starting production server 🚀" in result.output
356
386
  assert "Server started at http://0.0.0.0:8111" in result.output
357
387
  assert "Documentation at http://0.0.0.0:8111/docs" in result.output
@@ -386,6 +416,9 @@ def test_run_env_vars_and_args() -> None:
386
416
  "log_config": get_uvicorn_log_config(),
387
417
  }
388
418
  assert "Using import string: single_file_app:app" in result.output
419
+ assert "Module: path CLI argument" in result.output
420
+ assert "App name: auto-discovery" in result.output
421
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
389
422
  assert "Starting production server 🚀" in result.output
390
423
  assert "Server started at http://0.0.0.0:8080" in result.output
391
424
  assert "Documentation at http://0.0.0.0:8080/docs" in result.output
@@ -452,6 +485,7 @@ def test_version() -> None:
452
485
  result = runner.invoke(app, ["--version"])
453
486
  assert result.exit_code == 0, result.output
454
487
  assert "FastAPI CLI version:" in result.output
488
+ assert "FastAPI Cloud CLI version:" in result.output
455
489
 
456
490
 
457
491
  def test_dev_reload_dir() -> None:
@@ -498,6 +532,8 @@ def test_dev_with_import_string() -> None:
498
532
  "log_config": get_uvicorn_log_config(),
499
533
  }
500
534
  assert "Using import string: single_file_app:api" in result.output
535
+ assert "Import string: --entrypoint CLI option" in result.output
536
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
501
537
 
502
538
 
503
539
  def test_run_with_import_string() -> None:
@@ -520,6 +556,8 @@ def test_run_with_import_string() -> None:
520
556
  "log_config": get_uvicorn_log_config(),
521
557
  }
522
558
  assert "Using import string: single_file_app:app" in result.output
559
+ assert "Import string: --entrypoint CLI option" in result.output
560
+ assert "You can configure an entrypoint in pyproject.toml" not in result.output
523
561
 
524
562
 
525
563
  def test_script() -> None: