fastapi-cli 0.0.23__tar.gz → 0.0.25__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.23 → fastapi_cli-0.0.25}/PKG-INFO +1 -1
  2. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/pyproject.toml +20 -24
  3. fastapi_cli-0.0.25/scripts/add_latest_release_date.py +40 -0
  4. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/scripts/lint.sh +1 -0
  5. fastapi_cli-0.0.25/scripts/prepare_release.py +216 -0
  6. fastapi_cli-0.0.25/src/fastapi_cli/__init__.py +1 -0
  7. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/cli.py +39 -4
  8. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/config.py +5 -2
  9. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/discover.py +33 -3
  10. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_cli.py +24 -0
  11. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_cli_pyproject.py +7 -0
  12. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_discover.py +20 -6
  13. fastapi_cli-0.0.25/tests/test_prepare_release.py +296 -0
  14. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_requirements.py +3 -3
  15. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/utils.py +1 -2
  16. fastapi_cli-0.0.23/pdm_build.py +0 -40
  17. fastapi_cli-0.0.23/src/fastapi_cli/__init__.py +0 -1
  18. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/LICENSE +0 -0
  19. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/README.md +0 -0
  20. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/scripts/format.sh +0 -0
  21. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/scripts/test-cov-html.sh +0 -0
  22. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/scripts/test.sh +0 -0
  23. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/__main__.py +0 -0
  24. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/exceptions.py +0 -0
  25. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/logging.py +0 -0
  26. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/py.typed +0 -0
  27. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/utils/__init__.py +0 -0
  28. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/src/fastapi_cli/utils/cli.py +0 -0
  29. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/__init__.py +0 -0
  30. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/broken_package/mod/__init__.py +0 -0
  31. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/broken_package/mod/app.py +0 -0
  32. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/broken_package/utils.py +0 -0
  33. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_api/api.py +0 -0
  34. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app/api.py +0 -0
  35. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app/app.py +0 -0
  36. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  37. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  38. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  39. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  40. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  41. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  42. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  43. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  44. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  45. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  46. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  47. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_main/api.py +0 -0
  48. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_main/app.py +0 -0
  49. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/default_main/main.py +0 -0
  50. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  51. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/__init__.py +0 -0
  52. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/__init__.py +0 -0
  53. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/core/__init__.py +0 -0
  54. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/core/utils.py +0 -0
  55. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/mod/__init__.py +0 -0
  56. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/mod/api.py +0 -0
  57. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/mod/app.py +0 -0
  58. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/nested_package/package/mod/other.py +0 -0
  59. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/__init__.py +0 -0
  60. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/core/__init__.py +0 -0
  61. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/core/utils.py +0 -0
  62. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/mod/__init__.py +0 -0
  63. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/mod/api.py +0 -0
  64. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/mod/app.py +0 -0
  65. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/package/mod/other.py +0 -0
  66. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/pyproject_config/another_module.py +0 -0
  67. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/pyproject_config/my_module.py +0 -0
  68. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/pyproject_config/pyproject.toml +0 -0
  69. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/single_file_api.py +0 -0
  70. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/single_file_app.py +0 -0
  71. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/assets/single_file_other.py +0 -0
  72. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/conftest.py +0 -0
  73. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_utils_cli.py +0 -0
  74. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_utils_default_dir.py +0 -0
  75. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_utils_default_file.py +0 -0
  76. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/tests/test_utils_package.py +0 -0
  77. {fastapi_cli-0.0.23 → fastapi_cli-0.0.25}/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.23
3
+ Version: 0.0.25
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.23"
41
+ version = "0.0.25"
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.1",
76
+ "ruff>=0.15.15",
76
77
  "uvicorn>=0.39.0",
78
+ "ty>=0.0.25",
77
79
  ]
78
80
 
79
81
  [build-system]
@@ -95,27 +97,6 @@ source-includes = [
95
97
  "scripts/",
96
98
  ]
97
99
 
98
- [tool.tiangolo._internal-slim-build.packages.fastapi-cli-slim.project]
99
- name = "fastapi-cli-slim"
100
- readme = "fastapi-cli-slim/README.md"
101
- dependencies = [
102
- "fastapi-cli",
103
- ]
104
-
105
- [tool.tiangolo._internal-slim-build.packages.fastapi-cli-slim.project.optional-dependencies]
106
-
107
- [tool.tiangolo._internal-slim-build.packages.fastapi-cli-slim.project.scripts]
108
-
109
- [tool.tiangolo._internal-slim-build.packages.fastapi-cli-slim.tool.pdm.build]
110
- excludes = [
111
- "src",
112
- "tests",
113
- "pdm_build.py",
114
- ]
115
- source-includes = [
116
- "",
117
- ]
118
-
119
100
  [tool.pytest.ini_options]
