Skip to content

Commit 650ecef

Browse files
fix(settings): avoid side effects and create data dir at startup
1 parent ff51f20 commit 650ecef

File tree

6 files changed

+34
-5
lines changed

6 files changed

+34
-5
lines changed

src/local_rag_backend/app/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
from collections.abc import AsyncGenerator
2828
from importlib.resources.abc import Traversable
2929

30-
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
30+
_LOG_LEVEL = getattr(logging, settings.log_level, logging.INFO)
31+
logging.basicConfig(level=_LOG_LEVEL, stream=sys.stdout)
3132
logger = logging.getLogger(__name__)
3233

3334
# Frontend directory in the source tree (works for editable installs / running from repo)
@@ -38,6 +39,8 @@
3839
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
3940
"""Manage application startup and shutdown events."""
4041
logger.info("Initializing RAG service...")
42+
# Ensure the data directory exists (SQLite cannot create parent directories).
43+
settings.data_dir.mkdir(parents=True, exist_ok=True)
4144
# Use the module reference so tests can monkeypatch `db_base.engine` / `db_base.SessionLocal`.
4245
db_base.Base.metadata.create_all(bind=db_base.engine)
4346
# Best-effort preload: don't prevent the API from starting just because an LLM

src/local_rag_backend/bootstrap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING
66

77
from local_rag_backend.app.factory import build_rag_service
8+
from local_rag_backend.settings import settings
89

910
if TYPE_CHECKING:
1011
from local_rag_backend.core.services.rag import RagService
@@ -17,6 +18,7 @@ def bootstrap_rag_service() -> RagService:
1718
Note: this is a non-cached builder. FastAPI uses an internal cached singleton via DI.
1819
"""
1920

21+
settings.data_dir.mkdir(parents=True, exist_ok=True)
2022
return build_rag_service()
2123

2224

src/local_rag_backend/scripts/bootstrap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def main(csv_path: str | Path | None = None, **kwargs: Any) -> None:
4141
if "settings" not in kwargs:
4242
kwargs["settings"] = default_settings
4343
settings = kwargs["settings"]
44+
# Ensure data dir exists before touching SQLite.
45+
settings.data_dir.mkdir(parents=True, exist_ok=True)
4446
# 1) Create engine and session based on the updated URL
4547
engine = create_engine(settings.sqlite_url, connect_args={"check_same_thread": False})
4648
session_local = sessionmaker(bind=engine, autocommit=False, autoflush=False)

src/local_rag_backend/scripts/build_index.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def main() -> None:
4444
Initializes DB and optionally FAISS index using functionality from data_loader.
4545
"""
4646
logger.info("Starting build_index script...")
47+
# Ensure data dir exists before touching SQLite.
48+
settings.data_dir.mkdir(parents=True, exist_ok=True)
4749

4850
# 1. Create a dedicated engine and SessionLocal for this script
4951
# This avoids conflicts with the main app engine.

src/local_rag_backend/settings.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,19 @@ def __init__(self, **kwargs: Any) -> None: ...
105105
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
106106
)
107107

108+
@field_validator("log_level", mode="before")
109+
@classmethod
110+
def _normalize_log_level(cls, v: Any) -> Any:
111+
# Make env/config more forgiving while keeping a strict Literal type.
112+
if isinstance(v, str):
113+
return v.upper()
114+
return v
115+
108116
@field_validator("data_dir", mode="before")
109117
@classmethod
110-
def _ensure_data_dir_exists(cls, v: Any) -> Path:
111-
path = Path(v)
112-
path.mkdir(parents=True, exist_ok=True)
113-
return path
118+
def _normalize_data_dir(cls, v: Any) -> Path:
119+
# Keep Settings side-effect free; callers are responsible for creating directories.
120+
return Path(v)
114121

115122
@field_validator("sqlite_url")
116123
@classmethod

tests/unit/settings/test_settings_validators.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,16 @@ def test_chunk_overlap_lt_chars_validator():
1818
"""Ensure overlap must be strictly less than chunk size."""
1919
with pytest.raises(ValueError):
2020
Settings(ingest_chunk_chars=100, ingest_chunk_overlap=100)
21+
22+
23+
def test_log_level_is_normalized_to_uppercase():
24+
s = Settings(log_level="debug")
25+
assert s.log_level == "DEBUG"
26+
27+
28+
def test_data_dir_is_not_created_as_a_side_effect(tmp_path):
29+
d = tmp_path / "new-data-dir"
30+
assert not d.exists()
31+
s = Settings(data_dir=d)
32+
assert s.data_dir == d
33+
assert not d.exists()

0 commit comments

Comments
 (0)