fix(config): cascade .env file loading to nested settings classes
Why: - No setting in this app ever actually read from .env: only the outer Settings declared env_file=".env", and pydantic-settings does not cascade that to nested BaseSettings classes. Every previously-correct local value was coincidence (.env.example defaults matching class defaults). Found by testing EMBEDDING_OPENAI_API_KEY against the live OpenAI API. Changes: - Every nested settings class now declares env_file=".env" itself. - Settings.__init__/EmbeddingSettings.__init__ explicitly thread an _env_file override to every nested constructor, so overriding it (as tests do) reaches the whole tree, not just the outer class. - env_ignore_empty=True everywhere, since the fix surfaced a second bug: a blank env var (e.g. EMBEDDING_OPENAI_DIMENSIONS=) failed to parse as int | None instead of falling back to the field default. Impact: - Real deployments setting env vars directly (Docker Compose) are unaffected. Local .env-file development now actually works. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,31 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import Field, model_validator
|
from pydantic import Field, model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
# pydantic-settings does not cascade `env_file` from a parent `BaseSettings`
|
||||||
|
# to a nested one: each `BaseSettings` subclass only reads `.env` if its own
|
||||||
|
# `model_config` names it. `Settings` has no fields of its own -- only
|
||||||
|
# nested settings objects -- so every nested class below repeats
|
||||||
|
# `env_file=".env"` as its own default, or its fields would silently read
|
||||||
|
# only real process environment variables (fine under Docker Compose, broken
|
||||||
|
# for local `.env`-file development) while still *appearing* to work
|
||||||
|
# whenever a `.env.example` default happens to match the class default.
|
||||||
|
#
|
||||||
|
# That default alone isn't enough to let a caller point `Settings` at a
|
||||||
|
# *different* file (as the test suite does, to parse `.env.example`), since
|
||||||
|
# passing `_env_file=...` to `Settings(...)` only overrides `Settings`'s own
|
||||||
|
# `model_config` -- nested defaults still construct against their own
|
||||||
|
# hardcoded ".env". `Settings.__init__`/`EmbeddingSettings.__init__` below
|
||||||
|
# thread an explicit `_env_file` override through to every nested
|
||||||
|
# constructor so one override actually reaches the whole tree.
|
||||||
|
_UNSET: Any = object()
|
||||||
|
|
||||||
|
|
||||||
class PostgresSettings(BaseSettings):
|
class PostgresSettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix="POSTGRES_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="POSTGRES_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
host: str = "127.0.0.1"
|
host: str = "127.0.0.1"
|
||||||
port: int = 5433
|
port: int = 5433
|
||||||
@@ -17,7 +39,9 @@ class PostgresSettings(BaseSettings):
|
|||||||
|
|
||||||
|
|
||||||
class MinioSettings(BaseSettings):
|
class MinioSettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix="MINIO_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="MINIO_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
endpoint: str = "127.0.0.1:9100"
|
endpoint: str = "127.0.0.1:9100"
|
||||||
access_key: str = "chatbot"
|
access_key: str = "chatbot"
|
||||||
@@ -33,7 +57,9 @@ class IngestionSettings(BaseSettings):
|
|||||||
timeouts, or callers give up on work that is still succeeding.
|
timeouts, or callers give up on work that is still succeeding.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_prefix="INGESTION_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="INGESTION_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
max_concurrency: int = 4
|
max_concurrency: int = 4
|
||||||
thread_pool_size: int = 8
|
thread_pool_size: int = 8
|
||||||
@@ -57,7 +83,9 @@ class ChunkingSettings(BaseSettings):
|
|||||||
`search_document: ` task prefix and any heading text carried into a chunk.
|
`search_document: ` task prefix and any heading text carried into a chunk.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_prefix="CHUNKING_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="CHUNKING_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
strategy: str = "fixed_size"
|
strategy: str = "fixed_size"
|
||||||
chunk_size: int = 400
|
chunk_size: int = 400
|
||||||
@@ -81,7 +109,9 @@ class ChunkingSettings(BaseSettings):
|
|||||||
|
|
||||||
|
|
||||||
class QdrantSettings(BaseSettings):
|
class QdrantSettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix="QDRANT_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="QDRANT_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
url: str = "http://127.0.0.1:6343"
|
url: str = "http://127.0.0.1:6343"
|
||||||
api_key: str | None = None
|
api_key: str | None = None
|
||||||
@@ -102,7 +132,9 @@ class NomicEmbeddingSettings(BaseSettings):
|
|||||||
so it stays off until an emet run measures the pair together.
|
so it stays off until an emet run measures the pair together.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_prefix="EMBEDDING_NOMIC_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="EMBEDDING_NOMIC_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
base_url: str = "http://192.168.10.10:11435/v1"
|
base_url: str = "http://192.168.10.10:11435/v1"
|
||||||
model: str = "nomic-embed-text-v2-moe"
|
model: str = "nomic-embed-text-v2-moe"
|
||||||
@@ -121,7 +153,9 @@ class OpenaiEmbeddingSettings(BaseSettings):
|
|||||||
tweak.
|
tweak.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_prefix="EMBEDDING_OPENAI_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="EMBEDDING_OPENAI_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
base_url: str = "https://api.openai.com/v1"
|
base_url: str = "https://api.openai.com/v1"
|
||||||
model: str = "text-embedding-3-large"
|
model: str = "text-embedding-3-large"
|
||||||
@@ -144,7 +178,9 @@ class SparseEmbeddingSettings(BaseSettings):
|
|||||||
statistics without a code change.
|
statistics without a code change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_prefix="EMBEDDING_SPARSE_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="EMBEDDING_SPARSE_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
analyzer: str = "fa_norm_stop"
|
analyzer: str = "fa_norm_stop"
|
||||||
k: float = 1.2
|
k: float = 1.2
|
||||||
@@ -159,16 +195,27 @@ class EmbeddingSettings(BaseSettings):
|
|||||||
openai: OpenaiEmbeddingSettings = Field(default_factory=OpenaiEmbeddingSettings)
|
openai: OpenaiEmbeddingSettings = Field(default_factory=OpenaiEmbeddingSettings)
|
||||||
sparse: SparseEmbeddingSettings = Field(default_factory=SparseEmbeddingSettings)
|
sparse: SparseEmbeddingSettings = Field(default_factory=SparseEmbeddingSettings)
|
||||||
|
|
||||||
|
def __init__(self, _env_file: Any = _UNSET, **data: Any) -> None:
|
||||||
|
env_file = ".env" if _env_file is _UNSET else _env_file
|
||||||
|
data.setdefault("nomic", NomicEmbeddingSettings(_env_file=env_file))
|
||||||
|
data.setdefault("openai", OpenaiEmbeddingSettings(_env_file=env_file))
|
||||||
|
data.setdefault("sparse", SparseEmbeddingSettings(_env_file=env_file))
|
||||||
|
super().__init__(**data)
|
||||||
|
|
||||||
|
|
||||||
class AppLimitSettings(BaseSettings):
|
class AppLimitSettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix="APP_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="APP_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
env: str = "local"
|
env: str = "local"
|
||||||
readiness_check_timeout_seconds: float = 2.0
|
readiness_check_timeout_seconds: float = 2.0
|
||||||
|
|
||||||
|
|
||||||
class LoggingSettings(BaseSettings):
|
class LoggingSettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_prefix="LOG_", extra="ignore")
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="LOG_", extra="ignore", env_file=".env", env_ignore_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
level: str = "INFO"
|
level: str = "INFO"
|
||||||
json_format: bool = False
|
json_format: bool = False
|
||||||
@@ -185,3 +232,15 @@ class Settings(BaseSettings):
|
|||||||
embedding: EmbeddingSettings = Field(default_factory=EmbeddingSettings)
|
embedding: EmbeddingSettings = Field(default_factory=EmbeddingSettings)
|
||||||
app: AppLimitSettings = Field(default_factory=AppLimitSettings)
|
app: AppLimitSettings = Field(default_factory=AppLimitSettings)
|
||||||
logging: LoggingSettings = Field(default_factory=LoggingSettings)
|
logging: LoggingSettings = Field(default_factory=LoggingSettings)
|
||||||
|
|
||||||
|
def __init__(self, _env_file: Any = _UNSET, **data: Any) -> None:
|
||||||
|
env_file = ".env" if _env_file is _UNSET else _env_file
|
||||||
|
data.setdefault("postgres", PostgresSettings(_env_file=env_file))
|
||||||
|
data.setdefault("minio", MinioSettings(_env_file=env_file))
|
||||||
|
data.setdefault("ingestion", IngestionSettings(_env_file=env_file))
|
||||||
|
data.setdefault("chunking", ChunkingSettings(_env_file=env_file))
|
||||||
|
data.setdefault("qdrant", QdrantSettings(_env_file=env_file))
|
||||||
|
data.setdefault("embedding", EmbeddingSettings(_env_file=env_file))
|
||||||
|
data.setdefault("app", AppLimitSettings(_env_file=env_file))
|
||||||
|
data.setdefault("logging", LoggingSettings(_env_file=env_file))
|
||||||
|
super().__init__(_env_file=env_file, **data)
|
||||||
|
|||||||
Reference in New Issue
Block a user