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.
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/PKG-INFO +1 -1
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/pyproject.toml +20 -3
- fastapi_cli-0.0.26/scripts/add_latest_release_date.py +40 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/lint.sh +1 -0
- fastapi_cli-0.0.26/scripts/prepare_release.py +216 -0
- fastapi_cli-0.0.26/src/fastapi_cli/__init__.py +1 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/cli.py +58 -4
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/config.py +5 -2
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/discover.py +33 -3
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_cli.py +38 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_cli_pyproject.py +7 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_discover.py +20 -6
- fastapi_cli-0.0.26/tests/test_prepare_release.py +296 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_requirements.py +3 -3
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/utils.py +1 -2
- fastapi_cli-0.0.24/src/fastapi_cli/__init__.py +0 -1
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/LICENSE +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/README.md +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/format.sh +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/test-cov-html.sh +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/scripts/test.sh +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/__main__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/exceptions.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/logging.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/py.typed +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/utils/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/src/fastapi_cli/utils/cli.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/core/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/core/utils.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/nested_package/package/mod/other.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/pyproject_config/another_module.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/pyproject_config/my_module.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/pyproject_config/pyproject.toml +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/single_file_api.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/single_file_app.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/assets/single_file_other.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/conftest.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_cli.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_default_dir.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_default_file.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_package.py +0 -0
- {fastapi_cli-0.0.24 → fastapi_cli-0.0.26}/tests/test_utils_single_file.py +0 -0
|
@@ -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.
|
|
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.
|
|
74
|
+
"mypy==2.1.0",
|
|
74
75
|
"pytest>=7.4.0,<10.0.0",
|
|
75
|
-
"ruff
|
|
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()
|
|
@@ -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
|
|
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(
|
|
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,
|
|
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(
|
|
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:
|