5128e1551c
Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
than string formatting: the password contains '@', which would otherwise be
parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
no tables are created, at import time or otherwise
Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000
The database paper_doc was created in TiDB out of band; no table exists yet.
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
"""Application settings, loaded from ``backend/.env`` and the environment.
|
|
|
|
The database URL is assembled with :func:`sqlalchemy.engine.URL.create` rather
|
|
than string formatting. That matters here: the TiDB password contains ``@``,
|
|
and interpolating it into a DSN by hand silently truncates the password at the
|
|
first ``@`` (it gets parsed as the user/host separator). ``URL.create``
|
|
percent-encodes each component correctly.
|
|
"""
|
|
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Union
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
from sqlalchemy import URL
|
|
|
|
# backend/app/core/config.py -> backend/
|
|
BASE_DIR = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Runtime configuration for the API service."""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=BASE_DIR / ".env",
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
)
|
|
|
|
app_name: str = "paper-doc API"
|
|
api_prefix: str = "/api"
|
|
debug: bool = True
|
|
|
|
# The Vite dev server proxies /api to this service, so CORS is normally not
|
|
# exercised during development; it is configured for direct-origin setups.
|
|
cors_origins: list[str] = [
|
|
"http://127.0.0.1:5173",
|
|
"http://localhost:5173",
|
|
]
|
|
|
|
# --- Database (TiDB) -------------------------------------------------
|
|
db_host: str = "127.0.0.1"
|
|
db_port: int = 4000
|
|
db_user: str = "root"
|
|
db_password: str = ""
|
|
db_name: str = "paper_doc"
|
|
|
|
# When set, this wins over the DB_* parts above and is used verbatim.
|
|
database_url: str | None = None
|
|
|
|
# --- Connection pool -------------------------------------------------
|
|
db_echo: bool = False
|
|
db_pool_pre_ping: bool = True
|
|
db_pool_recycle: int = 3600
|
|
|
|
@property
|
|
def sqlalchemy_url(self) -> Union[URL, str]:
|
|
"""The SQLAlchemy connection URL.
|
|
|
|
Returns a :class:`~sqlalchemy.engine.URL` unless ``DATABASE_URL`` was
|
|
supplied explicitly, in which case that string is passed through.
|
|
"""
|
|
if self.database_url:
|
|
return self.database_url
|
|
return URL.create(
|
|
drivername="mysql+pymysql",
|
|
username=self.db_user,
|
|
password=self.db_password,
|
|
host=self.db_host,
|
|
port=self.db_port,
|
|
database=self.db_name,
|
|
)
|
|
|
|
@property
|
|
def safe_database_url(self) -> str:
|
|
"""The URL rendered with the password masked, for logs and diagnostics."""
|
|
url = self.sqlalchemy_url
|
|
if isinstance(url, str):
|
|
return URL.make_url(url).render_as_string(hide_password=True)
|
|
return url.render_as_string(hide_password=True)
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
"""Return the process-wide settings singleton."""
|
|
return Settings()
|