Skip to content

Commit c0fc6fa

Browse files
authored
Merge branch 'main' into feat/plugin-entry-points
2 parents 37034da + 760451b commit c0fc6fa

10 files changed

Lines changed: 247 additions & 12 deletions

File tree

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ repos:
1111
- --unsafe
1212
- id: end-of-file-fixer
1313
- id: trailing-whitespace
14+
- repo: https://github.com/crate-ci/typos
15+
rev: bbaefadf97b0ec5fdc942684b647f1a6ab250274 # v1.46.0
16+
hooks:
17+
- id: typos
18+
args: [--force-exclude]
1419
- repo: local
1520
hooks:
1621
- id: local-ruff-check

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,15 @@ keep-runtime-typing = true
161161

162162
[tool.ty.terminal]
163163
error-on-warning = true
164+
165+
[tool.typos.files]
166+
extend-exclude = [
167+
"coverage/",
168+
"dist/",
169+
"release-notes.md",
170+
"htmlcov/",
171+
"uv.lock",
172+
]
173+
174+
[tool.typos.default.extend-identifiers]
175+
alls = "alls"

release-notes.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,33 @@
22

33
## Latest Changes
44

5+
## 0.0.27 (2026-06-18)
6+
7+
### Features
8+
9+
* ✨ Add `FASTAPI_PUBLIC_URL` env var to configure app's public URL that is shown on startup. PR [#381](https://github.com/fastapi/fastapi-cli/pull/381) by [@YuriiMotov](https://github.com/YuriiMotov).
10+
11+
## 0.0.26 (2026-06-18)
12+
13+
### Features
14+
15+
* 🚸 Show tip on how to configure `entrypoint` in `pyproject.toml`. PR [#389](https://github.com/fastapi/fastapi-cli/pull/389) by [@YuriiMotov](https://github.com/YuriiMotov).
16+
17+
## 0.0.25 (2026-06-18)
18+
19+
### Features
20+
21+
* 🚸 Show import string source. PR [#388](https://github.com/fastapi/fastapi-cli/pull/388) by [@YuriiMotov](https://github.com/YuriiMotov).
22+
* ✨ Include the version of `fastapi-cloud-cli` to the output of `--version`. PR [#341](https://github.com/fastapi/fastapi-cli/pull/341) by [@YuriiMotov](https://github.com/YuriiMotov).
23+
524
### Docs
625

726
* 📝 Update docs about contributing. PR [#410](https://github.com/fastapi/fastapi-cli/pull/410) by [@tiangolo](https://github.com/tiangolo).
827
* 📝 Update security policy. PR [#408](https://github.com/fastapi/fastapi-cli/pull/408) by [@tiangolo](https://github.com/tiangolo).
928

1029
### Internal
1130

31+
* 👷 Add pre-commit hook to catch typos. PR [#418](https://github.com/fastapi/fastapi-cli/pull/418) by [@YuriiMotov](https://github.com/YuriiMotov).
1232
* ⬆ Bump typer from 0.26.2 to 0.26.7. PR [#427](https://github.com/fastapi/fastapi-cli/pull/427) by [@dependabot[bot]](https://github.com/apps/dependabot).
1333
* ⬆ Bump fastapi-cloud-cli from 0.17.1 to 0.19.0. PR [#429](https://github.com/fastapi/fastapi-cli/pull/429) by [@dependabot[bot]](https://github.com/apps/dependabot).
1434
* ⬆ Bump rich-toolkit from 0.19.9 to 0.20.0. PR [#428](https://github.com/fastapi/fastapi-cli/pull/428) by [@dependabot[bot]](https://github.com/apps/dependabot).

src/fastapi_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.24"
1+
__version__ = "0.0.27"

src/fastapi_cli/cli.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
import typer
77
from pydantic import ValidationError
88
from rich import print
9+
from rich.syntax import Syntax
910
from rich.tree import Tree
1011
from typer.models import CommandInfo
1112

1213
from fastapi_cli.config import FastAPIConfig
13-
from fastapi_cli.discover import get_import_data, get_import_data_from_import_string
14+
from fastapi_cli.discover import (
15+
AppConfigSource,
16+
ModuleConfigSource,
17+
get_import_data,
18+
get_import_data_from_import_string,
19+
)
1420
from fastapi_cli.exceptions import FastAPICLIException
1521

1622
from . import __version__
@@ -22,6 +28,15 @@
2228
logger = logging.getLogger(__name__)
2329

2430

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+
2540
try:
2641
import uvicorn
2742
except ImportError: # pragma: no cover
@@ -101,6 +116,15 @@ def _load_cli_plugins(typer_app: typer.Typer) -> None:
101116
def version_callback(value: bool) -> None:
102117
if value:
103118
print(f"FastAPI CLI version: [green]{__version__}[/green]")
119+
try:
120+
from fastapi_cloud_cli import (
121+
__version__ as cloud_cli_version,
122+
)
123+
124+
print(f"FastAPI Cloud CLI version: [green]{cloud_cli_version}[/green]")
125+
except ImportError: # pragma: no cover
126+
pass
127+
104128
raise typer.Exit()
105129

106130

@@ -158,6 +182,7 @@ def _run(
158182
entrypoint: str | None = None,
159183
proxy_headers: bool = False,
160184
forwarded_allow_ips: str | None = None,
185+
public_url: str | None = None,
161186
) -> None:
162187
with get_rich_toolkit() as toolkit:
163188
server_type = "development" if command == "dev" else "production"
@@ -195,7 +220,9 @@ def _run(
195220
if path or app:
196221
import_data = get_import_data(path=path, app_name=app)
197222
elif config.entrypoint:
198-
import_data = get_import_data_from_import_string(config.entrypoint)
223+
import_data = get_import_data_from_import_string(
224+
config.entrypoint, config.from_pyproject
225+
)
199226
else:
200227
import_data = get_import_data()
201228
except FastAPICLIException as e:
@@ -233,7 +260,35 @@ def _run(
233260
tag="app",
234261
)
235262

236-
url = f"http://{host}:{port}"
263+
mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
264+
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
265+
toolkit.print_line()
266+
toolkit.print("Configuration sources:", tag="info")
267+
if mod_source_desc == app_source_desc:
268+
toolkit.print(f" • Import string: {mod_source_desc}")
269+
else:
270+
toolkit.print(f" • Module: {mod_source_desc}")
271+
toolkit.print(f" • App name: {app_source_desc}")
272+
273+
if import_data.module_config_source == "auto-discovery":
274+
toolkit.print_line()
275+
toolkit.print(
276+
"You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
277+
tag="tip",
278+
)
279+
toolkit.print_line()
280+
toolkit.print(
281+
Syntax(
282+
(
283+
"[tool.fastapi]\n"
284+
f'entrypoint = "{import_data.module_data.module_import_str}:{import_data.app_name}"'
285+
),
286+
"toml",
287+
theme="ansi_light",
288+
)
289+
)
290+
291+
url = public_url.rstrip("/") if public_url else f"http://{host}:{port}"
237292
url_docs = f"{url}/docs"
238293

239294
toolkit.print_line()
@@ -379,6 +434,7 @@ def dev(
379434
command="dev",
380435
proxy_headers=proxy_headers,
381436
forwarded_allow_ips=forwarded_allow_ips,
437+
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
382438
)
383439

384440

@@ -486,6 +542,7 @@ def run(
486542
command="run",
487543
proxy_headers=proxy_headers,
488544
forwarded_allow_ips=forwarded_allow_ips,
545+
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
489546
)
490547

491548

src/fastapi_cli/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
class FastAPIConfig(BaseModel):
1111
entrypoint: StrictStr | None = None
12+
from_pyproject: bool = False
1213

1314
@classmethod
1415
def _read_pyproject_toml(cls) -> dict[str, Any]:
@@ -39,4 +40,6 @@ def resolve(cls, entrypoint: str | None = None) -> "FastAPIConfig":
3940
if entrypoint is not None:
4041
config["entrypoint"] = entrypoint
4142

43+
config["from_pyproject"] = ("entrypoint" in config) and (entrypoint is None)
44+
4245
return cls.model_validate(config)

src/fastapi_cli/discover.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from dataclasses import dataclass
44
from logging import getLogger
55
from pathlib import Path
6+
from typing import Literal, TypeAlias
67

78
from fastapi_cli.exceptions import FastAPICLIException
89

@@ -102,18 +103,35 @@ def get_app_name(*, mod_data: ModuleData, app_name: str | None = None) -> str:
102103
raise FastAPICLIException("Could not find FastAPI app in module, try using --app")
103104

104105

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+
105118
@dataclass
106119
class ImportData:
107120
app_name: str
108121
module_data: ModuleData
109122
import_string: str
110123

124+
module_config_source: ModuleConfigSource
125+
app_name_config_source: AppConfigSource
126+
111127

112128
def get_import_data(
113129
*, path: Path | None = None, app_name: str | None = None
114130
) -> ImportData:
131+
path_config_source: ModuleConfigSource = "path-argument"
115132
if not path:
116133
path = get_default_path()
134+
path_config_source = "auto-discovery"
117135

118136
logger.debug(f"Using path [blue]{path}[/blue]")
119137
logger.debug(f"Resolved absolute path {path.resolve()}")
@@ -127,11 +145,17 @@ def get_import_data(
127145
import_string = f"{mod_data.module_import_str}:{use_app_name}"
128146

129147
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",
131153
)
132154

133155

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:
135159
module_str, _, app_name = import_string.partition(":")
136160

137161
if not module_str or not app_name:
@@ -151,4 +175,10 @@ def get_import_data_from_import_string(import_string: str) -> ImportData:
151175
module_paths=[],
152176
),
153177
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+
),
154184
)

0 commit comments

Comments
 (0)