Skip to content

Commit e93b815

Browse files
committed
Add public-url option and env var
1 parent f3494ff commit e93b815

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

src/fastapi_cli/cli.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def _run(
112112
entrypoint: str | None = None,
113113
proxy_headers: bool = False,
114114
forwarded_allow_ips: str | None = None,
115+
public_url: str | None = None,
115116
) -> None:
116117
with get_rich_toolkit() as toolkit:
117118
server_type = "development" if command == "dev" else "production"
@@ -189,7 +190,7 @@ def _run(
189190
tag="app",
190191
)
191192

192-
url = f"http://{host}:{port}"
193+
url = public_url.rstrip("/") if public_url else f"http://{host}:{port}"
193194
url_docs = f"{url}/docs"
194195

195196
toolkit.print_line()
@@ -299,6 +300,13 @@ def dev(
299300
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
300301
),
301302
] = None,
303+
public_url: Annotated[
304+
str | None,
305+
typer.Option(
306+
help="The public URL where the server is accessible. Used for the URLs printed at startup. Defaults to [blue]http://{host}:{port}[/blue].",
307+
envvar="FASTAPI_PUBLIC_URL",
308+
),
309+
] = None,
302310
) -> Any:
303311
"""
304312
Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
@@ -337,6 +345,7 @@ def dev(
337345
command="dev",
338346
proxy_headers=proxy_headers,
339347
forwarded_allow_ips=forwarded_allow_ips,
348+
public_url=public_url,
340349
)
341350

342351

@@ -406,6 +415,13 @@ def run(
406415
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
407416
),
408417
] = None,
418+
public_url: Annotated[
419+
str | None,
420+
typer.Option(
421+
help="The public URL where the server is accessible. Used for the URLs printed at startup. Defaults to [blue]http://{host}:{port}[/blue].",
422+
envvar="FASTAPI_PUBLIC_URL",
423+
),
424+
] = None,
409425
) -> Any:
410426
"""
411427
Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
@@ -444,6 +460,7 @@ def run(
444460
command="run",
445461
proxy_headers=proxy_headers,
446462
forwarded_allow_ips=forwarded_allow_ips,
463+
public_url=public_url,
447464
)
448465

449466

tests/test_cli.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44
from unittest.mock import patch
55

6+
import pytest
67
import uvicorn
78
from typer.testing import CliRunner
89

@@ -391,6 +392,91 @@ def test_run_env_vars_and_args() -> None:
391392
assert "Documentation at http://0.0.0.0:8080/docs" in result.output
392393

393394

395+
@pytest.mark.parametrize("command", ["dev", "run"])
396+
@pytest.mark.parametrize(
397+
"public_url", ["https://myapp.example.com", "https://myapp.example.com/"]
398+
)
399+
def test_public_url(command: str, public_url: str) -> None:
400+
with changing_dir(assets_path):
401+
with patch.object(uvicorn, "run") as mock_run:
402+
result = runner.invoke(
403+
app,
404+
[
405+
command,
406+
"single_file_app.py",
407+
"--host",
408+
"0.0.0.0",
409+
"--public-url",
410+
public_url,
411+
],
412+
)
413+
assert result.exit_code == 0, result.output
414+
assert mock_run.called
415+
assert mock_run.call_args
416+
assert mock_run.call_args.kwargs == {
417+
"app": "single_file_app:app",
418+
"host": "0.0.0.0",
419+
"port": 8000,
420+
"reload": True if command == "dev" else False,
421+
"reload_dirs": None,
422+
"workers": None,
423+
"root_path": "",
424+
"proxy_headers": True,
425+
"forwarded_allow_ips": None,
426+
"log_config": get_uvicorn_log_config(),
427+
}
428+
429+
assert "Using import string: single_file_app:app" in result.output
430+
assert (
431+
f"Starting {'development' if command == 'dev' else 'production'} server 🚀"
432+
in result.output
433+
)
434+
assert "Server started at https://myapp.example.com" in result.output
435+
assert "Documentation at https://myapp.example.com/docs" in result.output
436+
437+
438+
@pytest.mark.parametrize("command", ["dev", "run"])
439+
@pytest.mark.parametrize(
440+
"public_url", ["https://myapp.example.com", "https://myapp.example.com/"]
441+
)
442+
def test_public_url_env_var(command: str, public_url: str) -> None:
443+
with changing_dir(assets_path):
444+
with patch.object(uvicorn, "run") as mock_run:
445+
result = runner.invoke(
446+
app,
447+
[
448+
command,
449+
"single_file_app.py",
450+
"--host",
451+
"0.0.0.0",
452+
],
453+
env={"FASTAPI_PUBLIC_URL": public_url},
454+
)
455+
assert result.exit_code == 0, result.output
456+
assert mock_run.called
457+
assert mock_run.call_args
458+
assert mock_run.call_args.kwargs == {
459+
"app": "single_file_app:app",
460+
"host": "0.0.0.0",
461+
"port": 8000,
462+
"reload": True if command == "dev" else False,
463+
"reload_dirs": None,
464+
"workers": None,
465+
"root_path": "",
466+
"proxy_headers": True,
467+
"forwarded_allow_ips": None,
468+
"log_config": get_uvicorn_log_config(),
469+
}
470+
471+
assert "Using import string: single_file_app:app" in result.output
472+
assert (
473+
f"Starting {'development' if command == 'dev' else 'production'} server 🚀"
474+
in result.output
475+
)
476+
assert "Server started at https://myapp.example.com" in result.output
477+
assert "Documentation at https://myapp.example.com/docs" in result.output
478+
479+
394480
def test_run_error() -> None:
395481
with changing_dir(assets_path):
396482
result = runner.invoke(app, ["run", "non_existing_file.py"])
@@ -417,6 +503,7 @@ def test_dev_help() -> None:
417503
assert "Set reload directories explicitly" in result.output
418504
assert "The root path is used to tell your app" in result.output
419505
assert "The name of the variable that contains the FastAPI app" in result.output
506+
assert "The public URL where the server is accessible" in result.output
420507
assert "Use multiple worker processes." not in result.output
421508

422509

@@ -438,6 +525,7 @@ def test_run_help() -> None:
438525
assert "Enable auto-reload of the server when (code) files change." in result.output
439526
assert "The root path is used to tell your app" in result.output
440527
assert "The name of the variable that contains the FastAPI app" in result.output
528+
assert "The public URL where the server is accessible" in result.output
441529
assert "Use multiple worker processes." in result.output
442530

443531

0 commit comments

Comments
 (0)