Add fastapi_vue.env accessor with FASTAPI_VUE prefix
This commit is contained in:
@@ -68,3 +68,13 @@ server.run("my_app.app:app", listen=["localhost:8000"])
|
||||
```
|
||||
|
||||
- As a deployment option, environment `FORWARDED_ALLOW_IPS` controls `X-Forwarded` trusted IPs (default: `127.0.0.1,::1`).
|
||||
|
||||
## Environment variables
|
||||
|
||||
The generated project entry point sets `FASTAPI_VUE` to the project's environment prefix (e.g. `MY_APP`), and settings are passed as `<PREFIX>_*` variables. `fastapi_vue.env` resolves them, lazily on each access:
|
||||
|
||||
- `fastapi_vue.env.dev` — running under the devserver (`<PREFIX>_DEV=1`)
|
||||
- `fastapi_vue.env.vite_url`, `fastapi_vue.env.backend_url` — URLs set by the devserver
|
||||
- `fastapi_vue.env.prefix` — the prefix itself
|
||||
|
||||
Value accessors return `None` when `FASTAPI_VUE` or the variable is not set.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""FastAPI Vue integration - serve Vue frontend from FastAPI."""
|
||||
|
||||
from .environ import env
|
||||
from .staticfiles import Frontend
|
||||
|
||||
__all__ = ["Frontend"]
|
||||
__all__ = ["Frontend", "env"]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Access to the project's fastapi-vue environment variables.
|
||||
|
||||
The project entry point (generated __main__.py) sets FASTAPI_VUE to the
|
||||
project-specific prefix (e.g. "MY_APP"). Project settings are then passed
|
||||
as "<PREFIX>_*" environment variables; this module is the single place
|
||||
that resolves those names.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
PREFIX_VARIABLE = "FASTAPI_VUE"
|
||||
|
||||
|
||||
class _Env:
|
||||
"""Lazy accessors for the project's "<PREFIX>_*" environment variables.
|
||||
|
||||
Evaluated on each access. Value accessors return None when FASTAPI_VUE
|
||||
or the variable itself is not set.
|
||||
"""
|
||||
|
||||
@property
|
||||
def prefix(self) -> str | None:
|
||||
"""Return the project prefix from the FASTAPI_VUE environment variable."""
|
||||
return os.environ.get(PREFIX_VARIABLE) or None
|
||||
|
||||
def _get(self, name: str) -> str | None:
|
||||
prefix = self.prefix
|
||||
return os.environ.get(f"{prefix}_{name}") if prefix else None
|
||||
|
||||
@property
|
||||
def dev(self) -> bool:
|
||||
"""Check whether running under the devserver (<PREFIX>_DEV=1)."""
|
||||
return self._get("DEV") == "1"
|
||||
|
||||
@property
|
||||
def vite_url(self) -> str | None:
|
||||
"""Return the vite devserver URL (<PREFIX>_VITE_URL), if set."""
|
||||
return self._get("VITE_URL")
|
||||
|
||||
@property
|
||||
def backend_url(self) -> str | None:
|
||||
"""Return the backend URL (<PREFIX>_BACKEND_URL), if set."""
|
||||
return self._get("BACKEND_URL")
|
||||
|
||||
|
||||
env = _Env()
|
||||
@@ -15,6 +15,7 @@ from uvicorn import Config, Server
|
||||
from uvicorn.main import STARTUP_FAILURE
|
||||
from uvicorn.supervisors import ChangeReload, Multiprocess
|
||||
|
||||
from .environ import env
|
||||
from .hostutil import parse_endpoints
|
||||
from .logging import (
|
||||
install_access_log,
|
||||
@@ -44,10 +45,16 @@ def _bind_hosts(host: str) -> list[str]:
|
||||
|
||||
|
||||
def _connect_url(endpoints: list[dict]) -> str:
|
||||
"""Return a URL the user can connect to for the first TCP endpoint."""
|
||||
for key, value in sorted(os.environ.items()):
|
||||
if key.endswith("_VITE_URL") and value:
|
||||
return value
|
||||
"""Return a URL the user can connect to for the first TCP endpoint.
|
||||
|
||||
When running under the devserver (<PREFIX>_VITE_URL is set), the vite
|
||||
devserver URL is shown instead, as that is where the page is served.
|
||||
Wildcard binds (0.0.0.0, ::) are shown as localhost, as that is the
|
||||
address a user can actually open. Unix-socket-only setups show plain
|
||||
http://localhost (the typical reverse-proxy target).
|
||||
"""
|
||||
if vite_url := env.vite_url:
|
||||
return vite_url
|
||||
for endpoint in endpoints:
|
||||
host = endpoint.get("host")
|
||||
if host is None:
|
||||
|
||||
@@ -19,6 +19,8 @@ from starlette.exceptions import HTTPException
|
||||
from starlette.routing import Route
|
||||
from zstandard import ZstdCompressor
|
||||
|
||||
from .environ import env
|
||||
|
||||
logger = logging.getLogger("uvicorn.error") # Use FastAPI logging style
|
||||
|
||||
__all__ = ["Frontend"]
|
||||
@@ -287,7 +289,8 @@ class Frontend:
|
||||
|
||||
def _devmode_respond(_request: Request, _name: str = "") -> JSONResponse:
|
||||
"""Return error response directing to Vite server."""
|
||||
at = f" at {env.vite_url}" if env.vite_url else ""
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"detail": "[devmode] Use Vite devserver instead."},
|
||||
content={"detail": f"[devmode] Use Vite devserver{at} instead."},
|
||||
)
|
||||
|
||||
+63
-23
@@ -161,7 +161,7 @@ NEW_BUILD_HOOK_PATH = "scripts/fastapi-vue/buildhook.py"
|
||||
# Frontend instantiation block for patching existing apps
|
||||
FRONTEND_BLOCK = """
|
||||
# Vue Frontend static files
|
||||
frontend = Frontend(Path(__file__).with_name("frontend-build"))
|
||||
frontend = fastapi_vue.Frontend(Path(__file__).with_name("frontend-build"))
|
||||
"""
|
||||
|
||||
# Lifespan block for patching apps that don't have one
|
||||
@@ -473,8 +473,8 @@ def _find_app_in_subpackage(subpkg_dir: Path) -> tuple[Path, str] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _add_devmode_to_main(content: str) -> str:
|
||||
"""Add DEVMODE variable to an existing main module."""
|
||||
def _add_env_prefix_to_main(content: str) -> str:
|
||||
"""Add FASTAPI_VUE environment prefix setup to an existing main module."""
|
||||
lines = content.splitlines()
|
||||
|
||||
# Check if os is imported
|
||||
@@ -489,7 +489,7 @@ def _add_devmode_to_main(content: str) -> str:
|
||||
elif stripped and not stripped.startswith("#"):
|
||||
break
|
||||
|
||||
# Insert imports and DEVMODE after existing imports
|
||||
# Insert imports and env setup after existing imports
|
||||
new_lines = []
|
||||
if not has_os_import:
|
||||
new_lines.append("import os")
|
||||
@@ -497,7 +497,7 @@ def _add_devmode_to_main(content: str) -> str:
|
||||
[
|
||||
"",
|
||||
"# Added by fastapi-vue-setup",
|
||||
'DEVMODE = os.getenv("ENVPREFIX_DEV") == "1"',
|
||||
'os.environ["FASTAPI_VUE"] = "ENVPREFIX"',
|
||||
"",
|
||||
]
|
||||
)
|
||||
@@ -597,11 +597,35 @@ def render_template(template: str, **kwargs: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def patch_app_file(path: Path, main_module_path: str, app_var: str, *, dry: bool = False) -> bool:
|
||||
def needs_app_migration(project_dir: Path) -> bool:
|
||||
"""Check if the project was set up with fastapi-vue older than 1.6.
|
||||
|
||||
Those versions patched app.py with `from fastapi_vue import Frontend` and
|
||||
a DEVMODE import from the main module; 1.6+ uses fastapi_vue.Frontend and
|
||||
fastapi_vue.env. Must be called before the dependency step rewrites the
|
||||
fastapi-vue requirement in pyproject.toml.
|
||||
"""
|
||||
pyproject = project_dir / "pyproject.toml"
|
||||
if not pyproject.exists():
|
||||
return False
|
||||
data = tomlkit.parse(pyproject.read_text("UTF-8"))
|
||||
for dep in data.get("project", {}).get("dependencies", []):
|
||||
match = re.match(r"\s*fastapi-vue(?:\[[^\]]*\])?\s*(.*)", str(dep))
|
||||
if match:
|
||||
version = re.search(r"(\d+)\.(\d+)", match.group(1))
|
||||
return version is not None and (int(version[1]), int(version[2])) < (1, 6)
|
||||
return False
|
||||
|
||||
|
||||
def patch_app_file(
|
||||
path: Path, main_module_path: str, app_var: str, *, migrate: bool = False, dry: bool = False
|
||||
) -> bool:
|
||||
"""Patch an existing app.py with frontend integration.
|
||||
|
||||
Inserts imports at top (ruff will sort them), route at bottom,
|
||||
and tries to patch lifespan with frontend.load().
|
||||
and tries to patch lifespan with frontend.load(). With migrate=True,
|
||||
pre-1.6 patching (plain Frontend, DEVMODE import) is first rewritten
|
||||
to the current format.
|
||||
|
||||
Returns True if patched, False if already patched or failed.
|
||||
"""
|
||||
@@ -612,24 +636,38 @@ def patch_app_file(path: Path, main_module_path: str, app_var: str, *, dry: bool
|
||||
original_content = path.read_text("UTF-8")
|
||||
content = original_content
|
||||
|
||||
# Check what's already patched
|
||||
has_frontend = "from fastapi_vue import Frontend" in content
|
||||
has_devmode = f"from {main_module_path} import DEVMODE" in content
|
||||
# Migrate pre-1.6 patching to the current format: Frontend via the
|
||||
# fastapi_vue module, DEVMODE via fastapi_vue.env
|
||||
if migrate:
|
||||
if "from fastapi_vue import Frontend\n" in content:
|
||||
content = content.replace("from fastapi_vue import Frontend\n", "")
|
||||
content = re.sub(r"(?<![\w.])Frontend\(", "fastapi_vue.Frontend(", content)
|
||||
old_import = f"from {main_module_path} import DEVMODE"
|
||||
if old_import in content:
|
||||
has_plain_import = re.search(r"^import fastapi_vue$", content, re.MULTILINE)
|
||||
content = content.replace(old_import, "" if has_plain_import else "import fastapi_vue")
|
||||
content = content.replace("debug=DEVMODE", "debug=fastapi_vue.env.dev")
|
||||
|
||||
# Check what's already patched; plain "Frontend(" so user modifications
|
||||
# of the integration (renames, different call shape) still count
|
||||
has_frontend = "Frontend(" in content
|
||||
has_debug_arg = re.search(r"FastAPI\s*\([^)]*debug\s*=", content) is not None
|
||||
has_lifespan = "await frontend.load()" in content
|
||||
|
||||
if has_frontend and has_devmode and has_debug_arg and has_lifespan:
|
||||
already_patched = has_frontend and has_debug_arg and has_lifespan
|
||||
if content == original_content and already_patched:
|
||||
print(f"✔️ {path} (already patched)")
|
||||
return False
|
||||
|
||||
route_line = f'frontend.route({app_var}, "/")'
|
||||
|
||||
# Add missing imports (using AST to find correct insertion point)
|
||||
# Add missing imports (using AST to find correct insertion point);
|
||||
# every patch path uses fastapi_vue.*, so always ensure the plain import
|
||||
imports = []
|
||||
if not has_frontend:
|
||||
imports.extend(["from pathlib import Path", "from fastapi_vue import Frontend"])
|
||||
if not has_devmode:
|
||||
imports.append(f"from {main_module_path} import DEVMODE")
|
||||
imports.append("from pathlib import Path")
|
||||
if not re.search(r"^import fastapi_vue$", content, re.MULTILINE):
|
||||
imports.append("import fastapi_vue")
|
||||
if imports:
|
||||
insert_line = find_import_insertion_line(content)
|
||||
lines = content.splitlines(keepends=True)
|
||||
@@ -664,14 +702,14 @@ def patch_app_file(path: Path, main_module_path: str, app_var: str, *, dry: bool
|
||||
lines.append(route_line)
|
||||
content = "\n".join(lines)
|
||||
|
||||
# Try to patch FastAPI() call with debug=DEVMODE if no debug arg exists
|
||||
# Try to patch FastAPI() call with debug=fastapi_vue.env.dev if no debug arg exists
|
||||
if not has_debug_arg:
|
||||
fastapi_pattern = r"(\w+\s*=\s*FastAPI\s*\()([^)]*)\)"
|
||||
for match in re.finditer(fastapi_pattern, content, re.DOTALL):
|
||||
args = match.group(2)
|
||||
if "debug" not in args:
|
||||
# Add debug=DEVMODE as last argument
|
||||
new_args = f"{args}, debug=DEVMODE" if args.strip() else "debug=DEVMODE"
|
||||
# Add debug=fastapi_vue.env.dev as last argument
|
||||
new_args = (f"{args}, " if args.strip() else "") + "debug=fastapi_vue.env.dev"
|
||||
content = (
|
||||
content[: match.start()]
|
||||
+ match.group(1)
|
||||
@@ -1316,7 +1354,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
||||
# Check if project already has a CLI entrypoint in pyproject.toml
|
||||
existing_cli_module = _find_existing_cli_module_path(project_dir, module_name)
|
||||
|
||||
# Determine main module path for DEVMODE import
|
||||
# Determine main module path (for migrating old DEVMODE imports)
|
||||
main_module_path = existing_cli_module or f"{module_name}.__main__"
|
||||
if existing_cli_module:
|
||||
print(f"ℹ️ Using existing CLI: {existing_cli_module}")
|
||||
@@ -1463,7 +1501,9 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
||||
# === Handle app module ===
|
||||
if app_file:
|
||||
# Existing app: patch with import, route, and try to patch lifespan
|
||||
patch_app_file(app_file, main_module_path, app_var, dry=dry)
|
||||
patch_app_file(
|
||||
app_file, main_module_path, app_var, migrate=needs_app_migration(project_dir), dry=dry
|
||||
)
|
||||
else:
|
||||
# No app: create full app.py
|
||||
# Create __init__.py if missing
|
||||
@@ -1509,7 +1549,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
||||
)
|
||||
else:
|
||||
# Existing CLI entrypoint: write our template as .new.py beside the existing module
|
||||
# and also patch the existing module with DEVMODE if needed
|
||||
# and also patch the existing module with FASTAPI_VUE setup if needed
|
||||
_write_fallback_file(
|
||||
main,
|
||||
main_fallback,
|
||||
@@ -1519,8 +1559,8 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
||||
)
|
||||
if main.exists():
|
||||
content = main.read_text("UTF-8")
|
||||
if "DEVMODE" not in content:
|
||||
new_content = _add_devmode_to_main(content)
|
||||
if "FASTAPI_VUE" not in content:
|
||||
new_content = _add_env_prefix_to_main(content)
|
||||
new_file = main.with_suffix(".new.py")
|
||||
_write_fallback_file(
|
||||
main,
|
||||
|
||||
@@ -5,10 +5,11 @@ import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import fastapi_vue
|
||||
from fastapi_vue import server
|
||||
|
||||
DEFAULT_PORT = TEMPLATE_DEFAULT_PORT
|
||||
DEVMODE = os.getenv("ENVPREFIX_DEV") == "1"
|
||||
os.environ["FASTAPI_VUE"] = "ENVPREFIX"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -26,7 +27,7 @@ def main() -> None:
|
||||
listen=args.listen,
|
||||
default_port=DEFAULT_PORT,
|
||||
server_header=False,
|
||||
reload=Path(__file__).parent if DEVMODE else False,
|
||||
reload=Path(__file__).parent if fastapi_vue.env.dev else False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import fastapi_vue
|
||||
from fastapi import FastAPI
|
||||
from fastapi_vue import Frontend
|
||||
from MAIN_MODULE import DEVMODE
|
||||
|
||||
# Vue Frontend static files
|
||||
frontend = Frontend(Path(__file__).with_name("frontend-build"))
|
||||
frontend = fastapi_vue.Frontend(Path(__file__).with_name("frontend-build"))
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -19,7 +18,7 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator:
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="PROJECT_TITLE", debug=DEVMODE, lifespan=lifespan)
|
||||
app = FastAPI(title="PROJECT_TITLE", debug=fastapi_vue.env.dev, lifespan=lifespan)
|
||||
|
||||
|
||||
# Add API routes here...
|
||||
|
||||
Reference in New Issue
Block a user