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