120
101
  addopts = [
121
102
  "--strict-config",
@@ -182,3 +163,18 @@ known-third-party = [
182
163
 
183
164
  [tool.ruff.lint.pyupgrade]
184
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.25"
@@ -8,7 +8,12 @@ from rich import print
8
8
  from rich.tree import Tree
9
9
 
10
10
  from fastapi_cli.config import FastAPIConfig
11
- from fastapi_cli.discover import get_import_data, get_import_data_from_import_string
11
+ from fastapi_cli.discover import (
12
+ AppConfigSource,
13
+ ModuleConfigSource,
14
+ get_import_data,
15
+ get_import_data_from_import_string,
16
+ )
12
17
  from fastapi_cli.exceptions import FastAPICLIException
13
18
 
14
19
  from . import __version__
@@ -22,10 +27,19 @@ app = typer.Typer(
22
27
  logger = logging.getLogger(__name__)
23
28
 
24
29
 
30
+ SOURCE_DESCRIPTIONS: dict[ModuleConfigSource | AppConfigSource, str] = {
31
+ "entrypoint-option": "[blue]--entrypoint[/] CLI option",
32
+ "entrypoint-pyproject": "[blue]entrypoint[/] in [blue]pyproject.toml[/]",
33
+ "path-argument": "[blue]path[/] CLI argument",
34
+ "app-option": "[blue]--app[/] CLI option",
35
+ "auto-discovery": "auto-discovery",
36
+ }
37
+
38
+
25
39
  try:
26
40
  import uvicorn
27
41
  except ImportError: # pragma: no cover
28
- uvicorn = None # type: ignore[assignment]
42
+ uvicorn = None # type: ignore[assignment] # ty: ignore[invalid-assignment]
29
43
 
30
44
 
31
45
  try:
@@ -39,7 +53,7 @@ except ImportError: # pragma: no cover
39
53
 
40
54
 
41
55
  try:
42
- from fastapi_new.cli import ( # type: ignore[import-not-found]
56
+ from fastapi_new.cli import ( # type: ignore[import-not-found] # ty: ignore[unresolved-import]
43
57
  app as fastapi_new_cli,
44
58
  )
45
59
 
@@ -51,6 +65,15 @@ except ImportError: # pragma: no cover
51
65
  def version_callback(value: bool) -> None:
52
66
  if value:
53
67
  print(f"FastAPI CLI version: [green]{__version__}[/green]")
68
+ try:
69
+ from fastapi_cloud_cli import (
70
+ __version__ as cloud_cli_version,
71
+ )
72
+
73
+ print(f"FastAPI Cloud CLI version: [green]{cloud_cli_version}[/green]")
74
+ except ImportError: # pragma: no cover
75
+ pass
76
+
54
77
  raise typer.Exit()
55
78
 
56
79
 
@@ -151,7 +174,9 @@ def _run(
151
174
  if path or app:
152
175
  import_data = get_import_data(path=path, app_name=app)
153
176
  elif config.entrypoint:
154
- import_data = get_import_data_from_import_string(config.entrypoint)
177
+ import_data = get_import_data_from_import_string(
178
+ config.entrypoint, config.from_pyproject
179
+ )
155
180
  else:
156
181
  import_data = get_import_data()
157
182
  except FastAPICLIException as e:
@@ -189,6 +214,16 @@ def _run(
189
214
  tag="app",
190
215
  )
191
216
 
217
+ mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
218
+ app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
219
+ toolkit.print_line()
220
+ toolkit.print("Configuration sources:", tag="info")
221
+ if mod_source_desc == app_source_desc:
222
+ toolkit.print(f" • Import string: {mod_source_desc}")
223
+ else:
224
+ toolkit.print(f" • Module: {mod_source_desc}")
225
+ toolkit.print(f" • App name: {app_source_desc}")
226
+
192
227
  url = f"http://{host}:{port}"
193
228
  url_docs = f"{url}/docs"
194
229
 
@@ -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,9 @@ 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
38
41
  assert "Starting development server 🚀" in result.output
39
42
  assert "Server started at http://127.0.0.1:8000" in result.output
40
43
  assert "Documentation at http://127.0.0.1:8000/docs" in result.output
@@ -59,6 +62,8 @@ def test_dev_no_args_auto_discovery() -> None:
59
62
  assert mock_run.call_args.kwargs["port"] == 8000
60
63
  assert mock_run.call_args.kwargs["reload"] is True
61
64
  assert "Using import string: main:app" in result.output
65
+ assert "Configuration sources:" in result.output
66
+ assert "Import string: auto-discovery" in result.output
62
67
 
63
68
 
64
69
  def test_dev_package() -> None:
@@ -81,6 +86,8 @@ def test_dev_package() -> None:
81
86
  "log_config": get_uvicorn_log_config(),
82
87
  }
83
88
  assert "Using import string: nested_package.package:app" in result.output
89
+ assert "Module: path CLI argument" in result.output
90
+ assert "App name: auto-discovery" in result.output
84
91
  assert "Starting development server 🚀" in result.output
85
92
  assert "Server started at http://127.0.0.1:8000" in result.output
86
93
  assert "Documentation at http://127.0.0.1:8000/docs" in result.output
@@ -131,6 +138,8 @@ def test_dev_args() -> None:
131
138
  "log_config": get_uvicorn_log_config(),
132
139
  }
133
140
  assert "Using import string: single_file_app:api" in result.output
141
+ assert "Module: path CLI argument" in result.output
142
+ assert "App name: --app CLI option" in result.output
134
143
  assert "Starting development server 🚀" in result.output
135
144
  assert "Server started at http://192.168.0.2:8080" in result.output
136
145
  assert "Documentation at http://192.168.0.2:8080/docs" in result.output
@@ -162,6 +171,8 @@ def test_dev_env_vars() -> None:
162
171
  "log_config": get_uvicorn_log_config(),
163
172
  }
164
173
  assert "Using import string: single_file_app:app" in result.output
174
+ assert "Module: path CLI argument" in result.output
175
+ assert "App name: auto-discovery" in result.output
165
176
  assert "Starting development server 🚀" in result.output
166
177
  assert "Server started at http://127.0.0.1:8111" in result.output
167
178
  assert "Documentation at http://127.0.0.1:8111/docs" in result.output
@@ -200,6 +211,8 @@ def test_dev_env_vars_and_args() -> None:
200
211
  "log_config": get_uvicorn_log_config(),
201
212
  }
202
213
  assert "Using import string: single_file_app:app" in result.output
214
+ assert "Module: path CLI argument" in result.output
215
+ assert "App name: auto-discovery" in result.output
203
216
  assert "Starting development server 🚀" in result.output
204
217
  assert "Server started at http://127.0.0.1:8080" in result.output
205
218
  assert "Documentation at http://127.0.0.1:8080/docs" in result.output
@@ -246,6 +259,8 @@ def test_run() -> None:
246
259
  "log_config": get_uvicorn_log_config(),
247
260
  }
