Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/fastapi-app/config/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

@dataclasses.dataclass
class FastAPIConfig:
app_url: str = dataclasses.field(default_factory=lambda: env("APP_URL", "http://localhost"))
host: str = dataclasses.field(default_factory=lambda: env("APP_HOST", "127.0.0.1"))
port: int = dataclasses.field(default_factory=lambda: env("APP_PORT", 8000))
reload: bool = dataclasses.field(default_factory=lambda: env("APP_RELOAD", True))
Expand Down
9 changes: 7 additions & 2 deletions example/fastapi-app/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from cleo.helpers import option

from fastapi_startkit import Config
from fastapi_startkit.console.command import Command
from fastapi_startkit.environment import value as cast_value
from fastapi_startkit.support import Uriable, Uri
from fastapi_startkit.support import Uri, Uriable


class ServeCommand(Command):
Expand Down Expand Up @@ -46,10 +47,15 @@ def resolve_option(self, key: str, default: str | int | None = None):
return cast_value(value)

def resolve_url(self) -> Uriable:
host = self.resolve_option("host", "127.0.0.1")
port = self.resolve_option("port", 8000)
host = self.option("host") or Config.get("fastapi.app_url", "http://127.0.0.1:8000")
port = self.option("port")

if host and not host.startswith("http"):
host = f"http://{host}"

uri = Uri.of(host)

return Uri.of(Config.get("fastapi.app_url", "http://127.0.0.1:8000")).with_host(host).with_port(port)
return uri.with_port(port) if port else uri

def handle(self):
import uvicorn
Expand All @@ -62,11 +68,12 @@ def handle(self):
cfg_reload_excludes = Config.get("fastapi.reload_excludes") or None

url = self.resolve_url()
reload = self.resolve_option("reload", True)

kwargs = {
"host": url.host(),
"port": url.port(),
"reload": self.resolve_option("reload", True),
"reload": reload,
"ws": "websockets-sansio",
}

Expand All @@ -77,9 +84,9 @@ def handle(self):
"factory": True,
}
)
if cfg_reload_dirs is not None:
if cfg_reload_dirs is not None and reload:
kwargs["reload_dirs"] = cfg_reload_dirs
if cfg_reload_excludes is not None:
if cfg_reload_excludes is not None and reload:
kwargs["reload_excludes"] = cfg_reload_excludes

self.line(f"<info>Starting Uvicorn server on {url.host()}:{url.port()} [{self.option('app')}]...</info>")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@

@dataclasses.dataclass
class FastAPIConfig:
"""Server configuration for the uvicorn/FastAPI serve command.

All fields can be overridden via environment variables or by publishing a
``config/fastapi.py`` file in the application root.
"""

host: str = dataclasses.field(default_factory=lambda: env("APP_HOST", "127.0.0.1"))
port: int = dataclasses.field(default_factory=lambda: env("APP_PORT", 8000))
app_url: str = dataclasses.field(default_factory=lambda: env("APP_URL", "http://127.0.0.1:8000"))
reload: bool = dataclasses.field(default_factory=lambda: env("APP_RELOAD", True))
reload_dirs: list | None = None
reload_excludes: list = dataclasses.field(
Expand Down
14 changes: 7 additions & 7 deletions fastapi_startkit/tests/fastapi/test_serve_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,17 @@ def test_config_port_used_when_no_cli_flag(self):
tester, _ = run(config={"fastapi.port": 7777, "fastapi.app_url": "http://127.0.0.1:7777"})
assert "7777" in tester.io.fetch_output()

def test_app_url_host_overridden_by_config_default(self):
"""resolve_url() merges APP_URL with fastapi.host/port.
def test_app_url_is_single_source_of_truth(self):
"""resolve_url() uses fastapi.app_url as the single source of truth.

APP_URL sets the base, but fastapi.host and fastapi.port (which default
to 127.0.0.1 / 8000) take priority in the current implementation.
When no explicit host/port is in config, defaults win over APP_URL.
When fastapi.app_url is set in config and no CLI --host/--port flags are
provided, the host and port are derived directly from the configured URL.
"""
tester, _ = run(config={"fastapi.app_url": "http://staging.example.com:5000"})
output = tester.io.fetch_output()
# Default host wins because resolve_option("host", "127.0.0.1") takes over.
assert _DEFAULT_HOST in output
# app_url is the source of truth — staging.example.com:5000 is used directly.
assert "staging.example.com" in output
assert "5000" in output


# ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion fastapi_startkit/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading