diff --git a/bin/test.sh b/bin/test.sh index 7cfbdece..c2286ba2 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -5,10 +5,11 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # ── Start MySQL via Docker Compose ──────────────────────────────────────────── echo "Starting test services..." +docker compose -f "$ROOT/docker-compose.yml" down --remove-orphans docker compose -f "$ROOT/docker-compose.yml" up -d --wait # Ensure MySQL is torn down on exit (even if tests fail) -trap 'echo "Stopping test services..."; docker compose -f "$ROOT/docker-compose.test.yml" down' EXIT +trap 'echo "Stopping test services..."; docker compose -f "$ROOT/docker-compose.yml" down' EXIT # ── Common DB env vars (match docker-compose.test.yml) ─────────────────────── export DB_HOST=127.0.0.1 diff --git a/example/config-app/uv.lock b/example/config-app/uv.lock index e2c55a3d..b5ded1c8 100644 --- a/example/config-app/uv.lock +++ b/example/config-app/uv.lock @@ -162,7 +162,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.21.0" +version = "0.23.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, diff --git a/example/database-app/uv.lock b/example/database-app/uv.lock index 5a426a93..79b46c58 100644 --- a/example/database-app/uv.lock +++ b/example/database-app/uv.lock @@ -498,7 +498,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.21.0" +version = "0.23.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, diff --git a/example/fastapi-app/bootstrap/application.py b/example/fastapi-app/bootstrap/application.py index 3d4555ba..deda77d9 100644 --- a/example/fastapi-app/bootstrap/application.py +++ b/example/fastapi-app/bootstrap/application.py @@ -3,11 +3,12 @@ from fastapi_startkit import Application from fastapi_startkit.logging import LogProvider from providers.fastapi_provider import FastAPIProvider +from config.fastapi import FastAPIConfig app: Application = Application( base_path=str(Path.cwd()), # This always gives path relative to the execution. providers=[ LogProvider, - FastAPIProvider, + (FastAPIProvider, FastAPIConfig), ] ) diff --git a/example/fastapi-app/config/fastapi.py b/example/fastapi-app/config/fastapi.py new file mode 100644 index 00000000..ea734791 --- /dev/null +++ b/example/fastapi-app/config/fastapi.py @@ -0,0 +1,18 @@ +import dataclasses + +from fastapi_startkit.environment import env + + +@dataclasses.dataclass +class FastAPIConfig: + 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)) + reload_dirs: list | None = None + reload_excludes: list = dataclasses.field( + default_factory=lambda: [ + "*.log", + "tests/*", + "node_modules/*", + ] + ) diff --git a/example/fastapi-app/pyproject.toml b/example/fastapi-app/pyproject.toml index ba316274..5334f80d 100644 --- a/example/fastapi-app/pyproject.toml +++ b/example/fastapi-app/pyproject.toml @@ -10,3 +10,8 @@ dependencies = [ [tool.uv.sources] fastapi-startkit = { path = "../../fastapi_startkit", editable = true } + +[dependency-groups] +dev = [ + "dumpdie>=1.5.0", +] diff --git a/example/fastapi-app/routes/api.py b/example/fastapi-app/routes/api.py index 84716814..149ed599 100644 --- a/example/fastapi-app/routes/api.py +++ b/example/fastapi-app/routes/api.py @@ -9,7 +9,7 @@ async def index(): Logger.info("Welcome to FastAPI StartKit!") Logger.info("Version: 1.0.0") Logger.info("Docs: /docs") - Logger.log("debug", "Debugging the application.") + Logger.log("debug", "Debugging the application!") return { "message": "Welcome to FastAPI StartKit!", "version": "1.0.0", diff --git a/example/fastapi-app/uv.lock b/example/fastapi-app/uv.lock index 99d33bc5..4565ce5f 100644 --- a/example/fastapi-app/uv.lock +++ b/example/fastapi-app/uv.lock @@ -192,6 +192,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, ] +[[package]] +name = "dumpdie" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/61/7ea2d95d55806d9e812e6a38880a479adb40a874c95c745a3a896e844998/dumpdie-1.5.0.tar.gz", hash = "sha256:0267fc9409a10fb0c3e023ba95b109df2108b72f5fb7f98e3cc68a3f825a3d46", size = 88462, upload-time = "2026-03-19T00:39:54.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/40/e7937be0300078626ff929e1d7856ebe3237aa03ad338d0c077c6cda301d/dumpdie-1.5.0-py3-none-any.whl", hash = "sha256:df487693dacfbe38fb2a3229e4734479e046c2485cb7ba723f3e7e5012da1087", size = 3369, upload-time = "2026-03-19T00:39:53.436Z" }, +] + [[package]] name = "email-validator" version = "2.3.0" @@ -238,9 +247,17 @@ dependencies = [ { name = "fastapi-startkit", extra = ["fastapi"] }, ] +[package.dev-dependencies] +dev = [ + { name = "dumpdie" }, +] + [package.metadata] requires-dist = [{ name = "fastapi-startkit", extras = ["fastapi"], editable = "../../fastapi_startkit" }] +[package.metadata.requires-dev] +dev = [{ name = "dumpdie", specifier = ">=1.5.0" }] + [[package]] name = "fastapi-cli" version = "0.0.24" @@ -282,7 +299,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.13.6" +version = "0.22.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -297,6 +314,7 @@ dependencies = [ [package.optional-dependencies] fastapi = [ { name = "fastapi", extra = ["standard"] }, + { name = "itsdangerous" }, ] [package.metadata] @@ -310,19 +328,24 @@ requires-dist = [ { name = "faker", marker = "extra == 'database'", specifier = ">=40.13.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.124.4,<0.125.0" }, { name = "inflection", specifier = ">=0.5.1" }, + { name = "itsdangerous", marker = "extra == 'fastapi'", specifier = ">=2.2.0" }, + { name = "jinja2", marker = "extra == 'inertia'", specifier = ">=3.1" }, { name = "jinja2", marker = "extra == 'vite'", specifier = ">=3.1" }, + { name = "markupsafe", marker = "extra == 'inertia'", specifier = ">=2.0" }, { name = "pendulum", specifier = ">=3.1.0,<4.0.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "requests", specifier = ">=2.32.5,<3.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database'", specifier = ">=2.0.38" }, ] -provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite"] +provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", "inertia"] [package.metadata.requires-dev] dev = [ { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.9.0" }, { name = "twine", specifier = ">=6.2.0" }, ] @@ -482,6 +505,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" diff --git a/fastapi_startkit/pyproject.toml b/fastapi_startkit/pyproject.toml index d2fca816..26f9079c 100644 --- a/fastapi_startkit/pyproject.toml +++ b/fastapi_startkit/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fastapi-startkit" -version = "0.22.0" +version = "0.23.0" description = "Fastapi Starter kit components" authors = [ {name = "Bedram Tamang", email = "tmgbedu@gmail.com"} diff --git a/fastapi_startkit/src/fastapi_startkit/__init__.py b/fastapi_startkit/src/fastapi_startkit/__init__.py index 892fc268..9660424d 100644 --- a/fastapi_startkit/src/fastapi_startkit/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/__init__.py @@ -2,4 +2,4 @@ from .console import ConsoleApplication from .configuration.config import Config -__all__ = ["Application", "Config"] +__all__ = ["Application", "ConsoleApplication", "Config"] diff --git a/fastapi_startkit/src/fastapi_startkit/fastapi/__init__.py b/fastapi_startkit/src/fastapi_startkit/fastapi/__init__.py index b19ad15c..254b5130 100644 --- a/fastapi_startkit/src/fastapi_startkit/fastapi/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/fastapi/__init__.py @@ -1,5 +1,6 @@ from .providers.fastapi_provider import FastAPIProvider from .routers.router import Router from .requests.model import RequestModel +from .config import FastAPIConfig -__all__ = ["FastAPIProvider", "Router", "RequestModel"] +__all__ = ["FastAPIProvider", "Router", "RequestModel", "FastAPIConfig"] diff --git a/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py b/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py index 9ba13166..d24f9e69 100644 --- a/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py +++ b/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py @@ -11,22 +11,22 @@ class ServeCommand(Command): "port", "p", flag=False, - default="8000", - description="The port to serve the application on", + default=None, + description="The port to serve the application on (overrides fastapi config)", ), option( "host", None, flag=False, - default="127.0.0.1", - description="The host to bind to", + default=None, + description="The host to bind to (overrides fastapi config)", ), option( "reload", "r", flag=False, - default=True, - description="Enable auto-reload on code changes", + default=None, + description="Enable auto-reload on code changes (overrides fastapi config)", ), option( "app", @@ -39,12 +39,20 @@ class ServeCommand(Command): def handle(self): import uvicorn + from fastapi_startkit import Config from fastapi_startkit.container import Container - port = int(self.option("port")) - host = self.option("host") + # Resolve server settings: CLI flag > fastapi config > uvicorn default (None) + cfg_host = Config.get("fastapi.host", "127.0.0.1") + cfg_port = Config.get("fastapi.port", 8000) + cfg_reload = Config.get("fastapi.reload", True) + cfg_reload_dirs = Config.get("fastapi.reload_dirs") or None + cfg_reload_excludes = Config.get("fastapi.reload_excludes") or None + + host = self.option("host") or cfg_host + port = int(self.option("port") or cfg_port) + reload = cfg_reload if self.option("reload") is None else self.option("reload") app = self.option("app") - reload = self.option("reload") exist = self.is_app_exist() @@ -59,11 +67,13 @@ def handle(self): kwargs.update( { "app": app, - "reload": reload, "factory": True, - "reload_excludes": ["*.log", "tests/*"], } ) + if cfg_reload_dirs is not None: + kwargs["reload_dirs"] = cfg_reload_dirs + if cfg_reload_excludes is not None: + kwargs["reload_excludes"] = cfg_reload_excludes self.line( f"Starting Uvicorn server on {host}:{port} [{app}]..." diff --git a/fastapi_startkit/src/fastapi_startkit/fastapi/config/__init__.py b/fastapi_startkit/src/fastapi_startkit/fastapi/config/__init__.py new file mode 100644 index 00000000..6ba558e7 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/fastapi/config/__init__.py @@ -0,0 +1,3 @@ +from .fastapi import FastAPIConfig + +__all__ = ["FastAPIConfig"] diff --git a/fastapi_startkit/src/fastapi_startkit/fastapi/config/fastapi.py b/fastapi_startkit/src/fastapi_startkit/fastapi/config/fastapi.py new file mode 100644 index 00000000..f85534c4 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/fastapi/config/fastapi.py @@ -0,0 +1,24 @@ +import dataclasses + +from fastapi_startkit.environment import env + + +@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)) + reload: bool = dataclasses.field(default_factory=lambda: env("APP_RELOAD", True)) + reload_dirs: list | None = None + reload_excludes: list = dataclasses.field( + default_factory=lambda: [ + "*.log", + "tests/*", + "node_modules/*", + ] + ) diff --git a/fastapi_startkit/src/fastapi_startkit/fastapi/providers/fastapi_provider.py b/fastapi_startkit/src/fastapi_startkit/fastapi/providers/fastapi_provider.py index 2f062e06..79eadab5 100644 --- a/fastapi_startkit/src/fastapi_startkit/fastapi/providers/fastapi_provider.py +++ b/fastapi_startkit/src/fastapi_startkit/fastapi/providers/fastapi_provider.py @@ -2,12 +2,18 @@ from fastapi import FastAPI from fastapi_startkit.fastapi.commands import ServeCommand +from fastapi_startkit.fastapi.config import FastAPIConfig from fastapi_startkit.providers import Provider class FastAPIProvider(Provider): + provider_key = "fastapi" + def register(self) -> None: """Create a FastAPI instance and register routers.""" + config = self.resolve_config(FastAPIConfig) + self.merge_config_from(config, self.provider_key) + fastapi = FastAPI( title="Jobins AI Agent (LangChain)", version="1.0.0", @@ -16,9 +22,16 @@ def register(self) -> None: self.app.use_fastapi(fastapi) def boot(self): + import os + self.commands([ServeCommand]) self._register_exception_handlers() + source = os.path.abspath( + os.path.join(os.path.dirname(__file__), "../config/fastapi.py") + ) + self.publishes({source: "config/fastapi.py"}) + def _register_exception_handlers(self): """Wire exception_manager as a catch-all handler for all exceptions.""" from fastapi import HTTPException diff --git a/fastapi_startkit/uv.lock b/fastapi_startkit/uv.lock index 2065bd39..f2d3b028 100644 --- a/fastapi_startkit/uv.lock +++ b/fastapi_startkit/uv.lock @@ -443,7 +443,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.21.0" +version = "0.23.0" source = { editable = "." } dependencies = [ { name = "cleo" },