|
|
|
|
|
import json |
|
|
import os |
|
|
from functools import lru_cache |
|
|
from urllib.parse import quote |
|
|
|
|
|
|
|
|
try: |
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict |
|
|
from pydantic import BaseModel, Field |
|
|
IS_V2 = True |
|
|
except Exception: |
|
|
from pydantic import BaseSettings as _BaseSettings, BaseModel, Field |
|
|
class BaseSettings(_BaseSettings): |
|
|
pass |
|
|
IS_V2 = False |
|
|
|
|
|
|
|
|
APPSETTINGS_PATH = os.environ.get("APPSETTINGS_JSON", "appsettings.json") |
|
|
|
|
|
def _load_json(path: str): |
|
|
if not path or not os.path.exists(path): |
|
|
return {} |
|
|
try: |
|
|
with open(path, "r", encoding="utf-8") as f: |
|
|
return json.load(f) |
|
|
except Exception: |
|
|
return {} |
|
|
|
|
|
def _env_or(raw_val: str | None, env_name: str | None): |
|
|
""" |
|
|
If raw_val == ".env" -> fetch from env_name (or the obvious uppercase key). |
|
|
Otherwise return raw_val. |
|
|
""" |
|
|
if raw_val != ".env": |
|
|
return raw_val |
|
|
key = env_name or "" |
|
|
|
|
|
if not key: |
|
|
return None |
|
|
return os.environ.get(key) |
|
|
|
|
|
def _bool(v, default=False): |
|
|
if v is None: |
|
|
return default |
|
|
if isinstance(v, bool): |
|
|
return v |
|
|
s = str(v).strip().lower() |
|
|
if s in {"1", "true", "yes", "y", "on"}: |
|
|
return True |
|
|
if s in {"0", "false", "no", "n", "off"}: |
|
|
return False |
|
|
return default |
|
|
|
|
|
|
|
|
class LocalSystemUrl(BaseModel): |
|
|
ExternalUrl: str | None = None |
|
|
IPAddress: str | None = None |
|
|
RabbitHostName: str | None = None |
|
|
RabbitPort: int | None = None |
|
|
RabbitInstanceName: str | None = None |
|
|
RabbitUserName: str | None = None |
|
|
RabbitPassword: str | None = None |
|
|
RabbitVHost: str | None = None |
|
|
UseTls: bool | None = None |
|
|
|
|
|
|
|
|
class _SettingsModel(BaseModel): |
|
|
|
|
|
AMQP_URL: str | None = None |
|
|
|
|
|
|
|
|
RABBIT_INSTANCE_NAME: str = "prod" |
|
|
RABBIT_EXCHANGE_TYPE: str = "topic" |
|
|
RABBIT_ROUTING_KEY: str = "" |
|
|
RABBIT_PREFETCH: int = 1 |
|
|
|
|
|
|
|
|
RABBIT_TLS_VERIFY: bool = False |
|
|
RABBIT_TLS_CHECK_HOSTNAME: bool = False |
|
|
RABBIT_TLS_CA_FILE: str | None = None |
|
|
|
|
|
SERVICE_ID: str = "gradllm" |
|
|
USE_TLS: bool = True |
|
|
|
|
|
EXCHANGE_TYPES: dict[str, str] = Field(default_factory=dict) |
|
|
|
|
|
|
|
|
LocalSystemUrl: LocalSystemUrl | None = None |
|
|
ServiceID: str | None = None |
|
|
RabbitRoutingKey: str | None = None |
|
|
RabbitExhangeType: str | None = None |
|
|
UseTls: bool | None = None |
|
|
|
|
|
|
|
|
RedisUrl: str | None = None |
|
|
RedisSecret: str | None = None |
|
|
|
|
|
|
|
|
if IS_V2: |
|
|
model_config = SettingsConfigDict(extra="ignore") |
|
|
else: |
|
|
class Config: |
|
|
extra = "ignore" |
|
|
|
|
|
|
|
|
def _build_amqp_url_from_local(local: LocalSystemUrl, top_level_use_tls: bool | None): |
|
|
""" |
|
|
Build amqp(s)://user:pass@host:port/vhost?heartbeat=30 |
|
|
Uses LocalSystemUrl + `.env` indirections. |
|
|
""" |
|
|
if not local or not local.RabbitHostName or not local.RabbitUserName: |
|
|
return None |
|
|
|
|
|
|
|
|
use_tls = local.UseTls |
|
|
if use_tls is None: |
|
|
use_tls = top_level_use_tls |
|
|
if use_tls is None: |
|
|
use_tls = True |
|
|
|
|
|
scheme = "amqps" if use_tls else "amqp" |
|
|
|
|
|
|
|
|
|
|
|
raw_pwd = local.RabbitPassword |
|
|
pwd = raw_pwd if raw_pwd and raw_pwd != ".env" else os.environ.get("RABBIT_PASSWORD") |
|
|
|
|
|
if not pwd: |
|
|
pwd = os.environ.get("RabbitPassword") or os.environ.get("RABBIT_PASS") |
|
|
|
|
|
user = local.RabbitUserName |
|
|
host = local.RabbitHostName |
|
|
port = local.RabbitPort or (5671 if scheme == "amqps" else 5672) |
|
|
vhost = local.RabbitVHost or "/" |
|
|
vhost_enc = quote(vhost, safe="") |
|
|
|
|
|
return f"{scheme}://{user}:{pwd}@{host}:{port}/{vhost_enc}?heartbeat=30" |
|
|
|
|
|
|
|
|
def _merge_env_over_json(j: dict) -> _SettingsModel: |
|
|
""" |
|
|
Precedence: |
|
|
1) Environment variables (HF Secrets) |
|
|
2) appsettings.json values |
|
|
3) built defaults |
|
|
We also synthesize AMQP_URL from LocalSystemUrl if not set via env. |
|
|
""" |
|
|
|
|
|
model = _SettingsModel(**j) |
|
|
|
|
|
|
|
|
if model.ServiceID: |
|
|
model.SERVICE_ID = model.ServiceID |
|
|
if model.RabbitRoutingKey: |
|
|
model.RABBIT_ROUTING_KEY = model.RabbitRoutingKey |
|
|
if model.RabbitExhangeType: |
|
|
model.RABBIT_EXCHANGE_TYPE = model.RabbitExhangeType |
|
|
if model.UseTls is not None: |
|
|
model.USE_TLS = _bool(model.UseTls, model.USE_TLS) |
|
|
|
|
|
|
|
|
if not model.AMQP_URL and model.LocalSystemUrl: |
|
|
built = _build_amqp_url_from_local(model.LocalSystemUrl, model.UseTls) |
|
|
if built: |
|
|
model.AMQP_URL = built |
|
|
|
|
|
|
|
|
env = os.environ |
|
|
|
|
|
|
|
|
model.AMQP_URL = env.get("AMQP_URL", model.AMQP_URL) |
|
|
|
|
|
|
|
|
model.RABBIT_INSTANCE_NAME = env.get("RABBIT_INSTANCE_NAME", model.RABBIT_INSTANCE_NAME) |
|
|
model.RABBIT_EXCHANGE_TYPE = env.get("RABBIT_EXCHANGE_TYPE", model.RABBIT_EXCHANGE_TYPE) |
|
|
model.RABBIT_ROUTING_KEY = env.get("RABBIT_ROUTING_KEY", model.RABBIT_ROUTING_KEY) |
|
|
model.RABBIT_PREFETCH = int(env.get("RABBIT_PREFETCH", model.RABBIT_PREFETCH)) |
|
|
|
|
|
|
|
|
if "RABBIT_TLS_VERIFY" in env: |
|
|
model.RABBIT_TLS_VERIFY = _bool(env["RABBIT_TLS_VERIFY"], model.RABBIT_TLS_VERIFY) |
|
|
if "RABBIT_TLS_CHECK_HOSTNAME" in env: |
|
|
model.RABBIT_TLS_CHECK_HOSTNAME = _bool(env["RABBIT_TLS_CHECK_HOSTNAME"], model.RABBIT_TLS_CHECK_HOSTNAME) |
|
|
model.RABBIT_TLS_CA_FILE = env.get("RABBIT_TLS_CA_FILE", model.RABBIT_TLS_CA_FILE) |
|
|
|
|
|
|
|
|
model.SERVICE_ID = env.get("SERVICE_ID", model.SERVICE_ID) |
|
|
|
|
|
|
|
|
et = env.get("EXCHANGE_TYPES") |
|
|
if et: |
|
|
try: |
|
|
model.EXCHANGE_TYPES = json.loads(et) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
|
|
|
if not model.AMQP_URL: |
|
|
raise RuntimeError( |
|
|
"AMQP_URL is not configured. Set it via:\n" |
|
|
"- env secret AMQP_URL, or\n" |
|
|
"- appsettings.json LocalSystemUrl (RabbitHostName/UserName/Password/VHost/UseTls)." |
|
|
) |
|
|
|
|
|
return model |
|
|
|
|
|
|
|
|
class Settings(BaseSettings): |
|
|
""" |
|
|
Thin wrapper that exposes the merged model as attributes. |
|
|
""" |
|
|
|
|
|
AMQP_URL: str |
|
|
|
|
|
|
|
|
RABBIT_INSTANCE_NAME: str = "prod" |
|
|
RABBIT_EXCHANGE_TYPE: str = "topic" |
|
|
RABBIT_ROUTING_KEY: str = "" |
|
|
RABBIT_PREFETCH: int = 1 |
|
|
|
|
|
|
|
|
RABBIT_TLS_VERIFY: bool = False |
|
|
RABBIT_TLS_CHECK_HOSTNAME: bool = False |
|
|
RABBIT_TLS_CA_FILE: str | None = None |
|
|
|
|
|
SERVICE_ID: str = "gradllm" |
|
|
USE_TLS: bool = True |
|
|
|
|
|
EXCHANGE_TYPES: dict[str, str] = Field(default_factory=dict) |
|
|
|
|
|
if IS_V2: |
|
|
model_config = SettingsConfigDict(case_sensitive=True) |
|
|
else: |
|
|
class Config: |
|
|
case_sensitive = True |
|
|
|
|
|
|
|
|
@lru_cache |
|
|
def get_settings() -> Settings: |
|
|
|
|
|
j = _load_json(APPSETTINGS_PATH) |
|
|
|
|
|
|
|
|
merged = _merge_env_over_json(j) |
|
|
|
|
|
|
|
|
data = merged.model_dump() if hasattr(merged, "model_dump") else merged.dict() |
|
|
return Settings(**data) |
|
|
|
|
|
|
|
|
settings = get_settings() |
|
|
|