248
261
  assert "Using import string: single_file_app:app" in result.output
262
+ assert "Module: path CLI argument" in result.output
263
+ assert "App name: auto-discovery" in result.output
249
264
  assert "Starting production server 🚀" in result.output
250
265
  assert "Server started at http://0.0.0.0:8000" in result.output
251
266
  assert "Documentation at http://0.0.0.0:8000/docs" in result.output
@@ -321,6 +336,8 @@ def test_run_args() -> None:
321
336
  }
322
337
 
323
338
  assert "Using import string: single_file_app:api" in result.output
339
+ assert "Module: path CLI argument" in result.output
340
+ assert "App name: --app CLI option" in result.output
324
341
  assert "Starting production server 🚀" in result.output
325
342
  assert "Server started at http://192.168.0.2:8080" in result.output
326
343
  assert "Documentation at http://192.168.0.2:8080/docs" in result.output
@@ -352,6 +369,8 @@ def test_run_env_vars() -> None:
352
369
  "log_config": get_uvicorn_log_config(),
353
370
  }
354
371
  assert "Using import string: single_file_app:app" in result.output
372
+ assert "Module: path CLI argument" in result.output
373
+ assert "App name: auto-discovery" in result.output
355
374
  assert "Starting production server 🚀" in result.output
356
375
  assert "Server started at http://0.0.0.0:8111" in result.output
357
376
  assert "Documentation at http://0.0.0.0:8111/docs" in result.output
@@ -386,6 +405,8 @@ def test_run_env_vars_and_args() -> None:
386
405
  "log_config": get_uvicorn_log_config(),
387
406
  }
388
407
  assert "Using import string: single_file_app:app" in result.output
408
+ assert "Module: path CLI argument" in result.output
409
+ assert "App name: auto-discovery" in result.output
389
410
  assert "Starting production server 🚀" in result.output
390
411
  assert "Server started at http://0.0.0.0:8080" in result.output
391
412
  assert "Documentation at http://0.0.0.0:8080/docs" in result.output
@@ -452,6 +473,7 @@ def test_version() -> None:
452
473
  result = runner.invoke(app, ["--version"])
453
474
  assert result.exit_code == 0, result.output
454
475
  assert "FastAPI CLI version:" in result.output
476
+ assert "FastAPI Cloud CLI version:" in result.output
455
477
 
456
478
 
457
479
  def test_dev_reload_dir() -> None:
@@ -498,6 +520,7 @@ def test_dev_with_import_string() -> None:
498
520
  "log_config": get_uvicorn_log_config(),
499
521
  }
500
522
  assert "Using import string: single_file_app:api" in result.output
523
+ assert "Import string: --entrypoint CLI option" in result.output
501
524
 
502
525
 
503
526
  def test_run_with_import_string() -> None:
@@ -520,6 +543,7 @@ def test_run_with_import_string() -> None:
520
543
  "log_config": get_uvicorn_log_config(),
521
544
  }
522
545
  assert "Using import string: single_file_app:app" in result.output
546
+ assert "Import string: --entrypoint CLI option" in result.output
523
547
 
524
548
 
525
549
  def test_script() -> None: