From 7cb811772b5ddbfd7ac7f42fd9eda100da62c09b Mon Sep 17 00:00:00 2001 From: salimM21 Date: Fri, 12 Sep 2025 08:27:47 +0100 Subject: [PATCH] =?UTF-8?q?Observabilit=C3=A9=20&=20Documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 + api.py | 202 --- api/__init__.py | 51 - api/__pycache__/__init__.cpython-312.pyc | Bin 3463 -> 0 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 2012 -> 1402 bytes .../api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 171 bytes .../app/api/__pycache__/deps.cpython-312.pyc | Bin 0 -> 1578 bytes .../app/api/__pycache__/users.cpython-312.pyc | Bin 0 -> 3878 bytes .../core/__pycache__/config.cpython-312.pyc | Bin 809 -> 835 bytes .../core/__pycache__/security.cpython-312.pyc | Bin 0 -> 2105 bytes backend/app/core/config.py | 13 +- backend/app/core/security.py | 52 +- .../db/__pycache__/session.cpython-312.pyc | Bin 1635 -> 1505 bytes backend/app/db/session.py | 26 +- backend/app/main.py | 90 +- .../conftest.cpython-312-pytest-8.3.2.pyc | Bin 864 -> 864 bytes .../itsdangerous-2.2.0.dist-info/INSTALLER | 1 + .../itsdangerous-2.2.0.dist-info/LICENSE.txt | 28 + .../itsdangerous-2.2.0.dist-info/METADATA | 60 + .../itsdangerous-2.2.0.dist-info/RECORD | 23 + .../itsdangerous-2.2.0.dist-info/REQUESTED | 0 .../itsdangerous-2.2.0.dist-info/WHEEL | 4 + .../site-packages/itsdangerous/__init__.py | 38 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1629 bytes .../__pycache__/_json.cpython-312.pyc | Bin 0 -> 1183 bytes .../__pycache__/encoding.cpython-312.pyc | Bin 0 -> 2683 bytes .../__pycache__/exc.cpython-312.pyc | Bin 0 -> 3943 bytes .../__pycache__/serializer.cpython-312.pyc | Bin 0 -> 15424 bytes .../__pycache__/signer.cpython-312.pyc | Bin 0 -> 11289 bytes .../__pycache__/timed.cpython-312.pyc | Bin 0 -> 8737 bytes .../__pycache__/url_safe.cpython-312.pyc | Bin 0 -> 3533 bytes venv/Lib/site-packages/itsdangerous/_json.py | 18 + .../site-packages/itsdangerous/encoding.py | 54 + venv/Lib/site-packages/itsdangerous/exc.py | 106 ++ venv/Lib/site-packages/itsdangerous/py.typed | 0 .../site-packages/itsdangerous/serializer.py | 406 ++++++ venv/Lib/site-packages/itsdangerous/signer.py | 266 ++++ venv/Lib/site-packages/itsdangerous/timed.py | 228 ++++ .../site-packages/itsdangerous/url_safe.py | 83 ++ .../INSTALLER | 1 + .../METADATA | 63 + .../pydantic_settings-2.10.1.dist-info/RECORD | 49 + .../REQUESTED | 0 .../pydantic_settings-2.10.1.dist-info/WHEEL | 4 + .../licenses/LICENSE | 21 + .../pydantic_settings/__init__.py | 63 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1359 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 0 -> 450 bytes .../__pycache__/main.cpython-312.pyc | Bin 0 -> 23483 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 1934 bytes .../__pycache__/version.cpython-312.pyc | Bin 0 -> 218 bytes .../pydantic_settings/exceptions.py | 4 + .../site-packages/pydantic_settings/main.py | 621 ++++++++++ .../site-packages/pydantic_settings/py.typed | 0 .../pydantic_settings/sources/__init__.py | 68 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1850 bytes .../sources/__pycache__/base.cpython-312.pyc | Bin 0 -> 23970 bytes .../sources/__pycache__/types.cpython-312.pyc | Bin 0 -> 2441 bytes .../sources/__pycache__/utils.cpython-312.pyc | Bin 0 -> 9354 bytes .../pydantic_settings/sources/base.py | 521 ++++++++ .../sources/providers/__init__.py | 41 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1181 bytes .../providers/__pycache__/aws.cpython-312.pyc | Bin 0 -> 3233 bytes .../__pycache__/azure.cpython-312.pyc | Bin 0 -> 6607 bytes .../providers/__pycache__/cli.cpython-312.pyc | Bin 0 -> 53916 bytes .../__pycache__/dotenv.cpython-312.pyc | Bin 0 -> 6847 bytes .../providers/__pycache__/env.cpython-312.pyc | Bin 0 -> 11288 bytes .../providers/__pycache__/gcp.cpython-312.pyc | Bin 0 -> 7172 bytes .../__pycache__/json.cpython-312.pyc | Bin 0 -> 2489 bytes .../__pycache__/pyproject.cpython-312.pyc | Bin 0 -> 3050 bytes .../__pycache__/secrets.cpython-312.pyc | Bin 0 -> 5799 bytes .../__pycache__/toml.cpython-312.pyc | Bin 0 -> 3054 bytes .../__pycache__/yaml.cpython-312.pyc | Bin 0 -> 3434 bytes .../sources/providers/aws.py | 77 ++ .../sources/providers/azure.py | 118 ++ .../sources/providers/cli.py | 1086 +++++++++++++++++ .../sources/providers/dotenv.py | 168 +++ .../sources/providers/env.py | 270 ++++ .../sources/providers/gcp.py | 152 +++ .../sources/providers/json.py | 47 + .../sources/providers/pyproject.py | 62 + .../sources/providers/secrets.py | 125 ++ .../sources/providers/toml.py | 66 + .../sources/providers/yaml.py | 75 ++ .../pydantic_settings/sources/types.py | 78 ++ .../pydantic_settings/sources/utils.py | 198 +++ .../site-packages/pydantic_settings/utils.py | 48 + .../pydantic_settings/version.py | 1 + 88 files changed, 5399 insertions(+), 382 deletions(-) delete mode 100644 api.py delete mode 100644 api/__init__.py delete mode 100644 api/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/deps.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/users.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/security.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER create mode 100644 venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt create mode 100644 venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA create mode 100644 venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD create mode 100644 venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED create mode 100644 venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL create mode 100644 venv/Lib/site-packages/itsdangerous/__init__.py create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/_json.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/encoding.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/exc.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/serializer.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/signer.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/timed.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/__pycache__/url_safe.cpython-312.pyc create mode 100644 venv/Lib/site-packages/itsdangerous/_json.py create mode 100644 venv/Lib/site-packages/itsdangerous/encoding.py create mode 100644 venv/Lib/site-packages/itsdangerous/exc.py create mode 100644 venv/Lib/site-packages/itsdangerous/py.typed create mode 100644 venv/Lib/site-packages/itsdangerous/serializer.py create mode 100644 venv/Lib/site-packages/itsdangerous/signer.py create mode 100644 venv/Lib/site-packages/itsdangerous/timed.py create mode 100644 venv/Lib/site-packages/itsdangerous/url_safe.py create mode 100644 venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/INSTALLER create mode 100644 venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/METADATA create mode 100644 venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/RECORD create mode 100644 venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/REQUESTED create mode 100644 venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/WHEEL create mode 100644 venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/licenses/LICENSE create mode 100644 venv/Lib/site-packages/pydantic_settings/__init__.py create mode 100644 venv/Lib/site-packages/pydantic_settings/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/__pycache__/exceptions.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/__pycache__/main.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/__pycache__/utils.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/__pycache__/version.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/exceptions.py create mode 100644 venv/Lib/site-packages/pydantic_settings/main.py create mode 100644 venv/Lib/site-packages/pydantic_settings/py.typed create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/__init__.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/__pycache__/base.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/__pycache__/types.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/__pycache__/utils.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/base.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__init__.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/__init__.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/aws.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/azure.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/cli.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/dotenv.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/env.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/gcp.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/json.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/pyproject.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/secrets.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/toml.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/yaml.cpython-312.pyc create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/aws.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/azure.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/cli.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/dotenv.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/env.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/gcp.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/json.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/pyproject.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/secrets.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/toml.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/providers/yaml.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/types.py create mode 100644 venv/Lib/site-packages/pydantic_settings/sources/utils.py create mode 100644 venv/Lib/site-packages/pydantic_settings/utils.py create mode 100644 venv/Lib/site-packages/pydantic_settings/version.py diff --git a/.vscode/settings.json b/.vscode/settings.json index deb21d509..74d7a9393 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,10 @@ "./backend/app/core", "./src/obesitrack/db", "./src/obesitrack" + ], + "cursorpyright.analysis.extraPaths": [ + "./backend/app/core", + "./src/obesitrack/db", + "./src/obesitrack" ] } \ No newline at end of file diff --git a/api.py b/api.py deleted file mode 100644 index e8cdbcc2a..000000000 --- a/api.py +++ /dev/null @@ -1,202 +0,0 @@ -from fastapi import FastAPI, Depends, Header, HTTPException, status -from fastapi.routing import APIRouter -from fastapi.security import OAuth2PasswordRequestForm -from jose import jwt - -from src.obesitrack.explainability import GLOBAL_EXPLAINER -from src.obesitrack.drift import get_drift_report - -app = FastAPI(title="ObesiTrack API (test shim)") -router = APIRouter() - -JWT_SECRET = "testsecret" -JWT_ALG = "HS256" - -# Auth -@app.post("/auth/token") -def login(form_data: OAuth2PasswordRequestForm = Depends()): - token = jwt.encode({"sub": form_data.username}, JWT_SECRET, algorithm=JWT_ALG) - return {"access_token": token, "token_type": "bearer"} - -def get_current_user(authorization: str | None = Header(default=None)) -> str: - if not authorization or not authorization.lower().startswith("bearer "): - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") - token = authorization.split(" ", 1)[1] - try: - payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG]) - username = payload.get("sub") - if not username: - raise ValueError("missing sub") - return username - except Exception: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") - -# Predict (minimal stub using in-memory fake model) -@router.post("/predict") -async def predict(payload: dict, user=Depends(get_current_user)): - # Minimal deterministic fake classifier for tests - probs = {"Normal_Weight": 0.7, "Overweight": 0.2, "Obesity": 0.1} - label = max(probs, key=probs.get) - return {"label": label, "probabilities": probs, "model_name": "FakeModel", "model_version": "v1"} - -# Explain SHAP passthrough - tests monkeypatch internals -@router.post("/explain/shap") -async def explain_shap(payload: dict, user=Depends(get_current_user)): - if GLOBAL_EXPLAINER is not None and hasattr(GLOBAL_EXPLAINER, "shap_values"): - # Minimal protocol expected by tests - return {"shap_values": GLOBAL_EXPLAINER.shap_values([payload])} - return {"shap_values": [[0.0]]} - -# Drift report - tests monkeypatch provider -@router.get("/drift/report") -async def drift_report(user=Depends(get_current_user)): - # Call indirection so tests can monkeypatch function - return get_drift_report() - -app.include_router(router) - - -import json -import os -import select -from fastapi import FastAPI, Depends, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse -from fastapi.templating import Jinja2Templates -from slowapi import Limiter -from starlette.middleware import Middleware -from starlette.middleware.sessions import SessionMiddleware -from starlette.middleware.gzip import GZipMiddleware -from slowapi import Limiter -from slowapi.util import get_remote_address -from slowapi.errors import RateLimitExceeded -from slowapi.middleware import SlowAPIMiddleware -import pandas as pd - -from drift import build_drift_report -from models_sqlalchemy import Prediction, User -from observability import init_tracing -from schemas import PredictIn, PredictOut, PredictRequest, PredictResponse -from security import get_current_user, token_endpoint -from .models import registry -from .logging_conf import setup_logging - -from fastapi import APIRouter, Depends, HTTPException -from .auth import get_current_user, require_admin -import pandas as pd -from .explain import ShapExplainerWrapper, _hash_payload - -from fastapi import APIRouter, Depends -from sqlmodel import select -from db.session import async_session -from db.models import User - -setup_logging() - -limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute", "1000/hour"]) # ajustez selon besoin - -middleware = [ -Middleware(GZipMiddleware, minimum_size=500), -Middleware(SessionMiddleware, secret_key="dummy"), -] - -app = FastAPI(title="ObesiTrack API", version="1.0.0", middleware=middleware) -app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) -app.state - -init_tracing(app, service_name="obesitrack", otlp_endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) - -router = APIRouter() -# assume global explainer is loaded at app startup -GLOBAL_EXPLAINER: ShapExplainerWrapper = None - -# -------------------------------------- -@router.get("/users") -async def list_users(): - async with async_session() as session: - result = await session.exec(select(User)) - users = result.all() - return users - - -# ---------------------------------------- -templates = Jinja2Templates(directory="src/obesitrack/templates") - -@app.get("/register", response_class=HTMLResponse) -def register_page(request: Request): - return templates.TemplateResponse("register.html", {"request": request}) - -@app.get("/login", response_class=HTMLResponse) -def login_page(request: Request): - return templates.TemplateResponse("login.html", {"request": request}) - -# ------------------------------- -@app.get("/predict-page", response_class=HTMLResponse) -def predict_page(request: Request, user=Depends(get_current_user)): - return templates.TemplateResponse("predict.html", {"request": request, "user": user}) - -@app.get("/history", response_class=HTMLResponse) -def history_page(request: Request, user=Depends(get_current_user)): - predictions = get_user_predictions(user.id) - return templates.TemplateResponse( - "history.html", - {"request": request, "user": user, "predictions": predictions} - ) -@app.get("/admin/users-page", response_class=HTMLResponse) -def admin_users_page(request: Request, admin=Depends(get_current_admin)): - users = get_all_users() - return templates.TemplateResponse( - "admin_users.html", - {"request": request, "admin": admin, "users": users} - ) -# ---------------------------------------- -@app.post("/predict", response_model=PredictOut) -async def predict(payload: PredictIn, user=Depends(get_current_user), db=Depends(get_db_session)): - df = pd.DataFrame([payload.model_dump()]) - bundle = registry.load() - probs = bundle.predict_proba(df)[0] - classes = list(bundle.classifier.classes_) - prob_map = {c: float(p) for c, p in zip(classes, probs)} - label = classes[int(probs.argmax())] - # persist - pred = Prediction( - user_id=user.id, - input_json=payload.model_dump(), - predicted_label=label, - probabilities=prob_map, - model_name=str(bundle.classifier.__class__.__name__), - model_version=getattr(bundle.classifier, "version", "v1") - ) - db.add(pred); db.commit() - return PredictOut(label=label, probabilities=prob_map, model_name=pred.model_name, model_version=pred.model_version) - -@router.post("/explain/shap") -async def explain_shap(payload: dict, user = Depends(get_current_user)): - # limit input size - if isinstance(payload, list): - if len(payload) > 4: - raise HTTPException(status_code=400, detail="Max 4 instances allowed") - else: - payload = [payload] - key = _hash_payload(json.dumps(payload, sort_keys=True)) - # caching can be implemented with redis for persistence - df = pd.DataFrame(payload) - try: - res = GLOBAL_EXPLAINER.explain(df) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - return res - -@router.get("/drift/report") -def drift_report(user = Depends(require_admin)): - # load baseline from file / bucket - baseline = pd.read_csv("data/baseline.csv") - # load last N predictions from DB (convert input_json -> dataframe) - preds = load_recent_predictions_from_db(limit=1000) # implement helper - current_df = pd.DataFrame([p["input_json"] for p in preds]) - report = build_drift_report(baseline, current_df) - return report.as_dict() # or return minimal summary - -# Planifier un job (cron) pour générer des rapports réguliers et alerter si drift détecté. -# Pour visualiser, Evidently fournit HTML export (report.save_html("drift_report.html")) — tu peux servir ce HTML via un endpoint sécurisé. -# report.save_html("drift_report.html") # Removed because 'report' is not defined in this scope diff --git a/api/__init__.py b/api/__init__.py deleted file mode 100644 index 9a4eabc82..000000000 --- a/api/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from fastapi import FastAPI, Depends, Header, HTTPException, status -from fastapi.routing import APIRouter -from fastapi.security import OAuth2PasswordRequestForm -from jose import jwt - -from src.obesitrack.explainability import GLOBAL_EXPLAINER -from src.obesitrack.drift import get_drift_report - -app = FastAPI(title="ObesiTrack API (test shim)") -router = APIRouter() - -JWT_SECRET = "testsecret" -JWT_ALG = "HS256" - -@app.post("/auth/token") -def login(form_data: OAuth2PasswordRequestForm = Depends()): - token = jwt.encode({"sub": form_data.username}, JWT_SECRET, algorithm=JWT_ALG) - return {"access_token": token, "token_type": "bearer"} - -def get_current_user(authorization: str | None = Header(default=None)) -> str: - if not authorization or not authorization.lower().startswith("bearer "): - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") - token = authorization.split(" ", 1)[1] - try: - payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG]) - username = payload.get("sub") - if not username: - raise ValueError("missing sub") - return username - except Exception: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") - -@router.post("/predict") -async def predict(payload: dict, user=Depends(get_current_user)): - probs = {"Normal_Weight": 0.7, "Overweight": 0.2, "Obesity": 0.1} - label = max(probs, key=probs.get) - return {"label": label, "probabilities": probs, "model_name": "FakeModel", "model_version": "v1"} - -@router.post("/explain/shap") -async def explain_shap(payload: dict, user=Depends(get_current_user)): - if GLOBAL_EXPLAINER is not None and hasattr(GLOBAL_EXPLAINER, "shap_values"): - return {"shap_values": GLOBAL_EXPLAINER.shap_values([payload])} - return {"shap_values": [[0.0]]} - -@router.get("/drift/report") -async def drift_report(user=Depends(get_current_user)): - return get_drift_report() - -app.include_router(router) - - diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index e885f1df48910079c82a3183b8afca6755e2aeaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3463 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 literal 2012 zcma)7O>7iZ9Di?SXJB;|_U3QC<$RsoG|9}6l-~Z#i z`8A)*V_LQnwo+4KI$(%pT9B#HXHL{cOjuNjgRYeTMOBt$XEkvtionL95nB|1TC*#;upe!{gJ z&0gExMY3t3GR=`eQfOfD-YG6A9U(@bzju>jm!#^oeb4Tamx%(_+n413u}*DiJw#G3 zWSxGFb>@YvmFHNOp0R#qiwt*=ouIdCN$SYR?yt~NSFJX9b5D8iUxF*+NWG|Y|6KK5 zAP2_AuUx%k5@OLCI-^2pQ=gdzFDIge9{9~j2{>gu%6g0v)9_8#;Y_q3N}L1n?AZ$r z@m$mKx#qb6)2u_niaNeIZ5p~z*Ql>qy4IjP<_P7Q@6J$%YwvwDQ7KDN!qIISWkFyt z(-TUO@}USC)F93~o9qJLv`nu1G+ zV}2e`8$fBE9z!Iy=~SX@wVzY)8N{lx)x~a-8ljr?N9$X6YITvD{_3Jg!+_)i%kQMs zRcHbVmx%yi0!8S%00xq99fw+guQEF{K1IRRO|2hRgEj=8LS$IAD;KU#bc)zmU1W5t zS{~NGllD{@I@R42Hb#D^hr2SX9I1>{MxddJZ~7KxSs}Qv!ER!H(;@7}Z zLMtx{JP9q)iSFx>3~G`3q=y4KA(MUQ(SYd~RzN5K4t&a@WOtPt&>2nm6xE5*vi(49 zq)yu|2`qYs9R?PDLx^keZOa(rhiLahbQHe-BoRJ#AC0y%Xs{HPK7ORgN>VU8!V>Q> z`@+)o4pYnzO0E1O4XFcR=GAbU9p;MR;J(LcMU`6e-`kX|)Kc3ygcA=?;T|f4i9%S| z84m3Vi$ik<;!it^aPcupVyRu|>CL@0f9m$BA8)N3T-mXP3csQAA(A^T=0@jF-#)#N zTy89X5{u6EMW-GV58NvrSeRb9v~o5UzTX!fpFeZ^%)-Hy^zyA(G}cZbN%^|;W$ByZ a+{L*QthP@6Iz^FR2&oF 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 literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 1fc9d249ef5d1e244e422d0bc2587259b333b384..6f01e8df1fbd9cbc666c38d8b385aa51fdad543f 100644 GIT binary patch delta 629 zcmXw$&ui0Q7{}lD&5x#S0^Qsk9i_q?NGMCdizAF-w}}Wcv2~aC60*E+yI`7Vk_}pf zb}*sfY4o6ff>KZZ1%emBi%<_G7xCy#aVNd_zG)lC^FANGd7kH+=Y1!ClC>|IRsig2 zTOUv2ORe1BX7CyiK%fsg5W^0_2m%D|1A;C95hxrByZ`_!AV7uj3;-6pqAva9q@z^b zb7I=0N#X_VI9*<3`(e=X+6SJSxbw2?5a;j*gFc^50fq!%bi`a>fr?ZjXor161T1GH zW+dSpRhTAcl*9Qeqj{DRg$EkWye^TG8H}Ba;jfcIwd*TFby49hV|L)%I^jdYWK5R~4b-G#m3M+5i39L<*%Y*W8e*()tN*iw< zd|vxjdVF4qt}i+r>QcJ&tV^6A@mxEeUsJ>fWHz#KsltUv`~ss@u4LtFEc>hMFq@}7 o-Hp~6LiWZtSokGC2)~2fD^Z3GIDByh`1!j8)RkA(xso;b57NV*B>(^b literal 809 zcmZ8f&ubGw6rS1LG@Fpvnlu+p5l<4(75vddDMFeqZD^Z>Yy!G04BPCqu1$91%vOka zP^?h(59qa5$)Dh#;6>;q40!V7O--SYlW#UDq7U|)@0<7L&6~IHOE#NEU>`0%KV8NM zeV5|W5;HP>0OSOD$irRK!#2i9K^w?Zj*zEPJXEJ~wxS@a3}=UIb)c0JKcp&Qky&#& zwP+Oi!9MTILgGc6iE$Ii38Kix9t{NZ|G;AtFskFZ2$(l4l5ce&}{*9vx!Ip5LXP&{*Cysa95; zHm8g`jf%5Q`9T!+9o}bt5Z!m`yOjGa=5`M5G+6kIb|PmN1PwgL?f0Ed$S8m;#OM#i z+_WZ^fxIdBcoN_Ujq=6!I?DlPivYD!ie;s^B4Hjts61j~o)9+(!pM#MFyMqRJ$6AI zOpaE3mR*%BPL-ETPF4(Zbogz6cW9I=oN0y8LjG-ibj$dv-8jvkX~w8fd|f@}XIk+> z!-=Ke2w<8>=7_u3A9!vM`5h*c#aV^UXQTGt#S?vX+TTa)8U)L~#+Lwosu<&M=;pbW W#7oDo&JirXawxO-apldl1%CjL!oh+7 diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fac3ae0d6454a90340a09f207152621b3f922eb4 GIT binary patch literal 2105 zcmahKOKcNIbY^#Luh))k5(uq;5Q#|OmWD#B0;PZ=W0gOHK(=rR0!9D)j(@14ocJ&~qHhU=F~T9I|;e)B)@sT**EX& zU%I*s1n}RJ+gh@V_#NIiu8}U_ z17sHof+PgL9W+P+(;kGt-Z!q3?%S?pi1ax2J6amJwG(c2R|DbRzonb7Ds+;ushurQ z;gy)syqyucB&V$+4J$&;P1!=fcr6iS%wl0I{1H|y*%7N~(~1qe&Z%u@ixXV9$4sdb zTBdAUaMi=jNm`&>7=)G>HNg`Rfl@w`Ev8Eu&ZjI!L~t_0Cn?z!xEZy}tZ23q;iLnS zk5u@r4FZ}+wj-1%C#BnfzsmqbnIm&O^Rw>0xEL*fcN{x(Bo=vcfdGmwI%*s*IPK^OWizJmPl~Sg~ zXo^#_%(8ZMuv8VnmYkn{sn&NnBv`JcN<$OsTZY`8Vx5)C1hPISry)s%(#%N|Z(QmS)TP zJCF-?sLivcZTYgy;v_FI}>pf)?zRzo_2=9D%%SNY#$(@Li#}G&EzL6 zXr9SJ*vI5Op%$$vCi5rMvTZV%8zw^+dxj#BXgr=yj9iMw($VXeFN{Xh!xvVW)6 zi4$&GG%Z4f5syYjqlxsT=ndf=`r`AE(F=+5!(rE^d)C*ocS;CTlBwpc4B^skzl2tY z^q7po@TN1SNjXp37W6%PA8ce_@vi_upBUYX_v=(VaGQePp(r`CI$ZnSgvl6LpYhVSG0?p574Ys?sP#~079>^;)hd*sQ9l~cow zQ^U{o*fNee9Vc9l(>JDKqqpYZ0rSZFmhP&(rN6dxpAH|~1``-R5{@6JwWZH)XsnF z&YSuF5Uk{^^0R>IsHtd5VAUU-y*P7muDm$8(s#7ccl1eR8oGr}Rz=*7eT{7AfcH8Fms>a*gmifZ+KUzeL9x=-5lt_d7cD7aI7(=v~qmrXP+! zH%`|*O*h6TmLg3A$D;$z@pNEKa!noM{R@s}|58MX_P;V*cmyxRn@FB_)z bool: - return pwd_ctx.verify(plain_password, hashed_password) + return pwd_ctx.verify(plain_password, hashed_password) + def hash_password(password: str) -> str: - return pwd_ctx.hash(password) + return pwd_ctx.hash(password) + # ----------------- JWT ----------------- def create_access_token(sub: str, role: str, expires_minutes: int | None = None) -> str: - expire = datetime.now(timezone.utc) + timedelta( - minutes=expires_minutes or settings.ACCESS_TOKEN_EXPIRE_MINUTES - ) - payload = {"sub": sub, "role": role, "exp": expire} - return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) - -# ----------------- Dépendances FastAPI ----------------- -def get_current_user(token: str = Depends(oauth2), db=Depends(get_db_session)) -> User: - credentials_exception = HTTPException(status_code=401, detail="Could not validate credentials") - try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - username = payload.get("sub") - if username is None: - raise credentials_exception - - user = db.query(User).filter_by(email=username).first() - if not user: - raise credentials_exception - return user - except JWTError: - raise credentials_exception - -def require_admin(user: User = Depends(get_current_user)) -> User: - if user.role != "admin": - raise HTTPException(status_code=403, detail="Admin privilege required") - return user + expire = datetime.now(timezone.utc) + timedelta( + minutes=expires_minutes or settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + payload = {"sub": sub, "role": role, "exp": expire} + return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + + +def decode_access_token(token: str) -> dict | None: + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + return payload + except JWTError: + return None diff --git a/backend/app/db/__pycache__/session.cpython-312.pyc b/backend/app/db/__pycache__/session.cpython-312.pyc index d3ea62ca2efde9c8308611f8933337806b18145d..14b59fdca4df062e6e3104cc1fc17b9da14526e2 100644 GIT binary patch literal 1505 zcmZWpUuYaf7@ygHw|BYAB{`ckF<`YR?bUc#n`@{<<vnu>#8TSslPav6~l~4 zi4hRpV0F*biC4Bg5_S~0b*^P3BV&p%X8T@N2$QS@U1Ofmu*)K*V^AZoKOlO^U?ht} zm60H@y)wI;sroD^Q^MYNUaYdZ=~v6IT<+ONMro_%H>Y7?iI{VKsLa-a8YQ99+Dldn z6-dCC9(Iw%s!fURd%Eent{sGNqZatHPL0il2@YGdx%;`%#0$F&eLDdM{x-r;+~A47 zvU`+I2mtNF8bQn0LLWlJSHgAiQ&~n=KoTGT5W^LW(Gse}_rKS|K_oGbE6Jz5mCmQR zwh)#v1Bg&A5zs)W7}aXdD$#DvskG+lToR!)4kZp% zmjs4o1V)%>%g1qe1x?@cqE{GS{LrDtZ=HFyIKv2K#mO_pvxLnDeznM|)b@gt#dk`C z+0)c8=U=P3tRiGDTBYKy)N<8&7_&V)&|yaduyO`E<8}TM?KIGD=>yl6 zt}cE4(IX_tr?&X;?K5}P=QlfhuDMs;8)t8x|N8vK;KYGZM#r9Y8l4Z|_#h+EH*#UK~m2-~MzniOh@ zD8I{L=e{tXx>VsFF77bRD`onWz~If=WvX&EdA2ZJ7%xnn(`VkD2oo$YXuuZiU@nYN zQgsZIw91{3CLZ)=zZ?E(=I4<+y^{^)vyOE%`JiiXQ%Za}_T!P;`PCWiZ&BzL X8mWgWQV*`j`Ww==B;r$a2(teNRJ(bo literal 1635 zcmah}UuYaf7@yg_+uQqpNt)C@r%V$yazYR0;W* zH^Qb+5qv*nL`<FY$ z36ZoG9RLV=4^^BFC14OKd{B-qY{R6hMXo0!L5cH*_-NO@Q;j+92q^59r$ zd~9-bNS+uj4iA>|oY$pNqPRp>oCQmji8ZBL#EVM|4f_DrNI1lCblWnO8A2JJ2yoF> z6$7y75ZBeMDTn3!sjULGL5Qf+wkKSkb!RE@gkXn@kPuWuaU9vpkoksAiELZ4YMZ9+ zs=+@8HUh&k^xeO~yoxZ;;0776_15w12f$ftf#=aO*3er}@iBLke6O+A{a=FTiL4okQ(tBN}Ji@Mz(k-RoWCwKZ>OXJSz^S{NYd8+Y(EA zJAQaCh#$^Aa^8RquRbolgg?R3ap6-Qyw6TaC;4UqOF5yL3^BhW4&i1NOMRi{Qv&mI zn8o`r^G^!W`AD;Xr89}<=?L>*j6ht4mG>L4gL@s*^)vz4`v~pT4AeHv8xG+V0I-Ug34c*Cu3FwG+ z-*|fI!VbrWyS7r9cg0`D*s8c9z8~AW&IQgR=Ll3^jrZdZLcQptj(+?_=L`Hdd_QQe z%ZoT~8oEo)1~Tgbq!{WnF&7GC-Yxh~hi+4LOqAU(FLkKGrY2<<%tXq zoz@gkqW@EA&{sjVDFI_$)~6`DiLh_*(9kGDi6N;hU63Y*<;mBJ!97UJvSL}btGMty zIkM~}gLkYwk4clV1G$BE!6N^p9*P5|t<4(bWqJZ)j7et#3_ zM!9WtY)3$u$2QZsjdX4)ajz%;<+I;41ODuXrv diff --git a/backend/app/db/session.py b/backend/app/db/session.py index a6011b70e..b9e0dd8eb 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -1,26 +1,22 @@ -from sqlite3 import SQLITE_LIMIT_COMPOUND_SELECT -from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine -from sqlalchemy.orm import sessionmaker -from obesitdb.database import SessionLocal +from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession +from sqlalchemy.orm import sessionmaker, declarative_base from app.core.config import settings -from session import AsyncSession DATABASE_URL = settings.DATABASE_URL +# Ensure async driver for PostgreSQL if using psycopg2 +if DATABASE_URL.startswith("postgresql+psycopg2"): + DATABASE_URL = DATABASE_URL.replace("postgresql+psycopg2", "postgresql+asyncpg") -engine: AsyncEngine = create_async_engine(DATABASE_URL, echo=True, future=True) +engine: AsyncEngine = create_async_engine(DATABASE_URL, echo=False, future=True) async_session = sessionmaker( - engine, class_=AsyncSession, expire_on_commit=False + engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, autocommit=False ) +Base = declarative_base() + async def init_db(): async with engine.begin() as conn: - await conn.run_sync(SQLITE_LIMIT_COMPOUND_SELECT.metadata.create_all) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() \ No newline at end of file + from app.db import models # noqa: F401 - ensure models are imported for metadata + await conn.run_sync(Base.metadata.create_all) \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index 526e3c368..bcf322ba1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,93 +1,37 @@ from fastapi import FastAPI from starlette.middleware import Middleware from starlette.middleware.gzip import GZipMiddleware -from starlette.middleware.sessions import SessionMiddleware from fastapi.middleware.cors import CORSMiddleware -from api import predict -from backend.app.api import users -import drift -import explain -from logging_conf import setup_logging -from observability import init_tracing +from app.api import users, predictions, auth, metrics -setup_logging() +# Optional logging/observability setup (disabled if modules missing) +# from logging_conf import setup_logging +# from observability import init_tracing +# setup_logging() middleware = [ Middleware(GZipMiddleware, minimum_size=500), - Middleware(SessionMiddleware, secret_key="dummy"), ] app = FastAPI(title="ObesiTrack API", version="1.0.0", middleware=middleware) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) -# init observability -init_tracing(app, service_name="obesitrack") +# try: +# init_tracing(app, service_name="obesitrack") +# except Exception: +# pass -# inclure les routers -app.include_router(predict.router) -app.include_router(explain.router) -app.include_router(drift.router) +# Include routers +app.include_router(auth.router, prefix="/auth") app.include_router(users.router) +app.include_router(predictions.router) +app.include_router(metrics.router) - -# from fastapi import FastAPI -# from fastapi.middleware.cors import CORSMiddleware -# from sklearn import metrics - -# # Import des routes -# from app.api import auth, users, predictions, metrics - -# # Définir les métadonnées pour la doc Swagger -# tags_metadata = [ -# { -# "name": "Auth", -# "description": "Endpoints pour l'authentification et la gestion des tokens JWT.", -# }, -# { -# "name": "Users", -# "description": "Gestion des utilisateurs (listes, rôles, suppression). Accessible uniquement par l'admin.", -# }, -# { -# "name": "Predictions", -# "description": "Prédiction de l’obésité et consultation de l’historique des prédictions personnelles.", -# }, -# ] - -# # Créer l'application FastAPI -# app = FastAPI( -# title="Obesity Prediction API", -# description="API REST pour gérer l'authentification, les utilisateurs et la prédiction de l'obésité.", -# version="1.0.0", -# openapi_tags=tags_metadata, -# ) - -# # Configurer CORS (frontend peut appeler l’API sans blocage) -# origins = [ -# "http://localhost:5500", # si tu ouvres le frontend avec live server -# "http://127.0.0.1:5500", -# "http://localhost:8000", -# "http://127.0.0.1:8000", -# ] -# app.add_middleware( -# CORSMiddleware, -# allow_origins=origins, # tu peux mettre ["*"] en dev -# allow_credentials=True, -# allow_methods=["*"], -# allow_headers=["*"], -# ) - -# # Inclure les routes -# app.include_router(auth.router, prefix="/api/auth", tags=["Auth"]) -# app.include_router(users.router, prefix="/api", tags=["Users"]) # users router already has /users -# app.include_router(predictions.router, prefix="/api", tags=["Predictions"]) # predictions router has /predictions -# app.include_router(metrics.router, prefix="/api", tags=["Metrics"]) # exposes /api/metrics - - -# # Endpoint de test (healthcheck) -# @app.get("/", tags=["Root"]) -# def read_root(): -# return {"message": "Bienvenue sur l'API de Prédiction de l'Obésité 🚀"} +# Healthcheck +@app.get("/", tags=["Root"]) +def read_root(): + return {"message": "Bienvenue sur l'API ObesiTrack 🚀"} diff --git a/tests/__pycache__/conftest.cpython-312-pytest-8.3.2.pyc b/tests/__pycache__/conftest.cpython-312-pytest-8.3.2.pyc index b161483fd04470d6f2cb1357ca3bede096ae6eab..e12aae91e6a816507bc017a397f160b21e099eed 100644 GIT binary patch delta 20 acmaFB_JEE1G%qg~0}z<$9oWbn$_xNHcm)3d delta 20 acmaFB_JEE1G%qg~0}w>C@7c&5$_xNKXa#lv diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt new file mode 100644 index 000000000..7b190ca67 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2011 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA new file mode 100644 index 000000000..ddf546484 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.1 +Name: itsdangerous +Version: 2.2.0 +Summary: Safely pass data to untrusted environments and back. +Maintainer-email: Pallets +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Changes, https://itsdangerous.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://itsdangerous.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/itsdangerous/ + +# ItsDangerous + +... so better sign this + +Various helpers to pass data to untrusted environments and to get it +back safe and sound. Data is cryptographically signed to ensure that a +token has not been tampered with. + +It's possible to customize how data is serialized. Data is compressed as +needed. A timestamp can be added and verified automatically while +loading a token. + + +## A Simple Example + +Here's how you could generate a token for transmitting a user's id and +name between web requests. + +```python +from itsdangerous import URLSafeSerializer +auth_s = URLSafeSerializer("secret key", "auth") +token = auth_s.dumps({"id": 5, "name": "itsdangerous"}) + +print(token) +# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg + +data = auth_s.loads(token) +print(data["name"]) +# itsdangerous +``` + + +## Donate + +The Pallets organization develops and supports ItsDangerous and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +[please donate today][]. + +[please donate today]: https://palletsprojects.com/donate + diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD new file mode 100644 index 000000000..b73b08a48 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/RECORD @@ -0,0 +1,23 @@ +itsdangerous-2.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +itsdangerous-2.2.0.dist-info/LICENSE.txt,sha256=Y68JiRtr6K0aQlLtQ68PTvun_JSOIoNnvtfzxa4LCdc,1475 +itsdangerous-2.2.0.dist-info/METADATA,sha256=0rk0-1ZwihuU5DnwJVwPWoEI4yWOyCexih3JyZHblhE,1924 +itsdangerous-2.2.0.dist-info/RECORD,, +itsdangerous-2.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +itsdangerous-2.2.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +itsdangerous/__init__.py,sha256=4SK75sCe29xbRgQE1ZQtMHnKUuZYAf3bSpZOrff1IAY,1427 +itsdangerous/__pycache__/__init__.cpython-312.pyc,, +itsdangerous/__pycache__/_json.cpython-312.pyc,, +itsdangerous/__pycache__/encoding.cpython-312.pyc,, +itsdangerous/__pycache__/exc.cpython-312.pyc,, +itsdangerous/__pycache__/serializer.cpython-312.pyc,, +itsdangerous/__pycache__/signer.cpython-312.pyc,, +itsdangerous/__pycache__/timed.cpython-312.pyc,, +itsdangerous/__pycache__/url_safe.cpython-312.pyc,, +itsdangerous/_json.py,sha256=wPQGmge2yZ9328EHKF6gadGeyGYCJQKxtU-iLKE6UnA,473 +itsdangerous/encoding.py,sha256=wwTz5q_3zLcaAdunk6_vSoStwGqYWe307Zl_U87aRFM,1409 +itsdangerous/exc.py,sha256=Rr3exo0MRFEcPZltwecyK16VV1bE2K9_F1-d-ljcUn4,3201 +itsdangerous/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +itsdangerous/serializer.py,sha256=PmdwADLqkSyQLZ0jOKAgDsAW4k_H0TlA71Ei3z0C5aI,15601 +itsdangerous/signer.py,sha256=YO0CV7NBvHA6j549REHJFUjUojw2pHqwcUpQnU7yNYQ,9647 +itsdangerous/timed.py,sha256=6RvDMqNumGMxf0-HlpaZdN9PUQQmRvrQGplKhxuivUs,8083 +itsdangerous/url_safe.py,sha256=az4e5fXi_vs-YbWj8YZwn4wiVKfeD--GEKRT5Ueu4P4,2505 diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL new file mode 100644 index 000000000..3b5e64b5e --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous-2.2.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/Lib/site-packages/itsdangerous/__init__.py b/venv/Lib/site-packages/itsdangerous/__init__.py new file mode 100644 index 000000000..ea55256eb --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/__init__.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import typing as t + +from .encoding import base64_decode as base64_decode +from .encoding import base64_encode as base64_encode +from .encoding import want_bytes as want_bytes +from .exc import BadData as BadData +from .exc import BadHeader as BadHeader +from .exc import BadPayload as BadPayload +from .exc import BadSignature as BadSignature +from .exc import BadTimeSignature as BadTimeSignature +from .exc import SignatureExpired as SignatureExpired +from .serializer import Serializer as Serializer +from .signer import HMACAlgorithm as HMACAlgorithm +from .signer import NoneAlgorithm as NoneAlgorithm +from .signer import Signer as Signer +from .timed import TimedSerializer as TimedSerializer +from .timed import TimestampSigner as TimestampSigner +from .url_safe import URLSafeSerializer as URLSafeSerializer +from .url_safe import URLSafeTimedSerializer as URLSafeTimedSerializer + + +def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " ItsDangerous 2.3. Use feature detection or" + " 'importlib.metadata.version(\"itsdangerous\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("itsdangerous") + + raise AttributeError(name) diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd917e2d8c542ec07d4041de18ab36dfedc91ab4 GIT binary patch literal 1629 zcmZXU&u<$=6vt=%W4&H`ZO8dts&*3!=^kRBDM*D1DI|p=l@O6sNGprg*fVi9*&o)- zIH{vVLWl#LIB;mM+(6~VKfs+!OAoEKvV>GPMB)%BIrYSwi5)p*w3_|A@65h=Gw;p* zVOb`E_S-wZZ~mA?=!G-}Ep-ao@xK7~5kW)?P^h_@COH+P+%!p(Oppn)ZWgpO$pY)H zu2=`oxjDtTzzFkhUas-axKM0;F4QXYz5`8;#L%wf|0Q5 zRuz|lN8M4y72q*6i$vjzf<`hvNOBjGi=f^rzneT2y}78o!O zc$;EE85kxQtGFA)n1EFTYt3&(Sag_lS^<0A59vAW%(!*0?K3LBGuJ5dap3PMpVsZq z7MB)-R?K{{8N#r&5=V4c(4`MGw=CV0bDt}T3mmr3W{uqa;?5f0py%dI^u<>^H022m z8k7@CBcvTa0Jx9#p?tQlBE&u*DS`rYpsBZZklIge$+@pIk{0?vBAH*a55bpg8rs*k zj8h)&w+h47DHHu+KcY1xtikv`8mQlBPHrVBc-{_W9CGY=Ocwp#PwSi1o*Rk~M81I1QNA{zRRh-K~;9) zdqJDBsO*^8bAH&4nF#zwEu;bysFYeio~yII;AG%u)|ms>TtNNQ4rQD&`;e^Wc0hM% zkQ9d}JGrF#f5(Zji&^AHEuLf~CDrBrRv?%!Plpm?5Jr;g$B~p42iw2JSj?Psl8JCg zll<8d>>>ojWsYpye)-kX`*qkb-~}eBpVuc2}mX1!o08LCW5MMW^KIYtVg-p5+SP z9sHTQ_}rLzZcP4VRDQ6&w~kUN>l5unN7m(MlT$|pl%F_3dfq%Lq002r^33D%%+s>- zxa|B~ds4pf$hh&hIrU)diFxT!=F%~^MNWpz!YfI0!4jQOxTVovR@4@wu5&RlJ#V8U zFD`^5&mhr7w++XfXd*GxZvhM@tDMQAW;x}G-fJ>h$t*8B`6Fhsh*?o~vRom(XP29m zq@Z(kds$gdsmN|b%2^?=GOJ2qR0>9i1s;cM%f_TOE<1TnzaaLe+sc zwg9HQDSQdKqqL@JJv93QRsTWPd-AFE(Ca-k*F#PZ&G*o|>Uq0|uBqqBK)KmNAD`$Y I?Xp_rKd{NgcmMzZ literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/_json.cpython-312.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/_json.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6349ded2c60a8236f7f40855dc2e171594a13014 GIT binary patch literal 1183 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 C+I;i? literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/serializer.cpython-312.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/serializer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7c95962424921d5978287200158f42d24bfa907 GIT binary patch literal 15424 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 Z>2)`rYo5jp51xN+de6nfHdD?B`fv9ar{Vwr literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/timed.cpython-312.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/timed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ea51c7c0c1f947d321c190dc429a091ae682195 GIT binary patch literal 8737 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 H>-&EIYnx}C literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/itsdangerous/__pycache__/url_safe.cpython-312.pyc b/venv/Lib/site-packages/itsdangerous/__pycache__/url_safe.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..941de554bf7e7352da90647d398d1d5e980c0d9f GIT binary patch literal 3533 zcmbtWU2Ggz6~41Gv;RN-+qIp<_%=?}Y)Ce=NrP|#)Uj!3)i@DO6xcc)k9V%^v1fOd zJF|7XTGI#xu@M3_4=AoEl7)Ck6_A49fhUw8@qp-qQzKywi4@fG(6>$PiYR^IoIAU= zV@eTly}onqIp?0abI$qBJ%36hq6EgTpS?Zv#|RL5qci6F`n!yUT!L~72 zsmX|*)Qz+Hf@SLlh}*QYBf6`PF>dOXS!En}(I-}^b7qklTdabAa(`|fR9gQgtiKPA zXCyBfBp)~r0m#ct$*V?y1r7NG$PL9%-0O zQrD(c%bbi-7zJ=1XO2VN8ActKo26-#7xg)Y`cAIR3v`mvvZWXNbL$j7Eho%W(>sIlDjeye*I}?Jy<)lA z)<8JEix~C=I+pc z+MNxLzPOHF$ZutuAK^}!?pTlSFyF)Zj6hf#ZqLSWgY0xKghAuLIo zo6}}Vw=~AN&8tIO<}mFj8rzn1TL!0W&VtV`I_`7Z zKdKuPo!`Oq=rU;}NoIFF`XJWvmssE3Sl^QI=h(nnrf*4KntXGverm0^ul{mF2}aWQ zI_YxvYG<}S@-Ru_nMNR(=)Kpy`}!N#-dJ{5yAQspY$Qor`s)1M)WGt{N^0=N(f3k= zw~pOS4X%U-A8quLw%(730){&FthJq9>!%y4+?i|yNmsfNk`w7q8Zt;f+2|l$eV_=| zGjy%%!rH(-Q0(pi#hzYJZ0|+IMDJrK2cPiM*ia|AnI3wM-06%A9hUFxNe&&5?;KE( zelBt{Dc?DsJQ53wpfW{smO*n-nyP_PtNi_q!Om1we*Uh3VgB-uBac5fq)*V1mrMm+JC!Wq38UzpkwH^wksh z(!JL&Uc0z_bTz%d9{nJetXF^V)wc`F`+j2Hc3ZmIv7WWeuB8jtO7&9@Qki;k9o_(!Am0TKPn}$FnZtJj{aAH#L!||Q;c8sav_?nm z65~*33I9Cm2RU;qyd)M`g+JXA@PkMlfC;YGu%?ytBGWW4s%b^rs8~qHHSOCK-D*lg znr7GqP2-p}JPDJR(zI~ZVS4mE z@-QH);YO4^`{flSv(j^DRXNd!lYRRiMn0z=f6$(3sPF{jv;oAB3X}9x4V1y2&YngL zsW^$IK28Ajj~Aq*+WnBg#IvopJ<+{1?>=e04a8f$3wAEhc*$P-zf_;g=;k%1!(3>$Hj^dR|$3@ zl`@p==H_|E3((Ke!i)~J6gv@LNtn^C@{B&oT&g>C9^8!2iHT>lXC@{NB5?=^VF&0O z(0-FK`i{a6p*OJH@WVKL0j76|?<0zgqpj)dK&&G^u8@x~G`=qy1h?tW44JQ;cparF!MbG(~5_F|suV!zI@=z22r zj{; t.Any: + return _json.loads(payload) + + @staticmethod + def dumps(obj: t.Any, **kwargs: t.Any) -> str: + kwargs.setdefault("ensure_ascii", False) + kwargs.setdefault("separators", (",", ":")) + return _json.dumps(obj, **kwargs) diff --git a/venv/Lib/site-packages/itsdangerous/encoding.py b/venv/Lib/site-packages/itsdangerous/encoding.py new file mode 100644 index 000000000..f5ca80f90 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/encoding.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import base64 +import string +import struct +import typing as t + +from .exc import BadData + + +def want_bytes( + s: str | bytes, encoding: str = "utf-8", errors: str = "strict" +) -> bytes: + if isinstance(s, str): + s = s.encode(encoding, errors) + + return s + + +def base64_encode(string: str | bytes) -> bytes: + """Base64 encode a string of bytes or text. The resulting bytes are + safe to use in URLs. + """ + string = want_bytes(string) + return base64.urlsafe_b64encode(string).rstrip(b"=") + + +def base64_decode(string: str | bytes) -> bytes: + """Base64 decode a URL-safe string of bytes or text. The result is + bytes. + """ + string = want_bytes(string, encoding="ascii", errors="ignore") + string += b"=" * (-len(string) % 4) + + try: + return base64.urlsafe_b64decode(string) + except (TypeError, ValueError) as e: + raise BadData("Invalid base64-encoded data") from e + + +# The alphabet used by base64.urlsafe_* +_base64_alphabet = f"{string.ascii_letters}{string.digits}-_=".encode("ascii") + +_int64_struct = struct.Struct(">Q") +_int_to_bytes = _int64_struct.pack +_bytes_to_int = t.cast("t.Callable[[bytes], tuple[int]]", _int64_struct.unpack) + + +def int_to_bytes(num: int) -> bytes: + return _int_to_bytes(num).lstrip(b"\x00") + + +def bytes_to_int(bytestr: bytes) -> int: + return _bytes_to_int(bytestr.rjust(8, b"\x00"))[0] diff --git a/venv/Lib/site-packages/itsdangerous/exc.py b/venv/Lib/site-packages/itsdangerous/exc.py new file mode 100644 index 000000000..a75adcd52 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/exc.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import typing as t +from datetime import datetime + + +class BadData(Exception): + """Raised if bad data of any sort was encountered. This is the base + for all exceptions that ItsDangerous defines. + + .. versionadded:: 0.15 + """ + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self) -> str: + return self.message + + +class BadSignature(BadData): + """Raised if a signature does not match.""" + + def __init__(self, message: str, payload: t.Any | None = None): + super().__init__(message) + + #: The payload that failed the signature test. In some + #: situations you might still want to inspect this, even if + #: you know it was tampered with. + #: + #: .. versionadded:: 0.14 + self.payload: t.Any | None = payload + + +class BadTimeSignature(BadSignature): + """Raised if a time-based signature is invalid. This is a subclass + of :class:`BadSignature`. + """ + + def __init__( + self, + message: str, + payload: t.Any | None = None, + date_signed: datetime | None = None, + ): + super().__init__(message, payload) + + #: If the signature expired this exposes the date of when the + #: signature was created. This can be helpful in order to + #: tell the user how long a link has been gone stale. + #: + #: .. versionchanged:: 2.0 + #: The datetime value is timezone-aware rather than naive. + #: + #: .. versionadded:: 0.14 + self.date_signed = date_signed + + +class SignatureExpired(BadTimeSignature): + """Raised if a signature timestamp is older than ``max_age``. This + is a subclass of :exc:`BadTimeSignature`. + """ + + +class BadHeader(BadSignature): + """Raised if a signed header is invalid in some form. This only + happens for serializers that have a header that goes with the + signature. + + .. versionadded:: 0.24 + """ + + def __init__( + self, + message: str, + payload: t.Any | None = None, + header: t.Any | None = None, + original_error: Exception | None = None, + ): + super().__init__(message, payload) + + #: If the header is actually available but just malformed it + #: might be stored here. + self.header: t.Any | None = header + + #: If available, the error that indicates why the payload was + #: not valid. This might be ``None``. + self.original_error: Exception | None = original_error + + +class BadPayload(BadData): + """Raised if a payload is invalid. This could happen if the payload + is loaded despite an invalid signature, or if there is a mismatch + between the serializer and deserializer. The original exception + that occurred during loading is stored on as :attr:`original_error`. + + .. versionadded:: 0.15 + """ + + def __init__(self, message: str, original_error: Exception | None = None): + super().__init__(message) + + #: If available, the error that indicates why the payload was + #: not valid. This might be ``None``. + self.original_error: Exception | None = original_error diff --git a/venv/Lib/site-packages/itsdangerous/py.typed b/venv/Lib/site-packages/itsdangerous/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/venv/Lib/site-packages/itsdangerous/serializer.py b/venv/Lib/site-packages/itsdangerous/serializer.py new file mode 100644 index 000000000..5ddf3871d --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/serializer.py @@ -0,0 +1,406 @@ +from __future__ import annotations + +import collections.abc as cabc +import json +import typing as t + +from .encoding import want_bytes +from .exc import BadPayload +from .exc import BadSignature +from .signer import _make_keys_list +from .signer import Signer + +if t.TYPE_CHECKING: + import typing_extensions as te + + # This should be either be str or bytes. To avoid having to specify the + # bound type, it falls back to a union if structural matching fails. + _TSerialized = te.TypeVar( + "_TSerialized", bound=t.Union[str, bytes], default=t.Union[str, bytes] + ) +else: + # Still available at runtime on Python < 3.13, but without the default. + _TSerialized = t.TypeVar("_TSerialized", bound=t.Union[str, bytes]) + + +class _PDataSerializer(t.Protocol[_TSerialized]): + def loads(self, payload: _TSerialized, /) -> t.Any: ... + # A signature with additional arguments is not handled correctly by type + # checkers right now, so an overload is used below for serializers that + # don't match this strict protocol. + def dumps(self, obj: t.Any, /) -> _TSerialized: ... + + +# Use TypeIs once it's available in typing_extensions or 3.13. +def is_text_serializer( + serializer: _PDataSerializer[t.Any], +) -> te.TypeGuard[_PDataSerializer[str]]: + """Checks whether a serializer generates text or binary.""" + return isinstance(serializer.dumps({}), str) + + +class Serializer(t.Generic[_TSerialized]): + """A serializer wraps a :class:`~itsdangerous.signer.Signer` to + enable serializing and securely signing data other than bytes. It + can unsign to verify that the data hasn't been changed. + + The serializer provides :meth:`dumps` and :meth:`loads`, similar to + :mod:`json`, and by default uses :mod:`json` internally to serialize + the data to bytes. + + The secret key should be a random string of ``bytes`` and should not + be saved to code or version control. Different salts should be used + to distinguish signing in different contexts. See :doc:`/concepts` + for information about the security of the secret key and salt. + + :param secret_key: The secret key to sign and verify with. Can be a + list of keys, oldest to newest, to support key rotation. + :param salt: Extra key to combine with ``secret_key`` to distinguish + signatures in different contexts. + :param serializer: An object that provides ``dumps`` and ``loads`` + methods for serializing data to a string. Defaults to + :attr:`default_serializer`, which defaults to :mod:`json`. + :param serializer_kwargs: Keyword arguments to pass when calling + ``serializer.dumps``. + :param signer: A ``Signer`` class to instantiate when signing data. + Defaults to :attr:`default_signer`, which defaults to + :class:`~itsdangerous.signer.Signer`. + :param signer_kwargs: Keyword arguments to pass when instantiating + the ``Signer`` class. + :param fallback_signers: List of signer parameters to try when + unsigning with the default signer fails. Each item can be a dict + of ``signer_kwargs``, a ``Signer`` class, or a tuple of + ``(signer, signer_kwargs)``. Defaults to + :attr:`default_fallback_signers`. + + .. versionchanged:: 2.0 + Added support for key rotation by passing a list to + ``secret_key``. + + .. versionchanged:: 2.0 + Removed the default SHA-512 fallback signer from + ``default_fallback_signers``. + + .. versionchanged:: 1.1 + Added support for ``fallback_signers`` and configured a default + SHA-512 fallback. This fallback is for users who used the yanked + 1.0.0 release which defaulted to SHA-512. + + .. versionchanged:: 0.14 + The ``signer`` and ``signer_kwargs`` parameters were added to + the constructor. + """ + + #: The default serialization module to use to serialize data to a + #: string internally. The default is :mod:`json`, but can be changed + #: to any object that provides ``dumps`` and ``loads`` methods. + default_serializer: _PDataSerializer[t.Any] = json + + #: The default ``Signer`` class to instantiate when signing data. + #: The default is :class:`itsdangerous.signer.Signer`. + default_signer: type[Signer] = Signer + + #: The default fallback signers to try when unsigning fails. + default_fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] = [] + + # Serializer[str] if no data serializer is provided, or if it returns str. + @t.overload + def __init__( + self: Serializer[str], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + serializer: None | _PDataSerializer[str] = None, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Serializer[bytes] with a bytes data serializer positional argument. + @t.overload + def __init__( + self: Serializer[bytes], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None, + serializer: _PDataSerializer[bytes], + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Serializer[bytes] with a bytes data serializer keyword argument. + @t.overload + def __init__( + self: Serializer[bytes], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + *, + serializer: _PDataSerializer[bytes], + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Fall back with a positional argument. If the strict signature of + # _PDataSerializer doesn't match, fall back to a union, requiring the user + # to specify the type. + @t.overload + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None, + serializer: t.Any, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Fall back with a keyword argument. + @t.overload + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + *, + serializer: t.Any, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + serializer: t.Any | None = None, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): + #: The list of secret keys to try for verifying signatures, from + #: oldest to newest. The newest (last) key is used for signing. + #: + #: This allows a key rotation system to keep a list of allowed + #: keys and remove expired ones. + self.secret_keys: list[bytes] = _make_keys_list(secret_key) + + if salt is not None: + salt = want_bytes(salt) + # if salt is None then the signer's default is used + + self.salt = salt + + if serializer is None: + serializer = self.default_serializer + + self.serializer: _PDataSerializer[_TSerialized] = serializer + self.is_text_serializer: bool = is_text_serializer(serializer) + + if signer is None: + signer = self.default_signer + + self.signer: type[Signer] = signer + self.signer_kwargs: dict[str, t.Any] = signer_kwargs or {} + + if fallback_signers is None: + fallback_signers = list(self.default_fallback_signers) + + self.fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] = fallback_signers + self.serializer_kwargs: dict[str, t.Any] = serializer_kwargs or {} + + @property + def secret_key(self) -> bytes: + """The newest (last) entry in the :attr:`secret_keys` list. This + is for compatibility from before key rotation support was added. + """ + return self.secret_keys[-1] + + def load_payload( + self, payload: bytes, serializer: _PDataSerializer[t.Any] | None = None + ) -> t.Any: + """Loads the encoded object. This function raises + :class:`.BadPayload` if the payload is not valid. The + ``serializer`` parameter can be used to override the serializer + stored on the class. The encoded ``payload`` should always be + bytes. + """ + if serializer is None: + use_serializer = self.serializer + is_text = self.is_text_serializer + else: + use_serializer = serializer + is_text = is_text_serializer(serializer) + + try: + if is_text: + return use_serializer.loads(payload.decode("utf-8")) # type: ignore[arg-type] + + return use_serializer.loads(payload) # type: ignore[arg-type] + except Exception as e: + raise BadPayload( + "Could not load the payload because an exception" + " occurred on unserializing the data.", + original_error=e, + ) from e + + def dump_payload(self, obj: t.Any) -> bytes: + """Dumps the encoded object. The return value is always bytes. + If the internal serializer returns text, the value will be + encoded as UTF-8. + """ + return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs)) + + def make_signer(self, salt: str | bytes | None = None) -> Signer: + """Creates a new instance of the signer to be used. The default + implementation uses the :class:`.Signer` base class. + """ + if salt is None: + salt = self.salt + + return self.signer(self.secret_keys, salt=salt, **self.signer_kwargs) + + def iter_unsigners(self, salt: str | bytes | None = None) -> cabc.Iterator[Signer]: + """Iterates over all signers to be tried for unsigning. Starts + with the configured signer, then constructs each signer + specified in ``fallback_signers``. + """ + if salt is None: + salt = self.salt + + yield self.make_signer(salt) + + for fallback in self.fallback_signers: + if isinstance(fallback, dict): + kwargs = fallback + fallback = self.signer + elif isinstance(fallback, tuple): + fallback, kwargs = fallback + else: + kwargs = self.signer_kwargs + + for secret_key in self.secret_keys: + yield fallback(secret_key, salt=salt, **kwargs) + + def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> _TSerialized: + """Returns a signed string serialized with the internal + serializer. The return value can be either a byte or unicode + string depending on the format of the internal serializer. + """ + payload = want_bytes(self.dump_payload(obj)) + rv = self.make_signer(salt).sign(payload) + + if self.is_text_serializer: + return rv.decode("utf-8") # type: ignore[return-value] + + return rv # type: ignore[return-value] + + def dump(self, obj: t.Any, f: t.IO[t.Any], salt: str | bytes | None = None) -> None: + """Like :meth:`dumps` but dumps into a file. The file handle has + to be compatible with what the internal serializer expects. + """ + f.write(self.dumps(obj, salt)) + + def loads( + self, s: str | bytes, salt: str | bytes | None = None, **kwargs: t.Any + ) -> t.Any: + """Reverse of :meth:`dumps`. Raises :exc:`.BadSignature` if the + signature validation fails. + """ + s = want_bytes(s) + last_exception = None + + for signer in self.iter_unsigners(salt): + try: + return self.load_payload(signer.unsign(s)) + except BadSignature as err: + last_exception = err + + raise t.cast(BadSignature, last_exception) + + def load(self, f: t.IO[t.Any], salt: str | bytes | None = None) -> t.Any: + """Like :meth:`loads` but loads from a file.""" + return self.loads(f.read(), salt) + + def loads_unsafe( + self, s: str | bytes, salt: str | bytes | None = None + ) -> tuple[bool, t.Any]: + """Like :meth:`loads` but without verifying the signature. This + is potentially very dangerous to use depending on how your + serializer works. The return value is ``(signature_valid, + payload)`` instead of just the payload. The first item will be a + boolean that indicates if the signature is valid. This function + never fails. + + Use it for debugging only and if you know that your serializer + module is not exploitable (for example, do not use it with a + pickle serializer). + + .. versionadded:: 0.15 + """ + return self._loads_unsafe_impl(s, salt) + + def _loads_unsafe_impl( + self, + s: str | bytes, + salt: str | bytes | None, + load_kwargs: dict[str, t.Any] | None = None, + load_payload_kwargs: dict[str, t.Any] | None = None, + ) -> tuple[bool, t.Any]: + """Low level helper function to implement :meth:`loads_unsafe` + in serializer subclasses. + """ + if load_kwargs is None: + load_kwargs = {} + + try: + return True, self.loads(s, salt=salt, **load_kwargs) + except BadSignature as e: + if e.payload is None: + return False, None + + if load_payload_kwargs is None: + load_payload_kwargs = {} + + try: + return ( + False, + self.load_payload(e.payload, **load_payload_kwargs), + ) + except BadPayload: + return False, None + + def load_unsafe( + self, f: t.IO[t.Any], salt: str | bytes | None = None + ) -> tuple[bool, t.Any]: + """Like :meth:`loads_unsafe` but loads from a file. + + .. versionadded:: 0.15 + """ + return self.loads_unsafe(f.read(), salt=salt) diff --git a/venv/Lib/site-packages/itsdangerous/signer.py b/venv/Lib/site-packages/itsdangerous/signer.py new file mode 100644 index 000000000..e324dc03d --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/signer.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import collections.abc as cabc +import hashlib +import hmac +import typing as t + +from .encoding import _base64_alphabet +from .encoding import base64_decode +from .encoding import base64_encode +from .encoding import want_bytes +from .exc import BadSignature + + +class SigningAlgorithm: + """Subclasses must implement :meth:`get_signature` to provide + signature generation functionality. + """ + + def get_signature(self, key: bytes, value: bytes) -> bytes: + """Returns the signature for the given key and value.""" + raise NotImplementedError() + + def verify_signature(self, key: bytes, value: bytes, sig: bytes) -> bool: + """Verifies the given signature matches the expected + signature. + """ + return hmac.compare_digest(sig, self.get_signature(key, value)) + + +class NoneAlgorithm(SigningAlgorithm): + """Provides an algorithm that does not perform any signing and + returns an empty signature. + """ + + def get_signature(self, key: bytes, value: bytes) -> bytes: + return b"" + + +def _lazy_sha1(string: bytes = b"") -> t.Any: + """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include + SHA-1, in which case the import and use as a default would fail before the + developer can configure something else. + """ + return hashlib.sha1(string) + + +class HMACAlgorithm(SigningAlgorithm): + """Provides signature generation using HMACs.""" + + #: The digest method to use with the MAC algorithm. This defaults to + #: SHA1, but can be changed to any other function in the hashlib + #: module. + default_digest_method: t.Any = staticmethod(_lazy_sha1) + + def __init__(self, digest_method: t.Any = None): + if digest_method is None: + digest_method = self.default_digest_method + + self.digest_method: t.Any = digest_method + + def get_signature(self, key: bytes, value: bytes) -> bytes: + mac = hmac.new(key, msg=value, digestmod=self.digest_method) + return mac.digest() + + +def _make_keys_list( + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], +) -> list[bytes]: + if isinstance(secret_key, (str, bytes)): + return [want_bytes(secret_key)] + + return [want_bytes(s) for s in secret_key] # pyright: ignore + + +class Signer: + """A signer securely signs bytes, then unsigns them to verify that + the value hasn't been changed. + + The secret key should be a random string of ``bytes`` and should not + be saved to code or version control. Different salts should be used + to distinguish signing in different contexts. See :doc:`/concepts` + for information about the security of the secret key and salt. + + :param secret_key: The secret key to sign and verify with. Can be a + list of keys, oldest to newest, to support key rotation. + :param salt: Extra key to combine with ``secret_key`` to distinguish + signatures in different contexts. + :param sep: Separator between the signature and value. + :param key_derivation: How to derive the signing key from the secret + key and salt. Possible values are ``concat``, ``django-concat``, + or ``hmac``. Defaults to :attr:`default_key_derivation`, which + defaults to ``django-concat``. + :param digest_method: Hash function to use when generating the HMAC + signature. Defaults to :attr:`default_digest_method`, which + defaults to :func:`hashlib.sha1`. Note that the security of the + hash alone doesn't apply when used intermediately in HMAC. + :param algorithm: A :class:`SigningAlgorithm` instance to use + instead of building a default :class:`HMACAlgorithm` with the + ``digest_method``. + + .. versionchanged:: 2.0 + Added support for key rotation by passing a list to + ``secret_key``. + + .. versionchanged:: 0.18 + ``algorithm`` was added as an argument to the class constructor. + + .. versionchanged:: 0.14 + ``key_derivation`` and ``digest_method`` were added as arguments + to the class constructor. + """ + + #: The default digest method to use for the signer. The default is + #: :func:`hashlib.sha1`, but can be changed to any :mod:`hashlib` or + #: compatible object. Note that the security of the hash alone + #: doesn't apply when used intermediately in HMAC. + #: + #: .. versionadded:: 0.14 + default_digest_method: t.Any = staticmethod(_lazy_sha1) + + #: The default scheme to use to derive the signing key from the + #: secret key and salt. The default is ``django-concat``. Possible + #: values are ``concat``, ``django-concat``, and ``hmac``. + #: + #: .. versionadded:: 0.14 + default_key_derivation: str = "django-concat" + + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous.Signer", + sep: str | bytes = b".", + key_derivation: str | None = None, + digest_method: t.Any | None = None, + algorithm: SigningAlgorithm | None = None, + ): + #: The list of secret keys to try for verifying signatures, from + #: oldest to newest. The newest (last) key is used for signing. + #: + #: This allows a key rotation system to keep a list of allowed + #: keys and remove expired ones. + self.secret_keys: list[bytes] = _make_keys_list(secret_key) + self.sep: bytes = want_bytes(sep) + + if self.sep in _base64_alphabet: + raise ValueError( + "The given separator cannot be used because it may be" + " contained in the signature itself. ASCII letters," + " digits, and '-_=' must not be used." + ) + + if salt is not None: + salt = want_bytes(salt) + else: + salt = b"itsdangerous.Signer" + + self.salt = salt + + if key_derivation is None: + key_derivation = self.default_key_derivation + + self.key_derivation: str = key_derivation + + if digest_method is None: + digest_method = self.default_digest_method + + self.digest_method: t.Any = digest_method + + if algorithm is None: + algorithm = HMACAlgorithm(self.digest_method) + + self.algorithm: SigningAlgorithm = algorithm + + @property + def secret_key(self) -> bytes: + """The newest (last) entry in the :attr:`secret_keys` list. This + is for compatibility from before key rotation support was added. + """ + return self.secret_keys[-1] + + def derive_key(self, secret_key: str | bytes | None = None) -> bytes: + """This method is called to derive the key. The default key + derivation choices can be overridden here. Key derivation is not + intended to be used as a security method to make a complex key + out of a short password. Instead you should use large random + secret keys. + + :param secret_key: A specific secret key to derive from. + Defaults to the last item in :attr:`secret_keys`. + + .. versionchanged:: 2.0 + Added the ``secret_key`` parameter. + """ + if secret_key is None: + secret_key = self.secret_keys[-1] + else: + secret_key = want_bytes(secret_key) + + if self.key_derivation == "concat": + return t.cast(bytes, self.digest_method(self.salt + secret_key).digest()) + elif self.key_derivation == "django-concat": + return t.cast( + bytes, self.digest_method(self.salt + b"signer" + secret_key).digest() + ) + elif self.key_derivation == "hmac": + mac = hmac.new(secret_key, digestmod=self.digest_method) + mac.update(self.salt) + return mac.digest() + elif self.key_derivation == "none": + return secret_key + else: + raise TypeError("Unknown key derivation method") + + def get_signature(self, value: str | bytes) -> bytes: + """Returns the signature for the given value.""" + value = want_bytes(value) + key = self.derive_key() + sig = self.algorithm.get_signature(key, value) + return base64_encode(sig) + + def sign(self, value: str | bytes) -> bytes: + """Signs the given string.""" + value = want_bytes(value) + return value + self.sep + self.get_signature(value) + + def verify_signature(self, value: str | bytes, sig: str | bytes) -> bool: + """Verifies the signature for the given value.""" + try: + sig = base64_decode(sig) + except Exception: + return False + + value = want_bytes(value) + + for secret_key in reversed(self.secret_keys): + key = self.derive_key(secret_key) + + if self.algorithm.verify_signature(key, value, sig): + return True + + return False + + def unsign(self, signed_value: str | bytes) -> bytes: + """Unsigns the given string.""" + signed_value = want_bytes(signed_value) + + if self.sep not in signed_value: + raise BadSignature(f"No {self.sep!r} found in value") + + value, sig = signed_value.rsplit(self.sep, 1) + + if self.verify_signature(value, sig): + return value + + raise BadSignature(f"Signature {sig!r} does not match", payload=value) + + def validate(self, signed_value: str | bytes) -> bool: + """Only validates the given signed value. Returns ``True`` if + the signature exists and is valid. + """ + try: + self.unsign(signed_value) + return True + except BadSignature: + return False diff --git a/venv/Lib/site-packages/itsdangerous/timed.py b/venv/Lib/site-packages/itsdangerous/timed.py new file mode 100644 index 000000000..73843755d --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/timed.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import collections.abc as cabc +import time +import typing as t +from datetime import datetime +from datetime import timezone + +from .encoding import base64_decode +from .encoding import base64_encode +from .encoding import bytes_to_int +from .encoding import int_to_bytes +from .encoding import want_bytes +from .exc import BadSignature +from .exc import BadTimeSignature +from .exc import SignatureExpired +from .serializer import _TSerialized +from .serializer import Serializer +from .signer import Signer + + +class TimestampSigner(Signer): + """Works like the regular :class:`.Signer` but also records the time + of the signing and can be used to expire signatures. The + :meth:`unsign` method can raise :exc:`.SignatureExpired` if the + unsigning failed because the signature is expired. + """ + + def get_timestamp(self) -> int: + """Returns the current timestamp. The function must return an + integer. + """ + return int(time.time()) + + def timestamp_to_datetime(self, ts: int) -> datetime: + """Convert the timestamp from :meth:`get_timestamp` into an + aware :class`datetime.datetime` in UTC. + + .. versionchanged:: 2.0 + The timestamp is returned as a timezone-aware ``datetime`` + in UTC rather than a naive ``datetime`` assumed to be UTC. + """ + return datetime.fromtimestamp(ts, tz=timezone.utc) + + def sign(self, value: str | bytes) -> bytes: + """Signs the given string and also attaches time information.""" + value = want_bytes(value) + timestamp = base64_encode(int_to_bytes(self.get_timestamp())) + sep = want_bytes(self.sep) + value = value + sep + timestamp + return value + sep + self.get_signature(value) + + # Ignore overlapping signatures check, return_timestamp is the only + # parameter that affects the return type. + + @t.overload + def unsign( # type: ignore[overload-overlap] + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: t.Literal[False] = False, + ) -> bytes: ... + + @t.overload + def unsign( + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: t.Literal[True] = True, + ) -> tuple[bytes, datetime]: ... + + def unsign( + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: bool = False, + ) -> tuple[bytes, datetime] | bytes: + """Works like the regular :meth:`.Signer.unsign` but can also + validate the time. See the base docstring of the class for + the general behavior. If ``return_timestamp`` is ``True`` the + timestamp of the signature will be returned as an aware + :class:`datetime.datetime` object in UTC. + + .. versionchanged:: 2.0 + The timestamp is returned as a timezone-aware ``datetime`` + in UTC rather than a naive ``datetime`` assumed to be UTC. + """ + try: + result = super().unsign(signed_value) + sig_error = None + except BadSignature as e: + sig_error = e + result = e.payload or b"" + + sep = want_bytes(self.sep) + + # If there is no timestamp in the result there is something + # seriously wrong. In case there was a signature error, we raise + # that one directly, otherwise we have a weird situation in + # which we shouldn't have come except someone uses a time-based + # serializer on non-timestamp data, so catch that. + if sep not in result: + if sig_error: + raise sig_error + + raise BadTimeSignature("timestamp missing", payload=result) + + value, ts_bytes = result.rsplit(sep, 1) + ts_int: int | None = None + ts_dt: datetime | None = None + + try: + ts_int = bytes_to_int(base64_decode(ts_bytes)) + except Exception: + pass + + # Signature is *not* okay. Raise a proper error now that we have + # split the value and the timestamp. + if sig_error is not None: + if ts_int is not None: + try: + ts_dt = self.timestamp_to_datetime(ts_int) + except (ValueError, OSError, OverflowError) as exc: + # Windows raises OSError + # 32-bit raises OverflowError + raise BadTimeSignature( + "Malformed timestamp", payload=value + ) from exc + + raise BadTimeSignature(str(sig_error), payload=value, date_signed=ts_dt) + + # Signature was okay but the timestamp is actually not there or + # malformed. Should not happen, but we handle it anyway. + if ts_int is None: + raise BadTimeSignature("Malformed timestamp", payload=value) + + # Check timestamp is not older than max_age + if max_age is not None: + age = self.get_timestamp() - ts_int + + if age > max_age: + raise SignatureExpired( + f"Signature age {age} > {max_age} seconds", + payload=value, + date_signed=self.timestamp_to_datetime(ts_int), + ) + + if age < 0: + raise SignatureExpired( + f"Signature age {age} < 0 seconds", + payload=value, + date_signed=self.timestamp_to_datetime(ts_int), + ) + + if return_timestamp: + return value, self.timestamp_to_datetime(ts_int) + + return value + + def validate(self, signed_value: str | bytes, max_age: int | None = None) -> bool: + """Only validates the given signed value. Returns ``True`` if + the signature exists and is valid.""" + try: + self.unsign(signed_value, max_age=max_age) + return True + except BadSignature: + return False + + +class TimedSerializer(Serializer[_TSerialized]): + """Uses :class:`TimestampSigner` instead of the default + :class:`.Signer`. + """ + + default_signer: type[TimestampSigner] = TimestampSigner + + def iter_unsigners( + self, salt: str | bytes | None = None + ) -> cabc.Iterator[TimestampSigner]: + return t.cast("cabc.Iterator[TimestampSigner]", super().iter_unsigners(salt)) + + # TODO: Signature is incompatible because parameters were added + # before salt. + + def loads( # type: ignore[override] + self, + s: str | bytes, + max_age: int | None = None, + return_timestamp: bool = False, + salt: str | bytes | None = None, + ) -> t.Any: + """Reverse of :meth:`dumps`, raises :exc:`.BadSignature` if the + signature validation fails. If a ``max_age`` is provided it will + ensure the signature is not older than that time in seconds. In + case the signature is outdated, :exc:`.SignatureExpired` is + raised. All arguments are forwarded to the signer's + :meth:`~TimestampSigner.unsign` method. + """ + s = want_bytes(s) + last_exception = None + + for signer in self.iter_unsigners(salt): + try: + base64d, timestamp = signer.unsign( + s, max_age=max_age, return_timestamp=True + ) + payload = self.load_payload(base64d) + + if return_timestamp: + return payload, timestamp + + return payload + except SignatureExpired: + # The signature was unsigned successfully but was + # expired. Do not try the next signer. + raise + except BadSignature as err: + last_exception = err + + raise t.cast(BadSignature, last_exception) + + def loads_unsafe( # type: ignore[override] + self, + s: str | bytes, + max_age: int | None = None, + salt: str | bytes | None = None, + ) -> tuple[bool, t.Any]: + return self._loads_unsafe_impl(s, salt, load_kwargs={"max_age": max_age}) diff --git a/venv/Lib/site-packages/itsdangerous/url_safe.py b/venv/Lib/site-packages/itsdangerous/url_safe.py new file mode 100644 index 000000000..56a079331 --- /dev/null +++ b/venv/Lib/site-packages/itsdangerous/url_safe.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import typing as t +import zlib + +from ._json import _CompactJSON +from .encoding import base64_decode +from .encoding import base64_encode +from .exc import BadPayload +from .serializer import _PDataSerializer +from .serializer import Serializer +from .timed import TimedSerializer + + +class URLSafeSerializerMixin(Serializer[str]): + """Mixed in with a regular serializer it will attempt to zlib + compress the string to make it shorter if necessary. It will also + base64 encode the string so that it can safely be placed in a URL. + """ + + default_serializer: _PDataSerializer[str] = _CompactJSON + + def load_payload( + self, + payload: bytes, + *args: t.Any, + serializer: t.Any | None = None, + **kwargs: t.Any, + ) -> t.Any: + decompress = False + + if payload.startswith(b"."): + payload = payload[1:] + decompress = True + + try: + json = base64_decode(payload) + except Exception as e: + raise BadPayload( + "Could not base64 decode the payload because of an exception", + original_error=e, + ) from e + + if decompress: + try: + json = zlib.decompress(json) + except Exception as e: + raise BadPayload( + "Could not zlib decompress the payload before decoding the payload", + original_error=e, + ) from e + + return super().load_payload(json, *args, **kwargs) + + def dump_payload(self, obj: t.Any) -> bytes: + json = super().dump_payload(obj) + is_compressed = False + compressed = zlib.compress(json) + + if len(compressed) < (len(json) - 1): + json = compressed + is_compressed = True + + base64d = base64_encode(json) + + if is_compressed: + base64d = b"." + base64d + + return base64d + + +class URLSafeSerializer(URLSafeSerializerMixin, Serializer[str]): + """Works like :class:`.Serializer` but dumps and loads into a URL + safe string consisting of the upper and lowercase character of the + alphabet as well as ``'_'``, ``'-'`` and ``'.'``. + """ + + +class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer[str]): + """Works like :class:`.TimedSerializer` but dumps and loads into a + URL safe string consisting of the upper and lowercase character of + the alphabet as well as ``'_'``, ``'-'`` and ``'.'``. + """ diff --git a/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/INSTALLER b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/METADATA b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/METADATA new file mode 100644 index 000000000..b92426cc7 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/METADATA @@ -0,0 +1,63 @@ +Metadata-Version: 2.4 +Name: pydantic-settings +Version: 2.10.1 +Summary: Settings management using Pydantic +Project-URL: Homepage, https://github.com/pydantic/pydantic-settings +Project-URL: Funding, https://github.com/sponsors/samuelcolvin +Project-URL: Source, https://github.com/pydantic/pydantic-settings +Project-URL: Changelog, https://github.com/pydantic/pydantic-settings/releases +Project-URL: Documentation, https://docs.pydantic.dev/dev-v2/concepts/pydantic_settings/ +Author-email: Samuel Colvin , Eric Jolibois , Hasan Ramezani +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Framework :: Pydantic +Classifier: Framework :: Pydantic :: 2 +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.9 +Requires-Dist: pydantic>=2.7.0 +Requires-Dist: python-dotenv>=0.21.0 +Requires-Dist: typing-inspection>=0.4.0 +Provides-Extra: aws-secrets-manager +Requires-Dist: boto3-stubs[secretsmanager]; extra == 'aws-secrets-manager' +Requires-Dist: boto3>=1.35.0; extra == 'aws-secrets-manager' +Provides-Extra: azure-key-vault +Requires-Dist: azure-identity>=1.16.0; extra == 'azure-key-vault' +Requires-Dist: azure-keyvault-secrets>=4.8.0; extra == 'azure-key-vault' +Provides-Extra: gcp-secret-manager +Requires-Dist: google-cloud-secret-manager>=2.23.1; extra == 'gcp-secret-manager' +Provides-Extra: toml +Requires-Dist: tomli>=2.0.1; extra == 'toml' +Provides-Extra: yaml +Requires-Dist: pyyaml>=6.0.1; extra == 'yaml' +Description-Content-Type: text/markdown + +# pydantic-settings + +[![CI](https://github.com/pydantic/pydantic-settings/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-settings/actions/workflows/ci.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/pydantic/pydantic-settings/branch/main/graph/badge.svg)](https://codecov.io/gh/pydantic/pydantic-settings) +[![pypi](https://img.shields.io/pypi/v/pydantic-settings.svg)](https://pypi.python.org/pypi/pydantic-settings) +[![license](https://img.shields.io/github/license/pydantic/pydantic-settings.svg)](https://github.com/pydantic/pydantic-settings/blob/main/LICENSE) +[![downloads](https://static.pepy.tech/badge/pydantic-settings/month)](https://pepy.tech/project/pydantic-settings) +[![versions](https://img.shields.io/pypi/pyversions/pydantic-settings.svg)](https://github.com/pydantic/pydantic-settings) + +Settings management using Pydantic. + +See [documentation](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for more details. diff --git a/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/RECORD b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/RECORD new file mode 100644 index 000000000..06c3a9064 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/RECORD @@ -0,0 +1,49 @@ +pydantic_settings-2.10.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pydantic_settings-2.10.1.dist-info/METADATA,sha256=R4TCEAA6hk0_YqITOPKSe5PV1hCzk8t_hoPSJ0gIqoQ,3393 +pydantic_settings-2.10.1.dist-info/RECORD,, +pydantic_settings-2.10.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic_settings-2.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +pydantic_settings-2.10.1.dist-info/licenses/LICENSE,sha256=6zVadT4CA0bTPYO_l2kTW4n8YQVorFMaAcKVvO5_2Zg,1103 +pydantic_settings/__init__.py,sha256=IUkO5TkUu6eYgRJhA1piTw4jp6-CBhV7kam0rEh1Flo,1563 +pydantic_settings/__pycache__/__init__.cpython-312.pyc,, +pydantic_settings/__pycache__/exceptions.cpython-312.pyc,, +pydantic_settings/__pycache__/main.cpython-312.pyc,, +pydantic_settings/__pycache__/utils.cpython-312.pyc,, +pydantic_settings/__pycache__/version.cpython-312.pyc,, +pydantic_settings/exceptions.py,sha256=SHLrIBHeFltPMc8abiQxw-MGqEadlYI-VdLELiZtWPU,97 +pydantic_settings/main.py,sha256=YfYjplX3qeX4wx3n-t7fzG-65nnZS6domI6D7R5Vz2k,29176 +pydantic_settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pydantic_settings/sources/__init__.py,sha256=Ti1bRZb0r7IxkO-wJWKy-qEpeBUFKYRpa3A1AQodOyk,2052 +pydantic_settings/sources/__pycache__/__init__.cpython-312.pyc,, +pydantic_settings/sources/__pycache__/base.cpython-312.pyc,, +pydantic_settings/sources/__pycache__/types.cpython-312.pyc,, +pydantic_settings/sources/__pycache__/utils.cpython-312.pyc,, +pydantic_settings/sources/base.py,sha256=8IwvDw2l_dDpYjc_QPh3omWpYkJgMkd9lV3VFp8BU-Q,20508 +pydantic_settings/sources/providers/__init__.py,sha256=jBTurqBXeJvMfTl2lvHr2iDVDOvHfO-8PVNJiKt7MBk,1205 +pydantic_settings/sources/providers/__pycache__/__init__.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/aws.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/azure.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/cli.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/dotenv.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/env.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/gcp.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/json.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/pyproject.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/secrets.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/toml.cpython-312.pyc,, +pydantic_settings/sources/providers/__pycache__/yaml.cpython-312.pyc,, +pydantic_settings/sources/providers/aws.py,sha256=RQB_n5mHMETVTEObTZW89xp_fyKjqIxNT4ePrd6l818,2416 +pydantic_settings/sources/providers/azure.py,sha256=jR18hCpALjDnEObWGegvHs4_Da5j7PoBIt2kBbHYMag,4108 +pydantic_settings/sources/providers/cli.py,sha256=zUpJlrjGNmjaoUSvqTfuM7u_UFNB5Nf5psRkhl5RoyM,51158 +pydantic_settings/sources/providers/dotenv.py,sha256=y_sDkf7D9jZEQJkKDeGWMnnVbR9JhkL-Zu8tSSuTRRc,5888 +pydantic_settings/sources/providers/env.py,sha256=E2q9YHjFrFUWAid2VpY3678PDSuIDQc_47iWcz_ojQ4,10717 +pydantic_settings/sources/providers/gcp.py,sha256=3bFh75aZp6mmn12VihQycND-5CLgnYWg6HBfNvIV26U,5644 +pydantic_settings/sources/providers/json.py,sha256=k0hWDu0fNLrI5z3zWTGtlKyR0xx-2pOPu-oWjwqmVXo,1436 +pydantic_settings/sources/providers/pyproject.py,sha256=zSQsV3-jtZhiLm3YlrlYoE2__tZBazp0KjQyKLNyLr0,2052 +pydantic_settings/sources/providers/secrets.py,sha256=JLMIj3VVwp86foGTP8fb6zWddmYpELBu95Ldzobnsw8,4303 +pydantic_settings/sources/providers/toml.py,sha256=5k9wMJbKrUqXNiCM5G1hYnCOEZNUJJBTAzFw6Pv2K6A,1827 +pydantic_settings/sources/providers/yaml.py,sha256=mhjmOkrwLT16AEGNDuYoex2PYHejusn7Y0J4KL6SVbw,2305 +pydantic_settings/sources/types.py,sha256=h0FA8TMUMCj2hPMcA6VqZddIffoLbXxaCCKpcDo5iXM,1554 +pydantic_settings/sources/utils.py,sha256=5LIf3WbkgABPGpBjl_SyLdMjdl3KYa-lHudZMm_zNEE,7288 +pydantic_settings/utils.py,sha256=SkOfKGo0omDB4REfg31XSO8yVmpzCQgeIcdg-qqcSrk,1382 +pydantic_settings/version.py,sha256=xCWGAR_AgQdjZg_-c3LrWksxw74Y-F1odSn9vNk1CkQ,19 diff --git a/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/REQUESTED b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/WHEEL b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/WHEEL new file mode 100644 index 000000000..12228d414 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/licenses/LICENSE b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/licenses/LICENSE new file mode 100644 index 000000000..d90598f2a --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings-2.10.1.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Samuel Colvin and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/venv/Lib/site-packages/pydantic_settings/__init__.py b/venv/Lib/site-packages/pydantic_settings/__init__.py new file mode 100644 index 000000000..60990a8fe --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/__init__.py @@ -0,0 +1,63 @@ +from .exceptions import SettingsError +from .main import BaseSettings, CliApp, SettingsConfigDict +from .sources import ( + CLI_SUPPRESS, + AWSSecretsManagerSettingsSource, + AzureKeyVaultSettingsSource, + CliExplicitFlag, + CliImplicitFlag, + CliMutuallyExclusiveGroup, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + CliSuppress, + CliUnknownArgs, + DotEnvSettingsSource, + EnvSettingsSource, + ForceDecode, + GoogleSecretManagerSettingsSource, + InitSettingsSource, + JsonConfigSettingsSource, + NoDecode, + PydanticBaseSettingsSource, + PyprojectTomlConfigSettingsSource, + SecretsSettingsSource, + TomlConfigSettingsSource, + YamlConfigSettingsSource, + get_subcommand, +) +from .version import VERSION + +__all__ = ( + 'CLI_SUPPRESS', + 'AWSSecretsManagerSettingsSource', + 'AzureKeyVaultSettingsSource', + 'BaseSettings', + 'CliApp', + 'CliExplicitFlag', + 'CliImplicitFlag', + 'CliMutuallyExclusiveGroup', + 'CliPositionalArg', + 'CliSettingsSource', + 'CliSubCommand', + 'CliSuppress', + 'CliUnknownArgs', + 'DotEnvSettingsSource', + 'EnvSettingsSource', + 'ForceDecode', + 'GoogleSecretManagerSettingsSource', + 'InitSettingsSource', + 'JsonConfigSettingsSource', + 'NoDecode', + 'PydanticBaseSettingsSource', + 'PyprojectTomlConfigSettingsSource', + 'SecretsSettingsSource', + 'SettingsConfigDict', + 'SettingsError', + 'TomlConfigSettingsSource', + 'YamlConfigSettingsSource', + '__version__', + 'get_subcommand', +) + +__version__ = VERSION diff --git a/venv/Lib/site-packages/pydantic_settings/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..668b1aa138a940a3c8e53aa7411a2769cc42b0bb GIT binary patch literal 1359 zcmcJPO-~a+7{_KE|p)z9HgPhKZ69z1y?A%=vL^ORypg`<1;W#&IS&)YmRU(@MPO?~gbe12IP*0i5E zx%eXe!O2H8_@K44mhNhx6J5t~#En3dL?K3EFhquWTGSneIEh1oBw&P$z$h7oBuPSw zq##YwkRch!k}QmoG02e|+MEVr)AFn~W`jPF1!s*a8&O%>~sig)J zPT(-la+i5q=9DtvA&pXaP-6g=*OJNJ)b}|RLT1!t&pY(kYftSKa$75{35JpH;Z zG`c=#uc#gDGH|anG}%9x0sDn(Hx!;**OY@04eW$s;y;UvqxaumN zK#U+_h#>@C9G*m^5NSjPkwuIla)>-)95I2IL`)&35d}mMF@q=})J4>I6fuh^E6Aj2 zb|@FB8%&eWDP^mAQ;yOjoBG%yqW8ayf#rCYVnj22=Te3H%Ma7paW*Bj@wI za7qy6CGVbvuRO_gl&o{WsvQX*#MsP4kZ5Jxe?Q)(TzvkXMyYd@e*?;W(=6 v`giTt4{hn2wsaaP>XB3J8l2$5@B5lwK90>LBX8r!k8~|N^)AV8D@%U>c&3cx literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/__pycache__/exceptions.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7c2362b1088a58cc2808ba5aba2b57ca4c2c846 GIT binary patch literal 450 zcmX|8Jxc>Y5Z%3tcnJw;Yjtgq#6oODEQ}(E#DE{gv9N4z$9U#)ckXV2u})=Wm){`% z3_JY=UM2VkglLn>-TQEgnK!)mmYLc2Mx%ywJifi&FkQb)Q^raG78@GOk%f*CM>E7R zpcgymJUq3UwqH6-#YMAN(_oGOd6*;5;@Go|Hf@}PQb9D3M?%EnVe^1VaN*vEL2~nVm=NHu2Xt9W?{jsQp#hW(r2_V!z`h* zOD-h{NqQY}2y&?6gvdk$k=iC_eUQP0VE(Yx6Y(whicBDykkg<~WT2pxXo?L$l4QzR zqynFo^KJc)E!)Xdn44tDoIpA{&Co|>=d@bB~ec67v~LXD{Dt aU(|J*J+`jFw~aCWv20v@I{8JqWR^eXjd<(; literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/__pycache__/main.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1f69c7fca01772ec1ba5c50d59947494d4ed0e8 GIT binary patch literal 23483 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 C#DnAj literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/__pycache__/utils.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be68b9174ab8267983c91b852433b21279d8454c GIT binary patch literal 1934 zcmZ8hO>7fK6rTOH9XmG0F){z!22d>p=$}N2`lB?Eh7yuU2|}@4ZEf#3-ekRN&5V;c z3Q`>*Q7Ba{^*|0iaINTpQ$5c2^WOKp_jYIA zd;5Ov?d?IZZvOI9rK}N(xVsuQY)b72W#Wqxn)%C59D;QP8b&@3JJeF5r9D+GL|iv!IH1AibuU1>#tKh(v6 zX7vDnBtTthMS(ZEIMA$^9nK}KUaVMgytk}ciEC;uW$nS5m4qjb`^vht7q~P=^MgTD zS6ZnB86gsu5$?a+p>vsrgmo;{g-8Fm}I z*Zc^(nAoLZ$1YH?_RL{sdc>ZyT^_M)f=kRJ4X&1L2d_k{UfpFz&9hy`L$pzKY15f??rBy97R^N`Z6$S4S_5PI8@s9WyS~xK1#u6So9yvAa~z6nM7RV7rZ0(frHS zj$4ob8u}QTDL`uygB0zod1%7_ufXpd`Xq@^9(=D#S7jNkN~`E@=POjYl~|QavV^9Q ziW+iOS$Vz3fHi8nmf_Wzu~aEAgL;G+UfEz3+nqBCMh&uP*z}a|x{~stdcfs|R+^oWNpP@WyP7TkDnKQFf=gslq56_QJbIoSBO0x=y!RfhP#jN64Ty+ao zOhsDYBriT4Ih(&sF`@aXk^DJK7noPe(;9&~yp^9UVrtKjLTTaT6!GSvV);evF6PJW zVxGdwPu9Rxn8P$*Ygh%B*(H-=#^4(Cbe-7_JymP)gt&mo8Z~S>g(7x{_>sg1}uo;!4S(JYI4NltP!8tX^|)X}5&nvD*xb0B)lQV!O?-in_(V+lg70 zNK8U(xAdz}+Q2rA1`yN5XT>^(NCG?~I|xCW1r5-YXHi31LrZ8`nMF%dR-PEmYP`p$ zwo6&TEn%*T#0ZAgi~iwxWCob?gm{FAPh@2xCJfmR8x?0W43Y-HQ>KGm8@tTx29u*; zc~y80!v?vPLt6Z4V*mQUH|$~J*n`B_cL(l`+&}U#JGK!X`}b+I?>W-IBbsiNzW87x zdg31)_3eKY?rlebenCa!tQ|JcoxUOEdwEF7%FjgOJQMRddyp%%LAfSwKzT@%*|Cd6 zIB*?EXagdWPDBoQa)&YTGQ%#lJ(v2|_ULBe2YlGxR)ryi}1Tu&#F84eR2k%Xes(LY;8r7q$jB735xxW zGJl}sf1}shI?^K7FE%f>4n9)D*TdXCt73eWF#$bpkzA}kD(??Y;6 wOG#}isn*;BMLC;r?waOd*Y@iUvp&)iS{c=U&(8|e$p3ICS=0g=YryZ`_I literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/__pycache__/version.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/__pycache__/version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1eaaa5ed0be1116a9976ee1fda66ddd5acfb5a26 GIT binary patch literal 218 zcmX@j%ge<81YM^NX9xr7#~=<2FhUuhIe?7m3@Hpz43&(UOjT?~dWHsihJKoix7fp6 zgMvN%{Z=x31}XlPbQ=D0ns#^e*N=#2Jjwz^2Nz5zBOpY&3Eh)*&OE1 None: + super().__init__( + **__pydantic_self__._settings_build_values( + values, + _case_sensitive=_case_sensitive, + _nested_model_default_partial_update=_nested_model_default_partial_update, + _env_prefix=_env_prefix, + _env_file=_env_file, + _env_file_encoding=_env_file_encoding, + _env_ignore_empty=_env_ignore_empty, + _env_nested_delimiter=_env_nested_delimiter, + _env_nested_max_split=_env_nested_max_split, + _env_parse_none_str=_env_parse_none_str, + _env_parse_enums=_env_parse_enums, + _cli_prog_name=_cli_prog_name, + _cli_parse_args=_cli_parse_args, + _cli_settings_source=_cli_settings_source, + _cli_parse_none_str=_cli_parse_none_str, + _cli_hide_none_type=_cli_hide_none_type, + _cli_avoid_json=_cli_avoid_json, + _cli_enforce_required=_cli_enforce_required, + _cli_use_class_docs_for_groups=_cli_use_class_docs_for_groups, + _cli_exit_on_error=_cli_exit_on_error, + _cli_prefix=_cli_prefix, + _cli_flag_prefix_char=_cli_flag_prefix_char, + _cli_implicit_flags=_cli_implicit_flags, + _cli_ignore_unknown_args=_cli_ignore_unknown_args, + _cli_kebab_case=_cli_kebab_case, + _cli_shortcuts=_cli_shortcuts, + _secrets_dir=_secrets_dir, + ) + ) + + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + """ + Define the sources and their order for loading the settings values. + + Args: + settings_cls: The Settings class. + init_settings: The `InitSettingsSource` instance. + env_settings: The `EnvSettingsSource` instance. + dotenv_settings: The `DotEnvSettingsSource` instance. + file_secret_settings: The `SecretsSettingsSource` instance. + + Returns: + A tuple containing the sources and their order for loading the settings values. + """ + return init_settings, env_settings, dotenv_settings, file_secret_settings + + def _settings_build_values( + self, + init_kwargs: dict[str, Any], + _case_sensitive: bool | None = None, + _nested_model_default_partial_update: bool | None = None, + _env_prefix: str | None = None, + _env_file: DotenvType | None = None, + _env_file_encoding: str | None = None, + _env_ignore_empty: bool | None = None, + _env_nested_delimiter: str | None = None, + _env_nested_max_split: int | None = None, + _env_parse_none_str: str | None = None, + _env_parse_enums: bool | None = None, + _cli_prog_name: str | None = None, + _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, + _cli_settings_source: CliSettingsSource[Any] | None = None, + _cli_parse_none_str: str | None = None, + _cli_hide_none_type: bool | None = None, + _cli_avoid_json: bool | None = None, + _cli_enforce_required: bool | None = None, + _cli_use_class_docs_for_groups: bool | None = None, + _cli_exit_on_error: bool | None = None, + _cli_prefix: str | None = None, + _cli_flag_prefix_char: str | None = None, + _cli_implicit_flags: bool | None = None, + _cli_ignore_unknown_args: bool | None = None, + _cli_kebab_case: bool | None = None, + _cli_shortcuts: Mapping[str, str | list[str]] | None = None, + _secrets_dir: PathType | None = None, + ) -> dict[str, Any]: + # Determine settings config values + case_sensitive = _case_sensitive if _case_sensitive is not None else self.model_config.get('case_sensitive') + env_prefix = _env_prefix if _env_prefix is not None else self.model_config.get('env_prefix') + nested_model_default_partial_update = ( + _nested_model_default_partial_update + if _nested_model_default_partial_update is not None + else self.model_config.get('nested_model_default_partial_update') + ) + env_file = _env_file if _env_file != ENV_FILE_SENTINEL else self.model_config.get('env_file') + env_file_encoding = ( + _env_file_encoding if _env_file_encoding is not None else self.model_config.get('env_file_encoding') + ) + env_ignore_empty = ( + _env_ignore_empty if _env_ignore_empty is not None else self.model_config.get('env_ignore_empty') + ) + env_nested_delimiter = ( + _env_nested_delimiter + if _env_nested_delimiter is not None + else self.model_config.get('env_nested_delimiter') + ) + env_nested_max_split = ( + _env_nested_max_split + if _env_nested_max_split is not None + else self.model_config.get('env_nested_max_split') + ) + env_parse_none_str = ( + _env_parse_none_str if _env_parse_none_str is not None else self.model_config.get('env_parse_none_str') + ) + env_parse_enums = _env_parse_enums if _env_parse_enums is not None else self.model_config.get('env_parse_enums') + + cli_prog_name = _cli_prog_name if _cli_prog_name is not None else self.model_config.get('cli_prog_name') + cli_parse_args = _cli_parse_args if _cli_parse_args is not None else self.model_config.get('cli_parse_args') + cli_settings_source = ( + _cli_settings_source if _cli_settings_source is not None else self.model_config.get('cli_settings_source') + ) + cli_parse_none_str = ( + _cli_parse_none_str if _cli_parse_none_str is not None else self.model_config.get('cli_parse_none_str') + ) + cli_parse_none_str = cli_parse_none_str if not env_parse_none_str else env_parse_none_str + cli_hide_none_type = ( + _cli_hide_none_type if _cli_hide_none_type is not None else self.model_config.get('cli_hide_none_type') + ) + cli_avoid_json = _cli_avoid_json if _cli_avoid_json is not None else self.model_config.get('cli_avoid_json') + cli_enforce_required = ( + _cli_enforce_required + if _cli_enforce_required is not None + else self.model_config.get('cli_enforce_required') + ) + cli_use_class_docs_for_groups = ( + _cli_use_class_docs_for_groups + if _cli_use_class_docs_for_groups is not None + else self.model_config.get('cli_use_class_docs_for_groups') + ) + cli_exit_on_error = ( + _cli_exit_on_error if _cli_exit_on_error is not None else self.model_config.get('cli_exit_on_error') + ) + cli_prefix = _cli_prefix if _cli_prefix is not None else self.model_config.get('cli_prefix') + cli_flag_prefix_char = ( + _cli_flag_prefix_char + if _cli_flag_prefix_char is not None + else self.model_config.get('cli_flag_prefix_char') + ) + cli_implicit_flags = ( + _cli_implicit_flags if _cli_implicit_flags is not None else self.model_config.get('cli_implicit_flags') + ) + cli_ignore_unknown_args = ( + _cli_ignore_unknown_args + if _cli_ignore_unknown_args is not None + else self.model_config.get('cli_ignore_unknown_args') + ) + cli_kebab_case = _cli_kebab_case if _cli_kebab_case is not None else self.model_config.get('cli_kebab_case') + cli_shortcuts = _cli_shortcuts if _cli_shortcuts is not None else self.model_config.get('cli_shortcuts') + + secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir') + + # Configure built-in sources + default_settings = DefaultSettingsSource( + self.__class__, nested_model_default_partial_update=nested_model_default_partial_update + ) + init_settings = InitSettingsSource( + self.__class__, + init_kwargs=init_kwargs, + nested_model_default_partial_update=nested_model_default_partial_update, + ) + env_settings = EnvSettingsSource( + self.__class__, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_nested_delimiter=env_nested_delimiter, + env_nested_max_split=env_nested_max_split, + env_ignore_empty=env_ignore_empty, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + ) + dotenv_settings = DotEnvSettingsSource( + self.__class__, + env_file=env_file, + env_file_encoding=env_file_encoding, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_nested_delimiter=env_nested_delimiter, + env_nested_max_split=env_nested_max_split, + env_ignore_empty=env_ignore_empty, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + ) + + file_secret_settings = SecretsSettingsSource( + self.__class__, secrets_dir=secrets_dir, case_sensitive=case_sensitive, env_prefix=env_prefix + ) + # Provide a hook to set built-in sources priority and add / remove sources + sources = self.settings_customise_sources( + self.__class__, + init_settings=init_settings, + env_settings=env_settings, + dotenv_settings=dotenv_settings, + file_secret_settings=file_secret_settings, + ) + (default_settings,) + if not any([source for source in sources if isinstance(source, CliSettingsSource)]): + if isinstance(cli_settings_source, CliSettingsSource): + sources = (cli_settings_source,) + sources + elif cli_parse_args is not None: + cli_settings = CliSettingsSource[Any]( + self.__class__, + cli_prog_name=cli_prog_name, + cli_parse_args=cli_parse_args, + cli_parse_none_str=cli_parse_none_str, + cli_hide_none_type=cli_hide_none_type, + cli_avoid_json=cli_avoid_json, + cli_enforce_required=cli_enforce_required, + cli_use_class_docs_for_groups=cli_use_class_docs_for_groups, + cli_exit_on_error=cli_exit_on_error, + cli_prefix=cli_prefix, + cli_flag_prefix_char=cli_flag_prefix_char, + cli_implicit_flags=cli_implicit_flags, + cli_ignore_unknown_args=cli_ignore_unknown_args, + cli_kebab_case=cli_kebab_case, + cli_shortcuts=cli_shortcuts, + case_sensitive=case_sensitive, + ) + sources = (cli_settings,) + sources + if sources: + state: dict[str, Any] = {} + states: dict[str, dict[str, Any]] = {} + for source in sources: + if isinstance(source, PydanticBaseSettingsSource): + source._set_current_state(state) + source._set_settings_sources_data(states) + + source_name = source.__name__ if hasattr(source, '__name__') else type(source).__name__ + source_state = source() + + states[source_name] = source_state + state = deep_update(source_state, state) + return state + else: + # no one should mean to do this, but I think returning an empty dict is marginally preferable + # to an informative error and much better than a confusing error + return {} + + model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict( + extra='forbid', + arbitrary_types_allowed=True, + validate_default=True, + case_sensitive=False, + env_prefix='', + nested_model_default_partial_update=False, + env_file=None, + env_file_encoding=None, + env_ignore_empty=False, + env_nested_delimiter=None, + env_nested_max_split=None, + env_parse_none_str=None, + env_parse_enums=None, + cli_prog_name=None, + cli_parse_args=None, + cli_parse_none_str=None, + cli_hide_none_type=False, + cli_avoid_json=False, + cli_enforce_required=False, + cli_use_class_docs_for_groups=False, + cli_exit_on_error=True, + cli_prefix='', + cli_flag_prefix_char='-', + cli_implicit_flags=False, + cli_ignore_unknown_args=False, + cli_kebab_case=False, + cli_shortcuts=None, + json_file=None, + json_file_encoding=None, + yaml_file=None, + yaml_file_encoding=None, + yaml_config_section=None, + toml_file=None, + secrets_dir=None, + protected_namespaces=('model_validate', 'model_dump', 'settings_customise_sources'), + enable_decoding=True, + ) + + +class CliApp: + """ + A utility class for running Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as + CLI applications. + """ + + @staticmethod + def _run_cli_cmd(model: Any, cli_cmd_method_name: str, is_required: bool) -> Any: + command = getattr(type(model), cli_cmd_method_name, None) + if command is None: + if is_required: + raise SettingsError(f'Error: {type(model).__name__} class is missing {cli_cmd_method_name} entrypoint') + return model + + # If the method is asynchronous, we handle its execution based on the current event loop status. + if inspect.iscoroutinefunction(command): + # For asynchronous methods, we have two execution scenarios: + # 1. If no event loop is running in the current thread, run the coroutine directly with asyncio.run(). + # 2. If an event loop is already running in the current thread, run the coroutine in a separate thread to avoid conflicts. + try: + # Check if an event loop is currently running in this thread. + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop and loop.is_running(): + # We're in a context with an active event loop (e.g., Jupyter Notebook). + # Running asyncio.run() here would cause conflicts, so we use a separate thread. + exception_container = [] + + def run_coro() -> None: + try: + # Execute the coroutine in a new event loop in this separate thread. + asyncio.run(command(model)) + except Exception as e: + exception_container.append(e) + + thread = threading.Thread(target=run_coro) + thread.start() + thread.join() + if exception_container: + # Propagate exceptions from the separate thread. + raise exception_container[0] + else: + # No event loop is running; safe to run the coroutine directly. + asyncio.run(command(model)) + else: + # For synchronous methods, call them directly. + command(model) + + return model + + @staticmethod + def run( + model_cls: type[T], + cli_args: list[str] | Namespace | SimpleNamespace | dict[str, Any] | None = None, + cli_settings_source: CliSettingsSource[Any] | None = None, + cli_exit_on_error: bool | None = None, + cli_cmd_method_name: str = 'cli_cmd', + **model_init_data: Any, + ) -> T: + """ + Runs a Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as a CLI application. + Running a model as a CLI application requires the `cli_cmd` method to be defined in the model class. + + Args: + model_cls: The model class to run as a CLI application. + cli_args: The list of CLI arguments to parse. If `cli_settings_source` is specified, this may + also be a namespace or dictionary of pre-parsed CLI arguments. Defaults to `sys.argv[1:]`. + cli_settings_source: Override the default CLI settings source with a user defined instance. + Defaults to `None`. + cli_exit_on_error: Determines whether this function exits on error. If model is subclass of + `BaseSettings`, defaults to BaseSettings `cli_exit_on_error` value. Otherwise, defaults to + `True`. + cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd". + model_init_data: The model init data. + + Returns: + The ran instance of model. + + Raises: + SettingsError: If model_cls is not subclass of `BaseModel` or `pydantic.dataclasses.dataclass`. + SettingsError: If model_cls does not have a `cli_cmd` entrypoint defined. + """ + + if not (is_pydantic_dataclass(model_cls) or is_model_class(model_cls)): + raise SettingsError( + f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass' + ) + + cli_settings = None + cli_parse_args = True if cli_args is None else cli_args + if cli_settings_source is not None: + if isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)): + cli_settings = cli_settings_source(parsed_args=cli_parse_args) + else: + cli_settings = cli_settings_source(args=cli_parse_args) + elif isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)): + raise SettingsError('Error: `cli_args` must be list[str] or None when `cli_settings_source` is not used') + + model_init_data['_cli_parse_args'] = cli_parse_args + model_init_data['_cli_exit_on_error'] = cli_exit_on_error + model_init_data['_cli_settings_source'] = cli_settings + if not issubclass(model_cls, BaseSettings): + + class CliAppBaseSettings(BaseSettings, model_cls): # type: ignore + __doc__ = model_cls.__doc__ + model_config = SettingsConfigDict( + nested_model_default_partial_update=True, + case_sensitive=True, + cli_hide_none_type=True, + cli_avoid_json=True, + cli_enforce_required=True, + cli_implicit_flags=True, + cli_kebab_case=True, + ) + + model = CliAppBaseSettings(**model_init_data) + model_init_data = {} + for field_name, field_info in type(model).model_fields.items(): + model_init_data[_field_name_for_signature(field_name, field_info)] = getattr(model, field_name) + + return CliApp._run_cli_cmd(model_cls(**model_init_data), cli_cmd_method_name, is_required=False) + + @staticmethod + def run_subcommand( + model: PydanticModel, cli_exit_on_error: bool | None = None, cli_cmd_method_name: str = 'cli_cmd' + ) -> PydanticModel: + """ + Runs the model subcommand. Running a model subcommand requires the `cli_cmd` method to be defined in + the nested model subcommand class. + + Args: + model: The model to run the subcommand from. + cli_exit_on_error: Determines whether this function exits with error if no subcommand is found. + Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`. + cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd". + + Returns: + The ran subcommand model. + + Raises: + SystemExit: When no subcommand is found and cli_exit_on_error=`True` (the default). + SettingsError: When no subcommand is found and cli_exit_on_error=`False`. + """ + + subcommand = get_subcommand(model, is_required=True, cli_exit_on_error=cli_exit_on_error) + return CliApp._run_cli_cmd(subcommand, cli_cmd_method_name, is_required=True) diff --git a/venv/Lib/site-packages/pydantic_settings/py.typed b/venv/Lib/site-packages/pydantic_settings/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/venv/Lib/site-packages/pydantic_settings/sources/__init__.py b/venv/Lib/site-packages/pydantic_settings/sources/__init__.py new file mode 100644 index 000000000..a795c49d7 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/__init__.py @@ -0,0 +1,68 @@ +"""Package for handling configuration sources in pydantic-settings.""" + +from .base import ( + ConfigFileSourceMixin, + DefaultSettingsSource, + InitSettingsSource, + PydanticBaseEnvSettingsSource, + PydanticBaseSettingsSource, + get_subcommand, +) +from .providers.aws import AWSSecretsManagerSettingsSource +from .providers.azure import AzureKeyVaultSettingsSource +from .providers.cli import ( + CLI_SUPPRESS, + CliExplicitFlag, + CliImplicitFlag, + CliMutuallyExclusiveGroup, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + CliSuppress, + CliUnknownArgs, +) +from .providers.dotenv import DotEnvSettingsSource, read_env_file +from .providers.env import EnvSettingsSource +from .providers.gcp import GoogleSecretManagerSettingsSource +from .providers.json import JsonConfigSettingsSource +from .providers.pyproject import PyprojectTomlConfigSettingsSource +from .providers.secrets import SecretsSettingsSource +from .providers.toml import TomlConfigSettingsSource +from .providers.yaml import YamlConfigSettingsSource +from .types import DEFAULT_PATH, ENV_FILE_SENTINEL, DotenvType, ForceDecode, NoDecode, PathType, PydanticModel + +__all__ = [ + 'CLI_SUPPRESS', + 'ENV_FILE_SENTINEL', + 'DEFAULT_PATH', + 'AWSSecretsManagerSettingsSource', + 'AzureKeyVaultSettingsSource', + 'CliExplicitFlag', + 'CliImplicitFlag', + 'CliMutuallyExclusiveGroup', + 'CliPositionalArg', + 'CliSettingsSource', + 'CliSubCommand', + 'CliSuppress', + 'CliUnknownArgs', + 'DefaultSettingsSource', + 'DotEnvSettingsSource', + 'DotenvType', + 'EnvSettingsSource', + 'ForceDecode', + 'GoogleSecretManagerSettingsSource', + 'InitSettingsSource', + 'JsonConfigSettingsSource', + 'NoDecode', + 'PathType', + 'PydanticBaseEnvSettingsSource', + 'PydanticBaseSettingsSource', + 'ConfigFileSourceMixin', + 'PydanticModel', + 'PyprojectTomlConfigSettingsSource', + 'SecretsSettingsSource', + 'TomlConfigSettingsSource', + 'YamlConfigSettingsSource', + 'get_subcommand', + 'read_env_file', +] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..587f1098a0b56f14e7b24bce6b5a2d7e4aa14fde GIT binary patch literal 1850 zcma)+&rcgi6vtGu6MeK##A#gg(|$2P2SkD<44m)dgJOsd zX*xqu z^;$*S_YU1wJG3I#_lnYQhc=T%*DD4c$MPcAUXd({AX2XWWq^pK0hKT7HC30hx{lv< zPhC&tx7eZ8=2873zB{Kzw>|f!Z?1N+`KBdV#XGsNWV(0y#$39^B2%^xY+ne-p=79q zjj!uSX~13E zjjG&gX~(wcaZEwPZaZpQoOddxHg8M!ggp)YcA&B_Rr95bYFd0FY^fZ0t{+IH(A7RD zcT-SFJq&`7NvYB>*!PY-|AhyuWhth{xBTd~MyZflj>)_e^APF)Ip%J~fOGMw@3){# zTAsVqDSR${uUpgW2gMC)ognm|GdpVdg5MF8zmWgd>cW34Dtu$zv0*}uY*nfo`#TM@ zw$btxOyD!v6tJ3Rh*@4zarLXpQ4mVFr zrN*wD!R3|H3aQ-HjO;#hEn4q35!_8JVz09U_e+ba`Xjg}vRiCjc3rh1oVlk$SQ1nm z9<^!yNM6B^yg4p^)Z7Q@WwTapZZUZr`9V_#q3cEOH@`e!(rtv$Raa`=H_|+T#G56GrTu0A6Q_@$nh?@H{q*9>8nF`2;aGjWe=*MxLCJ^)s@m3C8*##QTkSzY_1C O_);?VZ6aKUjsFGWt1n*w literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/base.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0047263742784e07b7ba10aa21eb361307b4e9d GIT binary patch literal 23970 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 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/types.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/types.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13e4d5daf378d481d4a023879b99bbcfc632968c GIT binary patch literal 2441 zcmb7F&2JM|5P$2B_}j5#LlTmZO$!BA#h^kUL8wxiIFL3mRjpd7lDu)q(oFA5mitD<3Z$gzpglf=0z**eMh_>6b979ZKwra(tw(zgKFOy@ZX5+Q&SL0w(PFbsMU~bxQ*$)Sw8fgm z#ClUzOh>C8v8m%|=AtdyR)bZko&5)M$p~&lF-^-+_$%-jyg;uss9B}p`wEJ)l<~Q- z;-x}Po<5hG{&FsV*7Z)AO*epa7Zv9EOD3?v>SSSyYesRDs;<~JfT7P7n=V)?)!kSp z&L1J>uUHLJt)h<3wN+waGYZHJLWv|m>v@_&#^XkE=5pRLDH7*zdEKBan~FhY*$vCG zVW|xr{irNoX()O-a*J%@mtQVTpDved%ItDsx;#Ve%Z^nq+jXXy&hhg33bnN&Q>vGb z6qxl5tvcmpYA%=OwMyC69D1YPBD*e74xVAn(S=7$$Ei~n?7(h{a~(> z`@SIC)4_L6Hv!>S>8gU{ZF(}0V&0meRjA(8V^iH-5*am|>>!xyqmFEhznARD ztOYH$r9At(a%jpbw&`1SvwbM%4T&R&p&5o>SK>rhB8FVz2C-qWKiud{Zg#3PUz7_| z#dB_W#&Tdwc_f+3UzBI(=5z9eT)sG$&&}_g+YTKVm#1~@LZdQm8HQr2d4}8erqlz7 zq4@-zel)z)2<@6FiA)6x+7Hf_qEE5Koyk7J&!ahp&MPv6c1@N*vN@x!YgNse)s?q! z9YzW?Ctl}5=>52I-@S|LW28Vc@j4emyXNY}yh`R})B4tg3feoDkZ%(joZ8z;h)^cR zL_|M8GlqtrZ{Cgk)Qln)MZ*>I67fCZU4Wm+XU5nN+;K?N8cu`3kYZj0qVl%M>-JVH zU5D9&lRK+as?3LL0(EF;U8^vhByJCk893~$2Ob#yN3$TB5XMG3!znAnRHF>KCM&C* zNla}P##OQIDnrps7Qxq1zU#Fe)=rM+#WUmy^9)5?(kpKl3|a81@I-maJb7LQ%p0eT zqU-;gHZOx`+OzsIb_$l^z_*`+dFmAe!6oBQ$fr-pfrn(^FEW0Qj6e1!9{Q#p`VKu# z53QZQJH9db#cy*Plcm=2i+`rSzP9iv-M_N%yhjj1s~5klB=ihk;5;L{#l!Rciz9jc*Jup Hzz4&B-%2U0 literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/utils.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d039f9e23742486987d47dd0de6ece63f97d0a9a GIT binary patch literal 9354 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 lv=@nE7mCE#4^-lwxn Optional[PydanticModel]: + """ + Get the subcommand from a model. + + Args: + model: The model to get the subcommand from. + is_required: Determines whether a model must have subcommand set and raises error if not + found. Defaults to `True`. + cli_exit_on_error: Determines whether this function exits with error if no subcommand is found. + Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`. + + Returns: + The subcommand model if found, otherwise `None`. + + Raises: + SystemExit: When no subcommand is found and is_required=`True` and cli_exit_on_error=`True` + (the default). + SettingsError: When no subcommand is found and is_required=`True` and + cli_exit_on_error=`False`. + """ + + model_cls = type(model) + if cli_exit_on_error is None and is_model_class(model_cls): + model_default = model_cls.model_config.get('cli_exit_on_error') + if isinstance(model_default, bool): + cli_exit_on_error = model_default + if cli_exit_on_error is None: + cli_exit_on_error = True + + subcommands: list[str] = [] + for field_name, field_info in _get_model_fields(model_cls).items(): + if _CliSubCommand in field_info.metadata: + if getattr(model, field_name) is not None: + return getattr(model, field_name) + subcommands.append(field_name) + + if is_required: + error_message = ( + f'Error: CLI subcommand is required {{{", ".join(subcommands)}}}' + if subcommands + else 'Error: CLI subcommand is required but no subcommands were found.' + ) + raise SystemExit(error_message) if cli_exit_on_error else SettingsError(error_message) + + return None + + +class PydanticBaseSettingsSource(ABC): + """ + Abstract base class for settings sources, every settings source classes should inherit from it. + """ + + def __init__(self, settings_cls: type[BaseSettings]): + self.settings_cls = settings_cls + self.config = settings_cls.model_config + self._current_state: dict[str, Any] = {} + self._settings_sources_data: dict[str, dict[str, Any]] = {} + + def _set_current_state(self, state: dict[str, Any]) -> None: + """ + Record the state of settings from the previous settings sources. This should + be called right before __call__. + """ + self._current_state = state + + def _set_settings_sources_data(self, states: dict[str, dict[str, Any]]) -> None: + """ + Record the state of settings from all previous settings sources. This should + be called right before __call__. + """ + self._settings_sources_data = states + + @property + def current_state(self) -> dict[str, Any]: + """ + The current state of the settings, populated by the previous settings sources. + """ + return self._current_state + + @property + def settings_sources_data(self) -> dict[str, dict[str, Any]]: + """ + The state of all previous settings sources. + """ + return self._settings_sources_data + + @abstractmethod + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value, the key for model creation, and a flag to determine whether value is complex. + + This is an abstract method that should be overridden in every settings source classes. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value, key and a flag to determine whether value is complex. + """ + pass + + def field_is_complex(self, field: FieldInfo) -> bool: + """ + Checks whether a field is complex, in which case it will attempt to be parsed as JSON. + + Args: + field: The field. + + Returns: + Whether the field is complex. + """ + return _annotation_is_complex(field.annotation, field.metadata) + + def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: + """ + Prepares the value of a field. + + Args: + field_name: The field name. + field: The field. + value: The value of the field that has to be prepared. + value_is_complex: A flag to determine whether value is complex. + + Returns: + The prepared value. + """ + if value is not None and (self.field_is_complex(field) or value_is_complex): + return self.decode_complex_value(field_name, field, value) + return value + + def decode_complex_value(self, field_name: str, field: FieldInfo, value: Any) -> Any: + """ + Decode the value for a complex field + + Args: + field_name: The field name. + field: The field. + value: The value of the field that has to be prepared. + + Returns: + The decoded value for further preparation + """ + if field and ( + NoDecode in field.metadata + or (self.config.get('enable_decoding') is False and ForceDecode not in field.metadata) + ): + return value + + return json.loads(value) + + @abstractmethod + def __call__(self) -> dict[str, Any]: + pass + + +class ConfigFileSourceMixin(ABC): + def _read_files(self, files: PathType | None) -> dict[str, Any]: + if files is None: + return {} + if isinstance(files, (str, os.PathLike)): + files = [files] + vars: dict[str, Any] = {} + for file in files: + file_path = Path(file).expanduser() + if file_path.is_file(): + vars.update(self._read_file(file_path)) + return vars + + @abstractmethod + def _read_file(self, path: Path) -> dict[str, Any]: + pass + + +class DefaultSettingsSource(PydanticBaseSettingsSource): + """ + Source class for loading default object values. + + Args: + settings_cls: The Settings class. + nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields. + Defaults to `False`. + """ + + def __init__(self, settings_cls: type[BaseSettings], nested_model_default_partial_update: bool | None = None): + super().__init__(settings_cls) + self.defaults: dict[str, Any] = {} + self.nested_model_default_partial_update = ( + nested_model_default_partial_update + if nested_model_default_partial_update is not None + else self.config.get('nested_model_default_partial_update', False) + ) + if self.nested_model_default_partial_update: + for field_name, field_info in settings_cls.model_fields.items(): + alias_names, *_ = _get_alias_names(field_name, field_info) + preferred_alias = alias_names[0] + if is_dataclass(type(field_info.default)): + self.defaults[preferred_alias] = asdict(field_info.default) + elif is_model_class(type(field_info.default)): + self.defaults[preferred_alias] = field_info.default.model_dump() + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + # Nothing to do here. Only implement the return statement to make mypy happy + return None, '', False + + def __call__(self) -> dict[str, Any]: + return self.defaults + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(nested_model_default_partial_update={self.nested_model_default_partial_update})' + ) + + +class InitSettingsSource(PydanticBaseSettingsSource): + """ + Source class for loading values provided during settings class initialization. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + init_kwargs: dict[str, Any], + nested_model_default_partial_update: bool | None = None, + ): + self.init_kwargs = {} + init_kwarg_names = set(init_kwargs.keys()) + for field_name, field_info in settings_cls.model_fields.items(): + alias_names, *_ = _get_alias_names(field_name, field_info) + init_kwarg_name = init_kwarg_names & set(alias_names) + if init_kwarg_name: + preferred_alias = alias_names[0] + init_kwarg_names -= init_kwarg_name + self.init_kwargs[preferred_alias] = init_kwargs[init_kwarg_name.pop()] + self.init_kwargs.update({key: val for key, val in init_kwargs.items() if key in init_kwarg_names}) + + super().__init__(settings_cls) + self.nested_model_default_partial_update = ( + nested_model_default_partial_update + if nested_model_default_partial_update is not None + else self.config.get('nested_model_default_partial_update', False) + ) + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + # Nothing to do here. Only implement the return statement to make mypy happy + return None, '', False + + def __call__(self) -> dict[str, Any]: + return ( + TypeAdapter(dict[str, Any]).dump_python(self.init_kwargs) + if self.nested_model_default_partial_update + else self.init_kwargs + ) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(init_kwargs={self.init_kwargs!r})' + + +class PydanticBaseEnvSettingsSource(PydanticBaseSettingsSource): + def __init__( + self, + settings_cls: type[BaseSettings], + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + ) -> None: + super().__init__(settings_cls) + self.case_sensitive = case_sensitive if case_sensitive is not None else self.config.get('case_sensitive', False) + self.env_prefix = env_prefix if env_prefix is not None else self.config.get('env_prefix', '') + self.env_ignore_empty = ( + env_ignore_empty if env_ignore_empty is not None else self.config.get('env_ignore_empty', False) + ) + self.env_parse_none_str = ( + env_parse_none_str if env_parse_none_str is not None else self.config.get('env_parse_none_str') + ) + self.env_parse_enums = env_parse_enums if env_parse_enums is not None else self.config.get('env_parse_enums') + + def _apply_case_sensitive(self, value: str) -> str: + return value.lower() if not self.case_sensitive else value + + def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: + """ + Extracts field info. This info is used to get the value of field from environment variables. + + It returns a list of tuples, each tuple contains: + * field_key: The key of field that has to be used in model creation. + * env_name: The environment variable name of the field. + * value_is_complex: A flag to determine whether the value from environment variable + is complex and has to be parsed. + + Args: + field (FieldInfo): The field. + field_name (str): The field name. + + Returns: + list[tuple[str, str, bool]]: List of tuples, each tuple contains field_key, env_name, and value_is_complex. + """ + field_info: list[tuple[str, str, bool]] = [] + if isinstance(field.validation_alias, (AliasChoices, AliasPath)): + v_alias: str | list[str | int] | list[list[str | int]] | None = field.validation_alias.convert_to_aliases() + else: + v_alias = field.validation_alias + + if v_alias: + if isinstance(v_alias, list): # AliasChoices, AliasPath + for alias in v_alias: + if isinstance(alias, str): # AliasPath + field_info.append((alias, self._apply_case_sensitive(alias), True if len(alias) > 1 else False)) + elif isinstance(alias, list): # AliasChoices + first_arg = cast(str, alias[0]) # first item of an AliasChoices must be a str + field_info.append( + (first_arg, self._apply_case_sensitive(first_arg), True if len(alias) > 1 else False) + ) + else: # string validation alias + field_info.append((v_alias, self._apply_case_sensitive(v_alias), False)) + + if not v_alias or self.config.get('populate_by_name', False): + if is_union_origin(get_origin(field.annotation)) and _union_is_complex(field.annotation, field.metadata): + field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), True)) + else: + field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), False)) + + return field_info + + def _replace_field_names_case_insensitively(self, field: FieldInfo, field_values: dict[str, Any]) -> dict[str, Any]: + """ + Replace field names in values dict by looking in models fields insensitively. + + By having the following models: + + ```py + class SubSubSub(BaseModel): + VaL3: str + + class SubSub(BaseModel): + Val2: str + SUB_sub_SuB: SubSubSub + + class Sub(BaseModel): + VAL1: str + SUB_sub: SubSub + + class Settings(BaseSettings): + nested: Sub + + model_config = SettingsConfigDict(env_nested_delimiter='__') + ``` + + Then: + _replace_field_names_case_insensitively( + field, + {"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3"}}} + ) + Returns {'VAL1': 'v1', 'SUB_sub': {'Val2': 'v2', 'SUB_sub_SuB': {'VaL3': 'v3'}}} + """ + values: dict[str, Any] = {} + + for name, value in field_values.items(): + sub_model_field: FieldInfo | None = None + + annotation = field.annotation + + # If field is Optional, we need to find the actual type + if is_union_origin(get_origin(field.annotation)): + args = get_args(annotation) + if len(args) == 2 and type(None) in args: + for arg in args: + if arg is not None: + annotation = arg + break + + # This is here to make mypy happy + # Item "None" of "Optional[Type[Any]]" has no attribute "model_fields" + if not annotation or not hasattr(annotation, 'model_fields'): + values[name] = value + continue + else: + model_fields: dict[str, FieldInfo] = annotation.model_fields + + # Find field in sub model by looking in fields case insensitively + field_key: str | None = None + for sub_model_field_name, sub_model_field in model_fields.items(): + aliases, _ = _get_alias_names(sub_model_field_name, sub_model_field) + _search = (alias for alias in aliases if alias.lower() == name.lower()) + if field_key := next(_search, None): + break + + if not field_key: + values[name] = value + continue + + if ( + sub_model_field is not None + and _lenient_issubclass(sub_model_field.annotation, BaseModel) + and isinstance(value, dict) + ): + values[field_key] = self._replace_field_names_case_insensitively(sub_model_field, value) + else: + values[field_key] = value + + return values + + def _replace_env_none_type_values(self, field_value: dict[str, Any]) -> dict[str, Any]: + """ + Recursively parse values that are of "None" type(EnvNoneType) to `None` type(None). + """ + values: dict[str, Any] = {} + + for key, value in field_value.items(): + if not isinstance(value, EnvNoneType): + values[key] = value if not isinstance(value, dict) else self._replace_env_none_type_values(value) + else: + values[key] = None + + return values + + def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value, the preferred alias key for model creation, and a flag to determine whether value + is complex. + + Note: + In V3, this method should either be made public, or, this method should be removed and the + abstract method get_field_value should be updated to include a "use_preferred_alias" flag. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value, preferred key and a flag to determine whether value is complex. + """ + field_value, field_key, value_is_complex = self.get_field_value(field, field_name) + if not (value_is_complex or (self.config.get('populate_by_name', False) and (field_key == field_name))): + field_infos = self._extract_field_info(field, field_name) + preferred_key, *_ = field_infos[0] + return field_value, preferred_key, value_is_complex + return field_value, field_key, value_is_complex + + def __call__(self) -> dict[str, Any]: + data: dict[str, Any] = {} + + for field_name, field in self.settings_cls.model_fields.items(): + try: + field_value, field_key, value_is_complex = self._get_resolved_field_value(field, field_name) + except Exception as e: + raise SettingsError( + f'error getting value for field "{field_name}" from source "{self.__class__.__name__}"' + ) from e + + try: + field_value = self.prepare_field_value(field_name, field, field_value, value_is_complex) + except ValueError as e: + raise SettingsError( + f'error parsing value for field "{field_name}" from source "{self.__class__.__name__}"' + ) from e + + if field_value is not None: + if self.env_parse_none_str is not None: + if isinstance(field_value, dict): + field_value = self._replace_env_none_type_values(field_value) + elif isinstance(field_value, EnvNoneType): + field_value = None + if ( + not self.case_sensitive + # and _lenient_issubclass(field.annotation, BaseModel) + and isinstance(field_value, dict) + ): + data[field_key] = self._replace_field_names_case_insensitively(field, field_value) + else: + data[field_key] = field_value + + return data + + +__all__ = [ + 'ConfigFileSourceMixin', + 'DefaultSettingsSource', + 'InitSettingsSource', + 'PydanticBaseEnvSettingsSource', + 'PydanticBaseSettingsSource', + 'SettingsError', +] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/__init__.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/__init__.py new file mode 100644 index 000000000..31759f339 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/__init__.py @@ -0,0 +1,41 @@ +"""Package containing individual source implementations.""" + +from .aws import AWSSecretsManagerSettingsSource +from .azure import AzureKeyVaultSettingsSource +from .cli import ( + CliExplicitFlag, + CliImplicitFlag, + CliMutuallyExclusiveGroup, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + CliSuppress, +) +from .dotenv import DotEnvSettingsSource +from .env import EnvSettingsSource +from .gcp import GoogleSecretManagerSettingsSource +from .json import JsonConfigSettingsSource +from .pyproject import PyprojectTomlConfigSettingsSource +from .secrets import SecretsSettingsSource +from .toml import TomlConfigSettingsSource +from .yaml import YamlConfigSettingsSource + +__all__ = [ + 'AWSSecretsManagerSettingsSource', + 'AzureKeyVaultSettingsSource', + 'CliExplicitFlag', + 'CliImplicitFlag', + 'CliMutuallyExclusiveGroup', + 'CliPositionalArg', + 'CliSettingsSource', + 'CliSubCommand', + 'CliSuppress', + 'DotEnvSettingsSource', + 'EnvSettingsSource', + 'GoogleSecretManagerSettingsSource', + 'JsonConfigSettingsSource', + 'PyprojectTomlConfigSettingsSource', + 'SecretsSettingsSource', + 'TomlConfigSettingsSource', + 'YamlConfigSettingsSource', +] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/__init__.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..873570cafefc7c72a96c43ee10f21a7d1b5e0555 GIT binary patch literal 1181 zcma)5OHUL*5boJs_QA5iE)N&*0mKU=fYF#3H6~<1;(HTNV`q}dFw?FL-P4oqnU(eC z)j#0fn~7I{fj9jHcCshq!JA492`8(2!2}g!AHIBDU0wBcb=8MTr60k1@&4VDw>gA9 zna$zK9FW~NKwhE{VH7%HhB-WgGmha|mgPB|vpmQ8cpm3@0T*}?7kLSnct7svWnAV1 zc)gQ=d&Mk;;HfQyw$I z33wz_L~8Mi123I(^qJ*{>+2+tBvE(#2*Sv9k|YpAt=o&>GPV3HmE;!bJn&PN{B~T> zLk&h(cVb2Znp|go%NlM#x?hF~@VJvEpoVo;cLJ6w+9qpKq_G}`qoz>Qi1OL8Z0Rak zj^@x6V@fw0f^$C#b=hv?SQ4c`-sp-*R-^XOJ7BN=%{a3rM2nH$i;sJ7{iYI8Lqv~h z>yM~2%}y-E773CK!PzlRV+R%e<97WYm+FuFV**xX7CuWt(<0etAa9^xplG0Ez%;ik z8yGN9F)(Og$iT3Hs)3q;5rDgMr**;eLJ@eL&iYT4&7%8!^V>F=&IXMBM@F3wMM9#s z&8)MAne;l_3Sz54=eD4tR*5dfhXu5vbV2pH&#Ke81gdH^>RbovVU-%0&^w;@8tp<{ zw4XLEyZ055%565>6{5Bi5xXjuG)gYG_cn>58xn?Mq1pQ>w+)iq+jP@~Mk5PxZ(fue zcS1i(XyB=XmS5@(i&}y&hVg*}OP&W4m3ZD_+}ST)U`~Uy| literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/aws.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/aws.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5dab20f6e2b0c33c72dc6fdd41925c7b6f336c3 GIT binary patch literal 3233 zcma)8U2Ggz6~6PgJ3Bl3WADaJoCc4xj_pa~9}=KYHKgFWtpjy}oC>nBH68EVxZ}>w z4)@MFw%J6c1mPg6^aTZ}Lh1uA$a#P#UXgg==K-5U@@iy5P~jnOE1ZhRym0Q!tdqJy z<*xS3nRCzmJLfy!+&^V93WE0IKmP7*FNM%Y?4{kLNa1$ZK)Hh~WN{9va%G-lI`0T& z0iJ>*R;98O+C(Q&O_q~Yxh%uGWF?$bRVgc=CoS1YSJkr0A)F)$7?*M~)oeKneY}~2 zf8NSi%1yD{!K2rZmA;8A6|3t)w1;x1rOtEZ!ZNRCA2O591!>cDYrg5*HJ47od-4s_ z^K5rs=Y!0p%NHk%@z*EDzcD%WS|DHWn3w51aT@zR zOhTt?byC4Ft@?G7;Mg6zG9EY* zd5|m{l%A}5HR4YYQX_CG4pSBgZO}yJ`;IPU*tW)PCU?k}iA-Mc~8jdLVxUIKrsRS*Xmv2^hkH z#lHl0O8`w|T5A7i$xrlWX;x|tUF9t4W~MA!ij{!clB~3qglA$6Epg@KG}6_e7f=&} zCdNz~>`@5>0`*Ct#9=XPE6QD0J5F(!;?!4U*kii^p+p6;1j$E5F3&OA!;NV$-B~eGMa;xo}QQr zvY!MYU@cAXJWzt+npM2Q4-dZzc+OU!(&1Pxz{p<2fx`H~BlxPl6cm~6V(Medu&`rS z0cj-2GpjxC)(DVE)$^Bw&Tve~E^f_**&uegCpPTXt5i>fm?8`sq=$(kAOeZjJ(yEA z4BNGR!_ZTKNU?J@P}}&07}FpXorNXh7#sUJCVH7T&O`}BkTSs4q|`7d%YhcRRZ?Uw zCqS$WeMZ_x+S_0g((@o5ucAjqlq%hq`?m*9Z=4z5_}0w*f!Q_jzFcbdl-9(bmVBxTzj)9@U%Oea*fT&11)#y`OJB_k1gdQb#^Ua!T2S*-&L43-FSFKjfF&{R=2} zAomOKJlGcY5%8o3JQ)oiXm`0brC#U!7z+sbEEZu~*eBz*g<;6zSsvOWVP^G7Z(9Q^ zqpkxDtSp%a+IqY*C^Lb5pya5`Capk?v}qqGZC+1>APDmF*l$cOWFQ&u*b7Unw08f?t|mu_X`Gt>SF{}*8@%06$a zs9EUVDvWFvMz#v)HVfx|U;K0LA9^U^lhqrp;FpZOS>s4ue6YqQpib|{Mk@=8+!gH6!xhBoZ|KYBammp;RD_R z0$E@nJJ6s<_;~^nBu=fQhxRM$>`M13?O7Fnc8}9_k?1fDD^$iFh7sjS1Dno;xrjbv zQ3Ak=V2>Ea|5I_1=PNK1Wn90C9^?wQZ```El^fX14QylwKm6XWzVMrKzdpBddZLB6 zSGoRX|Ik+d+0Fj58>N?j%e4e9GY(RS0}GdjOH)%*dY%lzbSs_yvWKrceV>^nL(oAE zGr`zoPXWTx8rl?aSBSH{^jxjxv@Idcyuh48t_d~959G@rf&^Si>ckC1HX3006)>(G zjc;XFVB%9+`y$5PW@lrcaCu!OUu2UghEc6qbqC@asD|;~y6Hqw4swR^)Qu0q%6N>N zX5ve1WF{uP3U(Y|PcF<-n2MJp-s@(rOt53n28hQ$M9qBR2N#;pjolN+S}H0HHU~~J z($a^UgU`1*4oS*`uA!C$&sG8zyIV=7$*4@!iX>E`qa&6q-xqOVOP@VtHd}i z`be>O1xW}!3+gXXSg&C?_vDwNM17UK4nB;{=_CliAjj>X(;uPq4l4Z(o%|;{zJrec yUC8f4Y?%^eSvKv2ln;o^c!U|!pa+M01{BGn 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 C;$J@i literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/pyproject.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/pyproject.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8813cafd18a53a194c82dd2e988262ff0a62209 GIT binary patch literal 3050 zcmb7G-ESP#6~FVfU*278uM^uz?F_^Y8N0TTL!nLM0CiG`9Bj8X5^RH%a3T&2RD9sss zLC6X|%^Q(IG#dq4&>}{x5YNULgrg(^a#15u=*o66h(%8$Ep`=YaU8$J1^Tl|ExyKN zQ(FnCt30x}RV3CrRvmG9;n|Dgs%~J>#*U+#Yqn@xC8FZVZ|FgZ@wyb#v>e6JEz^c2 zzo;%v$EEzP8lVx8$pX6UkpGHV${yz^q{A zgFj~f91wqiC}5OjAQG&`WVtxf_&5rGnovQj5TC25ERP`u8h4ey)zp=ZRM0C-HoAnQ zsP|yY$NQO7o=b=@o@zQQs)l0QqLWt~(XbTF7B>~5D=P-J#Z_VzL`8h57{+bV0Y6{z z12^ewQJZo0O^Lz`Yi9p-2+N%ju}i zzLx%Lqdaobp$?}zq+Imo!(ctr`G!yvgSL<)%zJ&UCzR@c|rs09X-Hu=-bPA ztZ0}>tj9Z+Wt4l0Tbg1zx;hcoi79l z7onEC9)I0_o7W{1xK^ezwigc!RUE|=>{1aELi5Xu$+B+hjx0-p$J^Lg^^&1UvTB6K zd?UCGyyeAZ*oF+3Wt-*~P5mqPHfK-gp0hErbBnXNbJ*T+tYXeC65Vu84xT6iOIgEqE)0qsS}+xWh&=^3v#CP>foj!Gaf|eW1?92D zaBKNKs4*G(cnIX}4`KTL3Th+4|6f?_GZPa&2g;Iy7~gyZzg}p`TaIy*c0D zfc;f0^;!DFO}-j??6&*o<&Q7_ zRpslgYNdLE5z1*y8S3{{R%4;s*masC2SzAqZvnqUt#;aLYUN$dWj4SMa>?cYn{L9o zJXj2t1Oucy+|J-_g!`gbw1xo=MR=@Tsh@qPG~c8?Gn``@#QZUp-{#9 zAh}4(QFJV^WaG(%59_o_o+4}Z>DG$wG?Q98EuMd+Xy~f$1V_Vt@#Q==McuR=3Kh^g z%{AyNx*fEbpkYuwboc;tTAVKxRxlB*RZ-IkG-iwF(058jIEO`tV3;gX&10}0WEAm- zYNF`SW^FnBpDZ8S2t`q7icx}X%h<$(=8?y#FKb>@w|$)PB1MH@)A2alAs$aG%K>1C zg)Vu5Z>YzTO;1p*5(o-vUN^KBn22Nul#&NvD*y6-sOKrMXKkm=U;pNjqL7`v1)3p!q&TcDnk8%p0~=cm9M$=zQa2gu3xD2 zja2(ab{~Gc+V^>Y0I^nd>v}JW)R?ZeRQ&b9lS7<6d|7G9x$H52No# zKg~?l`vw6Ix_c|}hJXf#-%GxmtPPA;2gY|N&sGP{ZVUD9LpOS^C$@z~3S|avjJ`e5 zNc-|+7)+L!c_~5$X$X3qts=hE{>k$qIRcD(UGxJ=bj5kg#9oA8r$kJTrz<#m0wl;d zouH?|3jq$3pVB3LFeyes-&6`%mc4{5Lu)BP6Op~7EdQ>g7)?%8mNiS2WgldrDbQ~> zwSb+xrTMSprJ9{U#fAaxgCRcwO8uU^(R!a~9 zaDz~o17F&{H@zq=Nkd-=5;g)cg<2xCf6;ziFoITi_(%O`vY_ZDp~&N3=Rr&q>g@X{ zuz{xPz1W!`f@jGLEUEwYV=w`nGtA%7*w<+KYcz0|Wf}Hv3^C(ha^3&nPu+=(GrhYf VpS*+Mzma25a%wj*_4}x=_uq8``w##C literal 0 HcmV?d00001 diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/secrets.cpython-312.pyc b/venv/Lib/site-packages/pydantic_settings/sources/providers/__pycache__/secrets.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77b486bc4894dec15fa72a292333649cf69ec924 GIT binary patch literal 5799 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 None: + global boto3_client + global SecretsManagerClient + + try: + from boto3 import client as boto3_client + from mypy_boto3_secretsmanager.client import SecretsManagerClient + except ImportError as e: # pragma: no cover + raise ImportError( + 'AWS Secrets Manager dependencies are not installed, run `pip install pydantic-settings[aws-secrets-manager]`' + ) from e + + +class AWSSecretsManagerSettingsSource(EnvSettingsSource): + _secret_id: str + _secretsmanager_client: SecretsManagerClient # type: ignore + + def __init__( + self, + settings_cls: type[BaseSettings], + secret_id: str, + region_name: str | None = None, + case_sensitive: bool | None = True, + env_prefix: str | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + ) -> None: + import_aws_secrets_manager() + self._secretsmanager_client = boto3_client('secretsmanager', region_name=region_name) # type: ignore + self._secret_id = secret_id + super().__init__( + settings_cls, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_nested_delimiter='--', + env_ignore_empty=False, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + ) + + def _load_env_vars(self) -> Mapping[str, Optional[str]]: + response = self._secretsmanager_client.get_secret_value(SecretId=self._secret_id) # type: ignore + + return parse_env_vars( + json.loads(response['SecretString']), + self.case_sensitive, + self.env_ignore_empty, + self.env_parse_none_str, + ) + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(secret_id={self._secret_id!r}, ' + f'env_nested_delimiter={self.env_nested_delimiter!r})' + ) + + +__all__ = [ + 'AWSSecretsManagerSettingsSource', +] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/azure.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/azure.py new file mode 100644 index 000000000..04f0bee51 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/azure.py @@ -0,0 +1,118 @@ +"""Azure Key Vault settings source.""" + +from __future__ import annotations as _annotations + +from collections.abc import Iterator, Mapping +from typing import TYPE_CHECKING, Optional + +from pydantic.fields import FieldInfo + +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from azure.core.credentials import TokenCredential + from azure.core.exceptions import ResourceNotFoundError + from azure.keyvault.secrets import SecretClient + + from pydantic_settings.main import BaseSettings +else: + TokenCredential = None + ResourceNotFoundError = None + SecretClient = None + + +def import_azure_key_vault() -> None: + global TokenCredential + global SecretClient + global ResourceNotFoundError + + try: + from azure.core.credentials import TokenCredential + from azure.core.exceptions import ResourceNotFoundError + from azure.keyvault.secrets import SecretClient + except ImportError as e: # pragma: no cover + raise ImportError( + 'Azure Key Vault dependencies are not installed, run `pip install pydantic-settings[azure-key-vault]`' + ) from e + + +class AzureKeyVaultMapping(Mapping[str, Optional[str]]): + _loaded_secrets: dict[str, str | None] + _secret_client: SecretClient + _secret_names: list[str] + + def __init__( + self, + secret_client: SecretClient, + case_sensitive: bool, + ) -> None: + self._loaded_secrets = {} + self._secret_client = secret_client + self._case_sensitive = case_sensitive + self._secret_map: dict[str, str] = self._load_remote() + + def _load_remote(self) -> dict[str, str]: + secret_names: Iterator[str] = ( + secret.name for secret in self._secret_client.list_properties_of_secrets() if secret.name and secret.enabled + ) + if self._case_sensitive: + return {name: name for name in secret_names} + return {name.lower(): name for name in secret_names} + + def __getitem__(self, key: str) -> str | None: + if not self._case_sensitive: + key = key.lower() + if key not in self._loaded_secrets and key in self._secret_map: + self._loaded_secrets[key] = self._secret_client.get_secret(self._secret_map[key]).value + return self._loaded_secrets[key] + + def __len__(self) -> int: + return len(self._secret_map) + + def __iter__(self) -> Iterator[str]: + return iter(self._secret_map.keys()) + + +class AzureKeyVaultSettingsSource(EnvSettingsSource): + _url: str + _credential: TokenCredential + + def __init__( + self, + settings_cls: type[BaseSettings], + url: str, + credential: TokenCredential, + dash_to_underscore: bool = False, + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + ) -> None: + import_azure_key_vault() + self._url = url + self._credential = credential + self._dash_to_underscore = dash_to_underscore + super().__init__( + settings_cls, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_nested_delimiter='--', + env_ignore_empty=False, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + ) + + def _load_env_vars(self) -> Mapping[str, Optional[str]]: + secret_client = SecretClient(vault_url=self._url, credential=self._credential) + return AzureKeyVaultMapping(secret_client, self.case_sensitive) + + def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: + if self._dash_to_underscore: + return list((x[0], x[1].replace('_', '-'), x[2]) for x in super()._extract_field_info(field, field_name)) + return super()._extract_field_info(field, field_name) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(url={self._url!r}, env_nested_delimiter={self.env_nested_delimiter!r})' + + +__all__ = ['AzureKeyVaultMapping', 'AzureKeyVaultSettingsSource'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/cli.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/cli.py new file mode 100644 index 000000000..9e99ebcf1 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/cli.py @@ -0,0 +1,1086 @@ +"""Command-line interface settings source.""" + +from __future__ import annotations as _annotations + +import json +import re +import shlex +import sys +import typing +from argparse import ( + SUPPRESS, + ArgumentParser, + BooleanOptionalAction, + Namespace, + RawDescriptionHelpFormatter, + _SubParsersAction, +) +from collections import defaultdict +from collections.abc import Mapping, Sequence +from enum import Enum +from textwrap import dedent +from types import SimpleNamespace +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + Generic, + NoReturn, + Optional, + TypeVar, + Union, + cast, + overload, +) + +import typing_extensions +from pydantic import BaseModel, Field +from pydantic._internal._repr import Representation +from pydantic._internal._utils import is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined +from typing_extensions import get_args, get_origin +from typing_inspection import typing_objects +from typing_inspection.introspection import is_union_origin + +from ...exceptions import SettingsError +from ...utils import _lenient_issubclass, _WithArgsTypes +from ..types import NoDecode, _CliExplicitFlag, _CliImplicitFlag, _CliPositionalArg, _CliSubCommand, _CliUnknownArgs +from ..utils import ( + _annotation_contains_types, + _annotation_enum_val_to_name, + _get_alias_names, + _get_model_fields, + _is_function, + _strip_annotated, + parse_env_vars, +) +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class _CliInternalArgParser(ArgumentParser): + def __init__(self, cli_exit_on_error: bool = True, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._cli_exit_on_error = cli_exit_on_error + + def error(self, message: str) -> NoReturn: + if not self._cli_exit_on_error: + raise SettingsError(f'error parsing CLI: {message}') + super().error(message) + + +class CliMutuallyExclusiveGroup(BaseModel): + pass + + +T = TypeVar('T') +CliSubCommand = Annotated[Union[T, None], _CliSubCommand] +CliPositionalArg = Annotated[T, _CliPositionalArg] +_CliBoolFlag = TypeVar('_CliBoolFlag', bound=bool) +CliImplicitFlag = Annotated[_CliBoolFlag, _CliImplicitFlag] +CliExplicitFlag = Annotated[_CliBoolFlag, _CliExplicitFlag] +CLI_SUPPRESS = SUPPRESS +CliSuppress = Annotated[T, CLI_SUPPRESS] +CliUnknownArgs = Annotated[list[str], Field(default=[]), _CliUnknownArgs, NoDecode] + + +class CliSettingsSource(EnvSettingsSource, Generic[T]): + """ + Source class for loading settings values from CLI. + + Note: + A `CliSettingsSource` connects with a `root_parser` object by using the parser methods to add + `settings_cls` fields as command line arguments. The `CliSettingsSource` internal parser representation + is based upon the `argparse` parsing library, and therefore, requires the parser methods to support + the same attributes as their `argparse` library counterparts. + + Args: + cli_prog_name: The CLI program name to display in help text. Defaults to `None` if cli_parse_args is `None`. + Otherwise, defaults to sys.argv[0]. + cli_parse_args: The list of CLI arguments to parse. Defaults to None. + If set to `True`, defaults to sys.argv[1:]. + cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into `None` + type(None). Defaults to "null" if cli_avoid_json is `False`, and "None" if cli_avoid_json is `True`. + cli_hide_none_type: Hide `None` values in CLI help text. Defaults to `False`. + cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to `False`. + cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`. + cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions. + Defaults to `False`. + cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs. + Defaults to `True`. + cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "". + cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. + cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags. + (e.g. --flag, --no-flag). Defaults to `False`. + cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. + cli_kebab_case: CLI args use kebab case. Defaults to `False`. + cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. + case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`. + Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI + subcommands. + root_parser: The root parser object. + parse_args_method: The root parser parse args method. Defaults to `argparse.ArgumentParser.parse_args`. + add_argument_method: The root parser add argument method. Defaults to `argparse.ArgumentParser.add_argument`. + add_argument_group_method: The root parser add argument group method. + Defaults to `argparse.ArgumentParser.add_argument_group`. + add_parser_method: The root parser add new parser (sub-command) method. + Defaults to `argparse._SubParsersAction.add_parser`. + add_subparsers_method: The root parser add subparsers (sub-commands) method. + Defaults to `argparse.ArgumentParser.add_subparsers`. + formatter_class: A class for customizing the root parser help text. Defaults to `argparse.RawDescriptionHelpFormatter`. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + cli_prog_name: str | None = None, + cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, + cli_parse_none_str: str | None = None, + cli_hide_none_type: bool | None = None, + cli_avoid_json: bool | None = None, + cli_enforce_required: bool | None = None, + cli_use_class_docs_for_groups: bool | None = None, + cli_exit_on_error: bool | None = None, + cli_prefix: str | None = None, + cli_flag_prefix_char: str | None = None, + cli_implicit_flags: bool | None = None, + cli_ignore_unknown_args: bool | None = None, + cli_kebab_case: bool | None = None, + cli_shortcuts: Mapping[str, str | list[str]] | None = None, + case_sensitive: bool | None = True, + root_parser: Any = None, + parse_args_method: Callable[..., Any] | None = None, + add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument, + add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group, + add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser, + add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers, + formatter_class: Any = RawDescriptionHelpFormatter, + ) -> None: + self.cli_prog_name = ( + cli_prog_name if cli_prog_name is not None else settings_cls.model_config.get('cli_prog_name', sys.argv[0]) + ) + self.cli_parse_args = ( + cli_parse_args if cli_parse_args is not None else settings_cls.model_config.get('cli_parse_args', None) + ) + self.cli_hide_none_type = ( + cli_hide_none_type + if cli_hide_none_type is not None + else settings_cls.model_config.get('cli_hide_none_type', False) + ) + self.cli_avoid_json = ( + cli_avoid_json if cli_avoid_json is not None else settings_cls.model_config.get('cli_avoid_json', False) + ) + if not cli_parse_none_str: + cli_parse_none_str = 'None' if self.cli_avoid_json is True else 'null' + self.cli_parse_none_str = cli_parse_none_str + self.cli_enforce_required = ( + cli_enforce_required + if cli_enforce_required is not None + else settings_cls.model_config.get('cli_enforce_required', False) + ) + self.cli_use_class_docs_for_groups = ( + cli_use_class_docs_for_groups + if cli_use_class_docs_for_groups is not None + else settings_cls.model_config.get('cli_use_class_docs_for_groups', False) + ) + self.cli_exit_on_error = ( + cli_exit_on_error + if cli_exit_on_error is not None + else settings_cls.model_config.get('cli_exit_on_error', True) + ) + self.cli_prefix = cli_prefix if cli_prefix is not None else settings_cls.model_config.get('cli_prefix', '') + self.cli_flag_prefix_char = ( + cli_flag_prefix_char + if cli_flag_prefix_char is not None + else settings_cls.model_config.get('cli_flag_prefix_char', '-') + ) + self._cli_flag_prefix = self.cli_flag_prefix_char * 2 + if self.cli_prefix: + if cli_prefix.startswith('.') or cli_prefix.endswith('.') or not cli_prefix.replace('.', '').isidentifier(): # type: ignore + raise SettingsError(f'CLI settings source prefix is invalid: {cli_prefix}') + self.cli_prefix += '.' + self.cli_implicit_flags = ( + cli_implicit_flags + if cli_implicit_flags is not None + else settings_cls.model_config.get('cli_implicit_flags', False) + ) + self.cli_ignore_unknown_args = ( + cli_ignore_unknown_args + if cli_ignore_unknown_args is not None + else settings_cls.model_config.get('cli_ignore_unknown_args', False) + ) + self.cli_kebab_case = ( + cli_kebab_case if cli_kebab_case is not None else settings_cls.model_config.get('cli_kebab_case', False) + ) + self.cli_shortcuts = ( + cli_shortcuts if cli_shortcuts is not None else settings_cls.model_config.get('cli_shortcuts', None) + ) + + case_sensitive = case_sensitive if case_sensitive is not None else True + if not case_sensitive and root_parser is not None: + raise SettingsError('Case-insensitive matching is only supported on the internal root parser') + + super().__init__( + settings_cls, + env_nested_delimiter='.', + env_parse_none_str=self.cli_parse_none_str, + env_parse_enums=True, + env_prefix=self.cli_prefix, + case_sensitive=case_sensitive, + ) + + root_parser = ( + _CliInternalArgParser( + cli_exit_on_error=self.cli_exit_on_error, + prog=self.cli_prog_name, + description=None if settings_cls.__doc__ is None else dedent(settings_cls.__doc__), + formatter_class=formatter_class, + prefix_chars=self.cli_flag_prefix_char, + allow_abbrev=False, + ) + if root_parser is None + else root_parser + ) + self._connect_root_parser( + root_parser=root_parser, + parse_args_method=parse_args_method, + add_argument_method=add_argument_method, + add_argument_group_method=add_argument_group_method, + add_parser_method=add_parser_method, + add_subparsers_method=add_subparsers_method, + formatter_class=formatter_class, + ) + if self.cli_parse_args not in (None, False): + if self.cli_parse_args is True: + self.cli_parse_args = sys.argv[1:] + elif not isinstance(self.cli_parse_args, (list, tuple)): + raise SettingsError( + f'cli_parse_args must be a list or tuple of strings, received {type(self.cli_parse_args)}' + ) + self._load_env_vars(parsed_args=self._parse_args(self.root_parser, self.cli_parse_args)) + + @overload + def __call__(self) -> dict[str, Any]: ... + + @overload + def __call__(self, *, args: list[str] | tuple[str, ...] | bool) -> CliSettingsSource[T]: + """ + Parse and load the command line arguments list into the CLI settings source. + + Args: + args: + The command line arguments to parse and load. Defaults to `None`, which means do not parse + command line arguments. If set to `True`, defaults to sys.argv[1:]. If set to `False`, does + not parse command line arguments. + + Returns: + CliSettingsSource: The object instance itself. + """ + ... + + @overload + def __call__(self, *, parsed_args: Namespace | SimpleNamespace | dict[str, Any]) -> CliSettingsSource[T]: + """ + Loads parsed command line arguments into the CLI settings source. + + Note: + The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary + (e.g., vars(argparse.Namespace)) format. + + Args: + parsed_args: The parsed args to load. + + Returns: + CliSettingsSource: The object instance itself. + """ + ... + + def __call__( + self, + *, + args: list[str] | tuple[str, ...] | bool | None = None, + parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None, + ) -> dict[str, Any] | CliSettingsSource[T]: + if args is not None and parsed_args is not None: + raise SettingsError('`args` and `parsed_args` are mutually exclusive') + elif args is not None: + if args is False: + return self._load_env_vars(parsed_args={}) + if args is True: + args = sys.argv[1:] + return self._load_env_vars(parsed_args=self._parse_args(self.root_parser, args)) + elif parsed_args is not None: + return self._load_env_vars(parsed_args=parsed_args) + else: + return super().__call__() + + @overload + def _load_env_vars(self) -> Mapping[str, str | None]: ... + + @overload + def _load_env_vars(self, *, parsed_args: Namespace | SimpleNamespace | dict[str, Any]) -> CliSettingsSource[T]: + """ + Loads the parsed command line arguments into the CLI environment settings variables. + + Note: + The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary + (e.g., vars(argparse.Namespace)) format. + + Args: + parsed_args: The parsed args to load. + + Returns: + CliSettingsSource: The object instance itself. + """ + ... + + def _load_env_vars( + self, *, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None + ) -> Mapping[str, str | None] | CliSettingsSource[T]: + if parsed_args is None: + return {} + + if isinstance(parsed_args, (Namespace, SimpleNamespace)): + parsed_args = vars(parsed_args) + + selected_subcommands: list[str] = [] + for field_name, val in parsed_args.items(): + if isinstance(val, list): + parsed_args[field_name] = self._merge_parsed_list(val, field_name) + elif field_name.endswith(':subcommand') and val is not None: + subcommand_name = field_name.split(':')[0] + val + subcommand_dest = self._cli_subcommands[field_name][subcommand_name] + selected_subcommands.append(subcommand_dest) + + for subcommands in self._cli_subcommands.values(): + for subcommand_dest in subcommands.values(): + if subcommand_dest not in selected_subcommands: + parsed_args[subcommand_dest] = self.cli_parse_none_str + + parsed_args = { + key: val + for key, val in parsed_args.items() + if not key.endswith(':subcommand') and val is not PydanticUndefined + } + if selected_subcommands: + last_selected_subcommand = max(selected_subcommands, key=len) + if not any(field_name for field_name in parsed_args.keys() if f'{last_selected_subcommand}.' in field_name): + parsed_args[last_selected_subcommand] = '{}' + + parsed_args.update(self._cli_unknown_args) + + self.env_vars = parse_env_vars( + cast(Mapping[str, str], parsed_args), + self.case_sensitive, + self.env_ignore_empty, + self.cli_parse_none_str, + ) + + return self + + def _get_merge_parsed_list_types( + self, parsed_list: list[str], field_name: str + ) -> tuple[Optional[type], Optional[type]]: + merge_type = self._cli_dict_args.get(field_name, list) + if ( + merge_type is list + or not is_union_origin(get_origin(merge_type)) + or not any( + type_ + for type_ in get_args(merge_type) + if type_ is not type(None) and get_origin(type_) not in (dict, Mapping) + ) + ): + inferred_type = merge_type + else: + inferred_type = list if parsed_list and (len(parsed_list) > 1 or parsed_list[0].startswith('[')) else str + + return merge_type, inferred_type + + def _merge_parsed_list(self, parsed_list: list[str], field_name: str) -> str: + try: + merged_list: list[str] = [] + is_last_consumed_a_value = False + merge_type, inferred_type = self._get_merge_parsed_list_types(parsed_list, field_name) + for val in parsed_list: + if not isinstance(val, str): + # If val is not a string, it's from an external parser and we can ignore parsing the rest of the + # list. + break + val = val.strip() + if val.startswith('[') and val.endswith(']'): + val = val[1:-1].strip() + while val: + val = val.strip() + if val.startswith(','): + val = self._consume_comma(val, merged_list, is_last_consumed_a_value) + is_last_consumed_a_value = False + else: + if val.startswith('{') or val.startswith('['): + val = self._consume_object_or_array(val, merged_list) + else: + try: + val = self._consume_string_or_number(val, merged_list, merge_type) + except ValueError as e: + if merge_type is inferred_type: + raise e + merge_type = inferred_type + val = self._consume_string_or_number(val, merged_list, merge_type) + is_last_consumed_a_value = True + if not is_last_consumed_a_value: + val = self._consume_comma(val, merged_list, is_last_consumed_a_value) + + if merge_type is str: + return merged_list[0] + elif merge_type is list: + return f'[{",".join(merged_list)}]' + else: + merged_dict: dict[str, str] = {} + for item in merged_list: + merged_dict.update(json.loads(item)) + return json.dumps(merged_dict) + except Exception as e: + raise SettingsError(f'Parsing error encountered for {field_name}: {e}') + + def _consume_comma(self, item: str, merged_list: list[str], is_last_consumed_a_value: bool) -> str: + if not is_last_consumed_a_value: + merged_list.append('""') + return item[1:] + + def _consume_object_or_array(self, item: str, merged_list: list[str]) -> str: + count = 1 + close_delim = '}' if item.startswith('{') else ']' + in_str = False + for consumed in range(1, len(item)): + if item[consumed] == '"' and item[consumed - 1] != '\\': + in_str = not in_str + elif in_str: + continue + elif item[consumed] in ('{', '['): + count += 1 + elif item[consumed] in ('}', ']'): + count -= 1 + if item[consumed] == close_delim and count == 0: + merged_list.append(item[: consumed + 1]) + return item[consumed + 1 :] + raise SettingsError(f'Missing end delimiter "{close_delim}"') + + def _consume_string_or_number(self, item: str, merged_list: list[str], merge_type: type[Any] | None) -> str: + consumed = 0 if merge_type is not str else len(item) + is_find_end_quote = False + while consumed < len(item): + if item[consumed] == '"' and (consumed == 0 or item[consumed - 1] != '\\'): + is_find_end_quote = not is_find_end_quote + if not is_find_end_quote and item[consumed] == ',': + break + consumed += 1 + if is_find_end_quote: + raise SettingsError('Mismatched quotes') + val_string = item[:consumed].strip() + if merge_type in (list, str): + try: + float(val_string) + except ValueError: + if val_string == self.cli_parse_none_str: + val_string = 'null' + if val_string not in ('true', 'false', 'null') and not val_string.startswith('"'): + val_string = f'"{val_string}"' + merged_list.append(val_string) + else: + key, val = (kv for kv in val_string.split('=', 1)) + if key.startswith('"') and not key.endswith('"') and not val.startswith('"') and val.endswith('"'): + raise ValueError(f'Dictionary key=val parameter is a quoted string: {val_string}') + key, val = key.strip('"'), val.strip('"') + merged_list.append(json.dumps({key: val})) + return item[consumed:] + + def _get_sub_models(self, model: type[BaseModel], field_name: str, field_info: FieldInfo) -> list[type[BaseModel]]: + field_types: tuple[Any, ...] = ( + (field_info.annotation,) if not get_args(field_info.annotation) else get_args(field_info.annotation) + ) + if self.cli_hide_none_type: + field_types = tuple([type_ for type_ in field_types if type_ is not type(None)]) + + sub_models: list[type[BaseModel]] = [] + for type_ in field_types: + if _annotation_contains_types(type_, (_CliSubCommand,), is_include_origin=False): + raise SettingsError(f'CliSubCommand is not outermost annotation for {model.__name__}.{field_name}') + elif _annotation_contains_types(type_, (_CliPositionalArg,), is_include_origin=False): + raise SettingsError(f'CliPositionalArg is not outermost annotation for {model.__name__}.{field_name}') + if is_model_class(_strip_annotated(type_)) or is_pydantic_dataclass(_strip_annotated(type_)): + sub_models.append(_strip_annotated(type_)) + return sub_models + + def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, field_info: FieldInfo) -> None: + if _CliImplicitFlag in field_info.metadata: + cli_flag_name = 'CliImplicitFlag' + elif _CliExplicitFlag in field_info.metadata: + cli_flag_name = 'CliExplicitFlag' + else: + return + + if field_info.annotation is not bool: + raise SettingsError(f'{cli_flag_name} argument {model.__name__}.{field_name} is not of type bool') + + def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]]: + positional_variadic_arg = [] + positional_args, subcommand_args, optional_args = [], [], [] + for field_name, field_info in _get_model_fields(model).items(): + if _CliSubCommand in field_info.metadata: + if not field_info.is_required(): + raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has a default value') + else: + alias_names, *_ = _get_alias_names(field_name, field_info) + if len(alias_names) > 1: + raise SettingsError(f'subcommand argument {model.__name__}.{field_name} has multiple aliases') + field_types = [type_ for type_ in get_args(field_info.annotation) if type_ is not type(None)] + for field_type in field_types: + if not (is_model_class(field_type) or is_pydantic_dataclass(field_type)): + raise SettingsError( + f'subcommand argument {model.__name__}.{field_name} has type not derived from BaseModel' + ) + subcommand_args.append((field_name, field_info)) + elif _CliPositionalArg in field_info.metadata: + alias_names, *_ = _get_alias_names(field_name, field_info) + if len(alias_names) > 1: + raise SettingsError(f'positional argument {model.__name__}.{field_name} has multiple aliases') + is_append_action = _annotation_contains_types( + field_info.annotation, (list, set, dict, Sequence, Mapping), is_strip_annotated=True + ) + if not is_append_action: + positional_args.append((field_name, field_info)) + else: + positional_variadic_arg.append((field_name, field_info)) + else: + self._verify_cli_flag_annotations(model, field_name, field_info) + optional_args.append((field_name, field_info)) + + if positional_variadic_arg: + if len(positional_variadic_arg) > 1: + field_names = ', '.join([name for name, info in positional_variadic_arg]) + raise SettingsError(f'{model.__name__} has multiple variadic positional arguments: {field_names}') + elif subcommand_args: + field_names = ', '.join([name for name, info in positional_variadic_arg + subcommand_args]) + raise SettingsError( + f'{model.__name__} has variadic positional arguments and subcommand arguments: {field_names}' + ) + + return positional_args + positional_variadic_arg + subcommand_args + optional_args + + @property + def root_parser(self) -> T: + """The connected root parser instance.""" + return self._root_parser + + def _connect_parser_method( + self, parser_method: Callable[..., Any] | None, method_name: str, *args: Any, **kwargs: Any + ) -> Callable[..., Any]: + if ( + parser_method is not None + and self.case_sensitive is False + and method_name == 'parse_args_method' + and isinstance(self._root_parser, _CliInternalArgParser) + ): + + def parse_args_insensitive_method( + root_parser: _CliInternalArgParser, + args: list[str] | tuple[str, ...] | None = None, + namespace: Namespace | None = None, + ) -> Any: + insensitive_args = [] + for arg in shlex.split(shlex.join(args)) if args else []: + flag_prefix = rf'\{self.cli_flag_prefix_char}{{1,2}}' + matched = re.match(rf'^({flag_prefix}[^\s=]+)(.*)', arg) + if matched: + arg = matched.group(1).lower() + matched.group(2) + insensitive_args.append(arg) + return parser_method(root_parser, insensitive_args, namespace) + + return parse_args_insensitive_method + + elif parser_method is None: + + def none_parser_method(*args: Any, **kwargs: Any) -> Any: + raise SettingsError( + f'cannot connect CLI settings source root parser: {method_name} is set to `None` but is needed for connecting' + ) + + return none_parser_method + + else: + return parser_method + + def _connect_group_method(self, add_argument_group_method: Callable[..., Any] | None) -> Callable[..., Any]: + add_argument_group = self._connect_parser_method(add_argument_group_method, 'add_argument_group_method') + + def add_group_method(parser: Any, **kwargs: Any) -> Any: + if not kwargs.pop('_is_cli_mutually_exclusive_group'): + kwargs.pop('required') + return add_argument_group(parser, **kwargs) + else: + main_group_kwargs = {arg: kwargs.pop(arg) for arg in ['title', 'description'] if arg in kwargs} + main_group_kwargs['title'] += ' (mutually exclusive)' + group = add_argument_group(parser, **main_group_kwargs) + if not hasattr(group, 'add_mutually_exclusive_group'): + raise SettingsError( + 'cannot connect CLI settings source root parser: ' + 'group object is missing add_mutually_exclusive_group but is needed for connecting' + ) + return group.add_mutually_exclusive_group(**kwargs) + + return add_group_method + + def _connect_root_parser( + self, + root_parser: T, + parse_args_method: Callable[..., Any] | None, + add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument, + add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group, + add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser, + add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers, + formatter_class: Any = RawDescriptionHelpFormatter, + ) -> None: + self._cli_unknown_args: dict[str, list[str]] = {} + + def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace: + args, unknown_args = ArgumentParser.parse_known_args(*args, **kwargs) + for dest in self._cli_unknown_args: + self._cli_unknown_args[dest] = unknown_args + return cast(Namespace, args) + + self._root_parser = root_parser + if parse_args_method is None: + parse_args_method = _parse_known_args if self.cli_ignore_unknown_args else ArgumentParser.parse_args + self._parse_args = self._connect_parser_method(parse_args_method, 'parse_args_method') + self._add_argument = self._connect_parser_method(add_argument_method, 'add_argument_method') + self._add_group = self._connect_group_method(add_argument_group_method) + self._add_parser = self._connect_parser_method(add_parser_method, 'add_parser_method') + self._add_subparsers = self._connect_parser_method(add_subparsers_method, 'add_subparsers_method') + self._formatter_class = formatter_class + self._cli_dict_args: dict[str, type[Any] | None] = {} + self._cli_subcommands: defaultdict[str, dict[str, str]] = defaultdict(dict) + self._add_parser_args( + parser=self.root_parser, + model=self.settings_cls, + added_args=[], + arg_prefix=self.env_prefix, + subcommand_prefix=self.env_prefix, + group=None, + alias_prefixes=[], + model_default=PydanticUndefined, + ) + + def _add_parser_args( + self, + parser: Any, + model: type[BaseModel], + added_args: list[str], + arg_prefix: str, + subcommand_prefix: str, + group: Any, + alias_prefixes: list[str], + model_default: Any, + is_model_suppressed: bool = False, + ) -> ArgumentParser: + subparsers: Any = None + alias_path_args: dict[str, str] = {} + # Ignore model default if the default is a model and not a subclass of the current model. + model_default = ( + None + if ( + (is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default))) + and not issubclass(type(model_default), model) + ) + else model_default + ) + for field_name, field_info in self._sort_arg_fields(model): + sub_models: list[type[BaseModel]] = self._get_sub_models(model, field_name, field_info) + alias_names, is_alias_path_only = _get_alias_names( + field_name, field_info, alias_path_args=alias_path_args, case_sensitive=self.case_sensitive + ) + preferred_alias = alias_names[0] + if _CliSubCommand in field_info.metadata: + for model in sub_models: + subcommand_alias = self._check_kebab_name( + model.__name__ if len(sub_models) > 1 else preferred_alias + ) + subcommand_name = f'{arg_prefix}{subcommand_alias}' + subcommand_dest = f'{arg_prefix}{preferred_alias}' + self._cli_subcommands[f'{arg_prefix}:subcommand'][subcommand_name] = subcommand_dest + + subcommand_help = None if len(sub_models) > 1 else field_info.description + if self.cli_use_class_docs_for_groups: + subcommand_help = None if model.__doc__ is None else dedent(model.__doc__) + + subparsers = ( + self._add_subparsers( + parser, + title='subcommands', + dest=f'{arg_prefix}:subcommand', + description=field_info.description if len(sub_models) > 1 else None, + ) + if subparsers is None + else subparsers + ) + + if hasattr(subparsers, 'metavar'): + subparsers.metavar = ( + f'{subparsers.metavar[:-1]},{subcommand_alias}}}' + if subparsers.metavar + else f'{{{subcommand_alias}}}' + ) + + self._add_parser_args( + parser=self._add_parser( + subparsers, + subcommand_alias, + help=subcommand_help, + formatter_class=self._formatter_class, + description=None if model.__doc__ is None else dedent(model.__doc__), + ), + model=model, + added_args=[], + arg_prefix=f'{arg_prefix}{preferred_alias}.', + subcommand_prefix=f'{subcommand_prefix}{preferred_alias}.', + group=None, + alias_prefixes=[], + model_default=PydanticUndefined, + ) + else: + flag_prefix: str = self._cli_flag_prefix + is_append_action = _annotation_contains_types( + field_info.annotation, (list, set, dict, Sequence, Mapping), is_strip_annotated=True + ) + is_parser_submodel = sub_models and not is_append_action + kwargs: dict[str, Any] = {} + kwargs['default'] = CLI_SUPPRESS + kwargs['help'] = self._help_format(field_name, field_info, model_default, is_model_suppressed) + kwargs['metavar'] = self._metavar_format(field_info.annotation) + kwargs['required'] = ( + self.cli_enforce_required and field_info.is_required() and model_default is PydanticUndefined + ) + kwargs['dest'] = ( + # Strip prefix if validation alias is set and value is not complex. + # Related https://github.com/pydantic/pydantic-settings/pull/25 + f'{arg_prefix}{preferred_alias}'[self.env_prefix_len :] + if arg_prefix and field_info.validation_alias is not None and not is_parser_submodel + else f'{arg_prefix}{preferred_alias}' + ) + + arg_names = self._get_arg_names(arg_prefix, subcommand_prefix, alias_prefixes, alias_names, added_args) + if not arg_names or (kwargs['dest'] in added_args): + continue + + self._convert_append_action(kwargs, field_info, is_append_action) + + if _CliPositionalArg in field_info.metadata: + arg_names, flag_prefix = self._convert_positional_arg( + kwargs, field_info, preferred_alias, model_default + ) + + self._convert_bool_flag(kwargs, field_info, model_default) + + if is_parser_submodel: + self._add_parser_submodels( + parser, + model, + sub_models, + added_args, + arg_prefix, + subcommand_prefix, + flag_prefix, + arg_names, + kwargs, + field_name, + field_info, + alias_names, + model_default=model_default, + is_model_suppressed=is_model_suppressed, + ) + elif _CliUnknownArgs in field_info.metadata: + self._cli_unknown_args[kwargs['dest']] = [] + elif not is_alias_path_only: + if group is not None: + if isinstance(group, dict): + group = self._add_group(parser, **group) + added_args += list(arg_names) + self._add_argument( + group, *(f'{flag_prefix[: len(name)]}{name}' for name in arg_names), **kwargs + ) + else: + added_args += list(arg_names) + self._add_argument( + parser, *(f'{flag_prefix[: len(name)]}{name}' for name in arg_names), **kwargs + ) + + self._add_parser_alias_paths(parser, alias_path_args, added_args, arg_prefix, subcommand_prefix, group) + return parser + + def _check_kebab_name(self, name: str) -> str: + if self.cli_kebab_case: + return name.replace('_', '-') + return name + + def _convert_append_action(self, kwargs: dict[str, Any], field_info: FieldInfo, is_append_action: bool) -> None: + if is_append_action: + kwargs['action'] = 'append' + if _annotation_contains_types(field_info.annotation, (dict, Mapping), is_strip_annotated=True): + self._cli_dict_args[kwargs['dest']] = field_info.annotation + + def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None: + if kwargs['metavar'] == 'bool': + if (self.cli_implicit_flags or _CliImplicitFlag in field_info.metadata) and ( + _CliExplicitFlag not in field_info.metadata + ): + del kwargs['metavar'] + kwargs['action'] = BooleanOptionalAction + + def _convert_positional_arg( + self, kwargs: dict[str, Any], field_info: FieldInfo, preferred_alias: str, model_default: Any + ) -> tuple[list[str], str]: + flag_prefix = '' + arg_names = [kwargs['dest']] + kwargs['default'] = PydanticUndefined + kwargs['metavar'] = self._check_kebab_name(preferred_alias.upper()) + + # Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in + # conjunction with model_default instead of the derived kwargs['required']. + is_required = field_info.is_required() and model_default is PydanticUndefined + if kwargs.get('action') == 'append': + del kwargs['action'] + kwargs['nargs'] = '+' if is_required else '*' + elif not is_required: + kwargs['nargs'] = '?' + + del kwargs['dest'] + del kwargs['required'] + return arg_names, flag_prefix + + def _get_arg_names( + self, + arg_prefix: str, + subcommand_prefix: str, + alias_prefixes: list[str], + alias_names: tuple[str, ...], + added_args: list[str], + ) -> list[str]: + arg_names: list[str] = [] + for prefix in [arg_prefix] + alias_prefixes: + for name in alias_names: + arg_name = self._check_kebab_name( + f'{prefix}{name}' + if subcommand_prefix == self.env_prefix + else f'{prefix.replace(subcommand_prefix, "", 1)}{name}' + ) + if arg_name not in added_args: + arg_names.append(arg_name) + + if self.cli_shortcuts: + for target, aliases in self.cli_shortcuts.items(): + if target in arg_names: + alias_list = [aliases] if isinstance(aliases, str) else aliases + arg_names.extend(alias for alias in alias_list if alias not in added_args) + + return arg_names + + def _add_parser_submodels( + self, + parser: Any, + model: type[BaseModel], + sub_models: list[type[BaseModel]], + added_args: list[str], + arg_prefix: str, + subcommand_prefix: str, + flag_prefix: str, + arg_names: list[str], + kwargs: dict[str, Any], + field_name: str, + field_info: FieldInfo, + alias_names: tuple[str, ...], + model_default: Any, + is_model_suppressed: bool, + ) -> None: + if issubclass(model, CliMutuallyExclusiveGroup): + # Argparse has deprecated "calling add_argument_group() or add_mutually_exclusive_group() on a + # mutually exclusive group" (https://docs.python.org/3/library/argparse.html#mutual-exclusion). + # Since nested models result in a group add, raise an exception for nested models in a mutually + # exclusive group. + raise SettingsError('cannot have nested models in a CliMutuallyExclusiveGroup') + + model_group: Any = None + model_group_kwargs: dict[str, Any] = {} + model_group_kwargs['title'] = f'{arg_names[0]} options' + model_group_kwargs['description'] = field_info.description + model_group_kwargs['required'] = kwargs['required'] + model_group_kwargs['_is_cli_mutually_exclusive_group'] = any( + issubclass(model, CliMutuallyExclusiveGroup) for model in sub_models + ) + if model_group_kwargs['_is_cli_mutually_exclusive_group'] and len(sub_models) > 1: + raise SettingsError('cannot use union with CliMutuallyExclusiveGroup') + if self.cli_use_class_docs_for_groups and len(sub_models) == 1: + model_group_kwargs['description'] = None if sub_models[0].__doc__ is None else dedent(sub_models[0].__doc__) + + if model_default is not PydanticUndefined: + if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): + model_default = getattr(model_default, field_name) + else: + if field_info.default is not PydanticUndefined: + model_default = field_info.default + elif field_info.default_factory is not None: + model_default = field_info.default_factory + if model_default is None: + desc_header = f'default: {self.cli_parse_none_str} (undefined)' + if model_group_kwargs['description'] is not None: + model_group_kwargs['description'] = dedent(f'{desc_header}\n{model_group_kwargs["description"]}') + else: + model_group_kwargs['description'] = desc_header + + preferred_alias = alias_names[0] + is_model_suppressed = self._is_field_suppressed(field_info) or is_model_suppressed + if is_model_suppressed: + model_group_kwargs['description'] = CLI_SUPPRESS + if not self.cli_avoid_json: + added_args.append(arg_names[0]) + kwargs['nargs'] = '?' + kwargs['const'] = '{}' + kwargs['help'] = ( + CLI_SUPPRESS if is_model_suppressed else f'set {arg_names[0]} from JSON string (default: {{}})' + ) + model_group = self._add_group(parser, **model_group_kwargs) + self._add_argument(model_group, *(f'{flag_prefix}{name}' for name in arg_names), **kwargs) + for model in sub_models: + self._add_parser_args( + parser=parser, + model=model, + added_args=added_args, + arg_prefix=f'{arg_prefix}{preferred_alias}.', + subcommand_prefix=subcommand_prefix, + group=model_group if model_group else model_group_kwargs, + alias_prefixes=[f'{arg_prefix}{name}.' for name in alias_names[1:]], + model_default=model_default, + is_model_suppressed=is_model_suppressed, + ) + + def _add_parser_alias_paths( + self, + parser: Any, + alias_path_args: dict[str, str], + added_args: list[str], + arg_prefix: str, + subcommand_prefix: str, + group: Any, + ) -> None: + if alias_path_args: + context = parser + if group is not None: + context = self._add_group(parser, **group) if isinstance(group, dict) else group + is_nested_alias_path = arg_prefix.endswith('.') + arg_prefix = arg_prefix[:-1] if is_nested_alias_path else arg_prefix + for name, metavar in alias_path_args.items(): + name = '' if is_nested_alias_path else name + arg_name = ( + f'{arg_prefix}{name}' + if subcommand_prefix == self.env_prefix + else f'{arg_prefix.replace(subcommand_prefix, "", 1)}{name}' + ) + kwargs: dict[str, Any] = {} + kwargs['default'] = CLI_SUPPRESS + kwargs['help'] = 'pydantic alias path' + kwargs['dest'] = f'{arg_prefix}{name}' + if metavar == 'dict' or is_nested_alias_path: + kwargs['metavar'] = 'dict' + else: + kwargs['action'] = 'append' + kwargs['metavar'] = 'list' + if arg_name not in added_args: + added_args.append(arg_name) + self._add_argument(context, f'{self._cli_flag_prefix}{arg_name}', **kwargs) + + def _get_modified_args(self, obj: Any) -> tuple[str, ...]: + if not self.cli_hide_none_type: + return get_args(obj) + else: + return tuple([type_ for type_ in get_args(obj) if type_ is not type(None)]) + + def _metavar_format_choices(self, args: list[str], obj_qualname: str | None = None) -> str: + if 'JSON' in args: + args = args[: args.index('JSON') + 1] + [arg for arg in args[args.index('JSON') + 1 :] if arg != 'JSON'] + metavar = ','.join(args) + if obj_qualname: + return f'{obj_qualname}[{metavar}]' + else: + return metavar if len(args) == 1 else f'{{{metavar}}}' + + def _metavar_format_recurse(self, obj: Any) -> str: + """Pretty metavar representation of a type. Adapts logic from `pydantic._repr.display_as_type`.""" + obj = _strip_annotated(obj) + if _is_function(obj): + # If function is locally defined use __name__ instead of __qualname__ + return obj.__name__ if '' in obj.__qualname__ else obj.__qualname__ + elif obj is ...: + return '...' + elif isinstance(obj, Representation): + return repr(obj) + elif typing_objects.is_typealiastype(obj): + return str(obj) + + origin = get_origin(obj) + if origin is None and not isinstance(obj, (type, typing.ForwardRef, typing_extensions.ForwardRef)): + obj = obj.__class__ + + if is_union_origin(origin): + return self._metavar_format_choices(list(map(self._metavar_format_recurse, self._get_modified_args(obj)))) + elif typing_objects.is_literal(origin): + return self._metavar_format_choices(list(map(str, self._get_modified_args(obj)))) + elif _lenient_issubclass(obj, Enum): + return self._metavar_format_choices([val.name for val in obj]) + elif isinstance(obj, _WithArgsTypes): + return self._metavar_format_choices( + list(map(self._metavar_format_recurse, self._get_modified_args(obj))), + obj_qualname=obj.__qualname__ if hasattr(obj, '__qualname__') else str(obj), + ) + elif obj is type(None): + return self.cli_parse_none_str + elif is_model_class(obj): + return 'JSON' + elif isinstance(obj, type): + return obj.__qualname__ + else: + return repr(obj).replace('typing.', '').replace('typing_extensions.', '') + + def _metavar_format(self, obj: Any) -> str: + return self._metavar_format_recurse(obj).replace(', ', ',') + + def _help_format( + self, field_name: str, field_info: FieldInfo, model_default: Any, is_model_suppressed: bool + ) -> str: + _help = field_info.description if field_info.description else '' + if is_model_suppressed or self._is_field_suppressed(field_info): + return CLI_SUPPRESS + + if field_info.is_required() and model_default in (PydanticUndefined, None): + if _CliPositionalArg not in field_info.metadata: + ifdef = 'ifdef: ' if model_default is None else '' + _help += f' ({ifdef}required)' if _help else f'({ifdef}required)' + else: + default = f'(default: {self.cli_parse_none_str})' + if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)): + default = f'(default: {getattr(model_default, field_name)})' + elif model_default not in (PydanticUndefined, None) and _is_function(model_default): + default = f'(default factory: {self._metavar_format(model_default)})' + elif field_info.default not in (PydanticUndefined, None): + enum_name = _annotation_enum_val_to_name(field_info.annotation, field_info.default) + default = f'(default: {field_info.default if enum_name is None else enum_name})' + elif field_info.default_factory is not None: + default = f'(default factory: {self._metavar_format(field_info.default_factory)})' + _help += f' {default}' if _help else default + return _help.replace('%', '%%') if issubclass(type(self._root_parser), ArgumentParser) else _help + + def _is_field_suppressed(self, field_info: FieldInfo) -> bool: + _help = field_info.description if field_info.description else '' + return _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info.metadata diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/dotenv.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/dotenv.py new file mode 100644 index 000000000..d953f5f0b --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/dotenv.py @@ -0,0 +1,168 @@ +"""Dotenv file settings source.""" + +from __future__ import annotations as _annotations + +import os +import warnings +from collections.abc import Mapping +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from dotenv import dotenv_values +from pydantic._internal._typing_extra import ( # type: ignore[attr-defined] + get_origin, +) +from typing_inspection.introspection import is_union_origin + +from ..types import ENV_FILE_SENTINEL, DotenvType +from ..utils import ( + _annotation_is_complex, + _union_is_complex, + parse_env_vars, +) +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class DotEnvSettingsSource(EnvSettingsSource): + """ + Source class for loading settings values from env files. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + env_file: DotenvType | None = ENV_FILE_SENTINEL, + env_file_encoding: str | None = None, + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_nested_delimiter: str | None = None, + env_nested_max_split: int | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + ) -> None: + self.env_file = env_file if env_file != ENV_FILE_SENTINEL else settings_cls.model_config.get('env_file') + self.env_file_encoding = ( + env_file_encoding if env_file_encoding is not None else settings_cls.model_config.get('env_file_encoding') + ) + super().__init__( + settings_cls, + case_sensitive, + env_prefix, + env_nested_delimiter, + env_nested_max_split, + env_ignore_empty, + env_parse_none_str, + env_parse_enums, + ) + + def _load_env_vars(self) -> Mapping[str, str | None]: + return self._read_env_files() + + @staticmethod + def _static_read_env_file( + file_path: Path, + *, + encoding: str | None = None, + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, + ) -> Mapping[str, str | None]: + file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8') + return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str) + + def _read_env_file( + self, + file_path: Path, + ) -> Mapping[str, str | None]: + return self._static_read_env_file( + file_path, + encoding=self.env_file_encoding, + case_sensitive=self.case_sensitive, + ignore_empty=self.env_ignore_empty, + parse_none_str=self.env_parse_none_str, + ) + + def _read_env_files(self) -> Mapping[str, str | None]: + env_files = self.env_file + if env_files is None: + return {} + + if isinstance(env_files, (str, os.PathLike)): + env_files = [env_files] + + dotenv_vars: dict[str, str | None] = {} + for env_file in env_files: + env_path = Path(env_file).expanduser() + if env_path.is_file(): + dotenv_vars.update(self._read_env_file(env_path)) + + return dotenv_vars + + def __call__(self) -> dict[str, Any]: + data: dict[str, Any] = super().__call__() + is_extra_allowed = self.config.get('extra') != 'forbid' + + # As `extra` config is allowed in dotenv settings source, We have to + # update data with extra env variables from dotenv file. + for env_name, env_value in self.env_vars.items(): + if not env_value or env_name in data: + continue + env_used = False + for field_name, field in self.settings_cls.model_fields.items(): + for _, field_env_name, _ in self._extract_field_info(field, field_name): + if env_name == field_env_name or ( + ( + _annotation_is_complex(field.annotation, field.metadata) + or ( + is_union_origin(get_origin(field.annotation)) + and _union_is_complex(field.annotation, field.metadata) + ) + ) + and env_name.startswith(field_env_name) + ): + env_used = True + break + if env_used: + break + if not env_used: + if is_extra_allowed and env_name.startswith(self.env_prefix): + # env_prefix should be respected and removed from the env_name + normalized_env_name = env_name[len(self.env_prefix) :] + data[normalized_env_name] = env_value + else: + data[env_name] = env_value + return data + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, ' + f'env_nested_delimiter={self.env_nested_delimiter!r}, env_prefix_len={self.env_prefix_len!r})' + ) + + +def read_env_file( + file_path: Path, + *, + encoding: str | None = None, + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, +) -> Mapping[str, str | None]: + warnings.warn( + 'read_env_file will be removed in the next version, use DotEnvSettingsSource._static_read_env_file if you must', + DeprecationWarning, + ) + return DotEnvSettingsSource._static_read_env_file( + file_path, + encoding=encoding, + case_sensitive=case_sensitive, + ignore_empty=ignore_empty, + parse_none_str=parse_none_str, + ) + + +__all__ = ['DotEnvSettingsSource', 'read_env_file'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/env.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/env.py new file mode 100644 index 000000000..5a350f1da --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/env.py @@ -0,0 +1,270 @@ +from __future__ import annotations as _annotations + +import os +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, +) + +from pydantic._internal._utils import deep_update, is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from pydantic.fields import FieldInfo +from typing_extensions import get_args, get_origin +from typing_inspection.introspection import is_union_origin + +from ...utils import _lenient_issubclass +from ..base import PydanticBaseEnvSettingsSource +from ..types import EnvNoneType +from ..utils import ( + _annotation_enum_name_to_val, + _get_model_fields, + _union_is_complex, + parse_env_vars, +) + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class EnvSettingsSource(PydanticBaseEnvSettingsSource): + """ + Source class for loading settings values from environment variables. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_nested_delimiter: str | None = None, + env_nested_max_split: int | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + ) -> None: + super().__init__( + settings_cls, case_sensitive, env_prefix, env_ignore_empty, env_parse_none_str, env_parse_enums + ) + self.env_nested_delimiter = ( + env_nested_delimiter if env_nested_delimiter is not None else self.config.get('env_nested_delimiter') + ) + self.env_nested_max_split = ( + env_nested_max_split if env_nested_max_split is not None else self.config.get('env_nested_max_split') + ) + self.maxsplit = (self.env_nested_max_split or 0) - 1 + self.env_prefix_len = len(self.env_prefix) + + self.env_vars = self._load_env_vars() + + def _load_env_vars(self) -> Mapping[str, str | None]: + return parse_env_vars(os.environ, self.case_sensitive, self.env_ignore_empty, self.env_parse_none_str) + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value for field from environment variables and a flag to determine whether value is complex. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value (`None` if not found), key, and + a flag to determine whether value is complex. + """ + + env_val: str | None = None + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): + env_val = self.env_vars.get(env_name) + if env_val is not None: + break + + return env_val, field_key, value_is_complex + + def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: + """ + Prepare value for the field. + + * Extract value for nested field. + * Deserialize value to python object for complex field. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple contains prepared value for the field. + + Raises: + ValuesError: When There is an error in deserializing value for complex field. + """ + is_complex, allow_parse_failure = self._field_is_complex(field) + if self.env_parse_enums: + enum_val = _annotation_enum_name_to_val(field.annotation, value) + value = value if enum_val is None else enum_val + + if is_complex or value_is_complex: + if isinstance(value, EnvNoneType): + return value + elif value is None: + # field is complex but no value found so far, try explode_env_vars + env_val_built = self.explode_env_vars(field_name, field, self.env_vars) + if env_val_built: + return env_val_built + else: + # field is complex and there's a value, decode that as JSON, then add explode_env_vars + try: + value = self.decode_complex_value(field_name, field, value) + except ValueError as e: + if not allow_parse_failure: + raise e + + if isinstance(value, dict): + return deep_update(value, self.explode_env_vars(field_name, field, self.env_vars)) + else: + return value + elif value is not None: + # simplest case, field is not complex, we only need to add the value if it was found + return value + + def _field_is_complex(self, field: FieldInfo) -> tuple[bool, bool]: + """ + Find out if a field is complex, and if so whether JSON errors should be ignored + """ + if self.field_is_complex(field): + allow_parse_failure = False + elif is_union_origin(get_origin(field.annotation)) and _union_is_complex(field.annotation, field.metadata): + allow_parse_failure = True + else: + return False, False + + return True, allow_parse_failure + + # Default value of `case_sensitive` is `None`, because we don't want to break existing behavior. + # We have to change the method to a non-static method and use + # `self.case_sensitive` instead in V3. + def next_field( + self, field: FieldInfo | Any | None, key: str, case_sensitive: bool | None = None + ) -> FieldInfo | None: + """ + Find the field in a sub model by key(env name) + + By having the following models: + + ```py + class SubSubModel(BaseSettings): + dvals: Dict + + class SubModel(BaseSettings): + vals: list[str] + sub_sub_model: SubSubModel + + class Cfg(BaseSettings): + sub_model: SubModel + ``` + + Then: + next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class + next_field(sub_model, 'sub_sub_model') Returns `sub_sub_model` field of `SubModel` class + + Args: + field: The field. + key: The key (env name). + case_sensitive: Whether to search for key case sensitively. + + Returns: + Field if it finds the next field otherwise `None`. + """ + if not field: + return None + + annotation = field.annotation if isinstance(field, FieldInfo) else field + for type_ in get_args(annotation): + type_has_key = self.next_field(type_, key, case_sensitive) + if type_has_key: + return type_has_key + if is_model_class(annotation) or is_pydantic_dataclass(annotation): # type: ignore[arg-type] + fields = _get_model_fields(annotation) + # `case_sensitive is None` is here to be compatible with the old behavior. + # Has to be removed in V3. + for field_name, f in fields.items(): + for _, env_name, _ in self._extract_field_info(f, field_name): + if case_sensitive is None or case_sensitive: + if field_name == key or env_name == key: + return f + elif field_name.lower() == key.lower() or env_name.lower() == key.lower(): + return f + return None + + def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[str, str | None]) -> dict[str, Any]: + """ + Process env_vars and extract the values of keys containing env_nested_delimiter into nested dictionaries. + + This is applied to a single field, hence filtering by env_var prefix. + + Args: + field_name: The field name. + field: The field. + env_vars: Environment variables. + + Returns: + A dictionary contains extracted values from nested env values. + """ + if not self.env_nested_delimiter: + return {} + + ann = field.annotation + is_dict = ann is dict or _lenient_issubclass(get_origin(ann), dict) + + prefixes = [ + f'{env_name}{self.env_nested_delimiter}' for _, env_name, _ in self._extract_field_info(field, field_name) + ] + result: dict[str, Any] = {} + for env_name, env_val in env_vars.items(): + try: + prefix = next(prefix for prefix in prefixes if env_name.startswith(prefix)) + except StopIteration: + continue + # we remove the prefix before splitting in case the prefix has characters in common with the delimiter + env_name_without_prefix = env_name[len(prefix) :] + *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter, self.maxsplit) + env_var = result + target_field: FieldInfo | None = field + for key in keys: + target_field = self.next_field(target_field, key, self.case_sensitive) + if isinstance(env_var, dict): + env_var = env_var.setdefault(key, {}) + + # get proper field with last_key + target_field = self.next_field(target_field, last_key, self.case_sensitive) + + # check if env_val maps to a complex field and if so, parse the env_val + if (target_field or is_dict) and env_val: + if target_field: + is_complex, allow_json_failure = self._field_is_complex(target_field) + if self.env_parse_enums: + enum_val = _annotation_enum_name_to_val(target_field.annotation, env_val) + env_val = env_val if enum_val is None else enum_val + else: + # nested field type is dict + is_complex, allow_json_failure = True, True + if is_complex: + try: + env_val = self.decode_complex_value(last_key, target_field, env_val) # type: ignore + except ValueError as e: + if not allow_json_failure: + raise e + if isinstance(env_var, dict): + if last_key not in env_var or not isinstance(env_val, EnvNoneType) or env_var[last_key] == {}: + env_var[last_key] = env_val + + return result + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}(env_nested_delimiter={self.env_nested_delimiter!r}, ' + f'env_prefix_len={self.env_prefix_len!r})' + ) + + +__all__ = ['EnvSettingsSource'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/gcp.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/gcp.py new file mode 100644 index 000000000..62f356a76 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/gcp.py @@ -0,0 +1,152 @@ +from __future__ import annotations as _annotations + +from collections.abc import Iterator, Mapping +from functools import cached_property +from typing import TYPE_CHECKING, Optional + +from .env import EnvSettingsSource + +if TYPE_CHECKING: + from google.auth import default as google_auth_default + from google.auth.credentials import Credentials + from google.cloud.secretmanager import SecretManagerServiceClient + + from pydantic_settings.main import BaseSettings +else: + Credentials = None + SecretManagerServiceClient = None + google_auth_default = None + + +def import_gcp_secret_manager() -> None: + global Credentials + global SecretManagerServiceClient + global google_auth_default + + try: + from google.auth import default as google_auth_default + from google.auth.credentials import Credentials + from google.cloud.secretmanager import SecretManagerServiceClient + except ImportError as e: # pragma: no cover + raise ImportError( + 'GCP Secret Manager dependencies are not installed, run `pip install pydantic-settings[gcp-secret-manager]`' + ) from e + + +class GoogleSecretManagerMapping(Mapping[str, Optional[str]]): + _loaded_secrets: dict[str, str | None] + _secret_client: SecretManagerServiceClient + + def __init__(self, secret_client: SecretManagerServiceClient, project_id: str, case_sensitive: bool) -> None: + self._loaded_secrets = {} + self._secret_client = secret_client + self._project_id = project_id + self._case_sensitive = case_sensitive + + @property + def _gcp_project_path(self) -> str: + return self._secret_client.common_project_path(self._project_id) + + @cached_property + def _secret_names(self) -> list[str]: + rv: list[str] = [] + + secrets = self._secret_client.list_secrets(parent=self._gcp_project_path) + for secret in secrets: + name = self._secret_client.parse_secret_path(secret.name).get('secret', '') + if not self._case_sensitive: + name = name.lower() + rv.append(name) + return rv + + def _secret_version_path(self, key: str, version: str = 'latest') -> str: + return self._secret_client.secret_version_path(self._project_id, key, version) + + def __getitem__(self, key: str) -> str | None: + if not self._case_sensitive: + key = key.lower() + if key not in self._loaded_secrets: + # If we know the key isn't available in secret manager, raise a key error + if key not in self._secret_names: + raise KeyError(key) + + try: + self._loaded_secrets[key] = self._secret_client.access_secret_version( + name=self._secret_version_path(key) + ).payload.data.decode('UTF-8') + except Exception: + raise KeyError(key) + + return self._loaded_secrets[key] + + def __len__(self) -> int: + return len(self._secret_names) + + def __iter__(self) -> Iterator[str]: + return iter(self._secret_names) + + +class GoogleSecretManagerSettingsSource(EnvSettingsSource): + _credentials: Credentials + _secret_client: SecretManagerServiceClient + _project_id: str + + def __init__( + self, + settings_cls: type[BaseSettings], + credentials: Credentials | None = None, + project_id: str | None = None, + env_prefix: str | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + secret_client: SecretManagerServiceClient | None = None, + case_sensitive: bool | None = True, + ) -> None: + # Import Google Packages if they haven't already been imported + if SecretManagerServiceClient is None or Credentials is None or google_auth_default is None: + import_gcp_secret_manager() + + # If credentials or project_id are not passed, then + # try to get them from the default function + if not credentials or not project_id: + _creds, _project_id = google_auth_default() # type: ignore[no-untyped-call] + + # Set the credentials and/or project id if they weren't specified + if credentials is None: + credentials = _creds + + if project_id is None: + if isinstance(_project_id, str): + project_id = _project_id + else: + raise AttributeError( + 'project_id is required to be specified either as an argument or from the google.auth.default. See https://google-auth.readthedocs.io/en/master/reference/google.auth.html#google.auth.default' + ) + + self._credentials: Credentials = credentials + self._project_id: str = project_id + + if secret_client: + self._secret_client = secret_client + else: + self._secret_client = SecretManagerServiceClient(credentials=self._credentials) + + super().__init__( + settings_cls, + case_sensitive=case_sensitive, + env_prefix=env_prefix, + env_ignore_empty=False, + env_parse_none_str=env_parse_none_str, + env_parse_enums=env_parse_enums, + ) + + def _load_env_vars(self) -> Mapping[str, Optional[str]]: + return GoogleSecretManagerMapping( + self._secret_client, project_id=self._project_id, case_sensitive=self.case_sensitive + ) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(project_id={self._project_id!r}, env_nested_delimiter={self.env_nested_delimiter!r})' + + +__all__ = ['GoogleSecretManagerSettingsSource', 'GoogleSecretManagerMapping'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/json.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/json.py new file mode 100644 index 000000000..837601c39 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/json.py @@ -0,0 +1,47 @@ +"""JSON file settings source.""" + +from __future__ import annotations as _annotations + +import json +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, +) + +from ..base import ConfigFileSourceMixin, InitSettingsSource +from ..types import DEFAULT_PATH, PathType + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class JsonConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + """ + A source class that loads variables from a JSON file + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + json_file: PathType | None = DEFAULT_PATH, + json_file_encoding: str | None = None, + ): + self.json_file_path = json_file if json_file != DEFAULT_PATH else settings_cls.model_config.get('json_file') + self.json_file_encoding = ( + json_file_encoding + if json_file_encoding is not None + else settings_cls.model_config.get('json_file_encoding') + ) + self.json_data = self._read_files(self.json_file_path) + super().__init__(settings_cls, self.json_data) + + def _read_file(self, file_path: Path) -> dict[str, Any]: + with open(file_path, encoding=self.json_file_encoding) as json_file: + return json.load(json_file) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(json_file={self.json_file_path})' + + +__all__ = ['JsonConfigSettingsSource'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/pyproject.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/pyproject.py new file mode 100644 index 000000000..bb02cbbda --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/pyproject.py @@ -0,0 +1,62 @@ +"""Pyproject TOML file settings source.""" + +from __future__ import annotations as _annotations + +from pathlib import Path +from typing import ( + TYPE_CHECKING, +) + +from .toml import TomlConfigSettingsSource + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class PyprojectTomlConfigSettingsSource(TomlConfigSettingsSource): + """ + A source class that loads variables from a `pyproject.toml` file. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + toml_file: Path | None = None, + ) -> None: + self.toml_file_path = self._pick_pyproject_toml_file( + toml_file, settings_cls.model_config.get('pyproject_toml_depth', 0) + ) + self.toml_table_header: tuple[str, ...] = settings_cls.model_config.get( + 'pyproject_toml_table_header', ('tool', 'pydantic-settings') + ) + self.toml_data = self._read_files(self.toml_file_path) + for key in self.toml_table_header: + self.toml_data = self.toml_data.get(key, {}) + super(TomlConfigSettingsSource, self).__init__(settings_cls, self.toml_data) + + @staticmethod + def _pick_pyproject_toml_file(provided: Path | None, depth: int) -> Path: + """Pick a `pyproject.toml` file path to use. + + Args: + provided: Explicit path provided when instantiating this class. + depth: Number of directories up the tree to check of a pyproject.toml. + + """ + if provided: + return provided.resolve() + rv = Path.cwd() / 'pyproject.toml' + count = 0 + if not rv.is_file(): + child = rv.parent.parent / 'pyproject.toml' + while count < depth: + if child.is_file(): + return child + if str(child.parent) == rv.root: + break # end discovery after checking system root once + child = child.parent.parent / 'pyproject.toml' + count += 1 + return rv + + +__all__ = ['PyprojectTomlConfigSettingsSource'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/secrets.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/secrets.py new file mode 100644 index 000000000..00a8f47ad --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/secrets.py @@ -0,0 +1,125 @@ +"""Secrets file settings source.""" + +from __future__ import annotations as _annotations + +import os +import warnings +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, +) + +from pydantic.fields import FieldInfo + +from pydantic_settings.utils import path_type_label + +from ...exceptions import SettingsError +from ..base import PydanticBaseEnvSettingsSource +from ..types import PathType + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + +class SecretsSettingsSource(PydanticBaseEnvSettingsSource): + """ + Source class for loading settings values from secret files. + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + secrets_dir: PathType | None = None, + case_sensitive: bool | None = None, + env_prefix: str | None = None, + env_ignore_empty: bool | None = None, + env_parse_none_str: str | None = None, + env_parse_enums: bool | None = None, + ) -> None: + super().__init__( + settings_cls, case_sensitive, env_prefix, env_ignore_empty, env_parse_none_str, env_parse_enums + ) + self.secrets_dir = secrets_dir if secrets_dir is not None else self.config.get('secrets_dir') + + def __call__(self) -> dict[str, Any]: + """ + Build fields from "secrets" files. + """ + secrets: dict[str, str | None] = {} + + if self.secrets_dir is None: + return secrets + + secrets_dirs = [self.secrets_dir] if isinstance(self.secrets_dir, (str, os.PathLike)) else self.secrets_dir + secrets_paths = [Path(p).expanduser() for p in secrets_dirs] + self.secrets_paths = [] + + for path in secrets_paths: + if not path.exists(): + warnings.warn(f'directory "{path}" does not exist') + else: + self.secrets_paths.append(path) + + if not len(self.secrets_paths): + return secrets + + for path in self.secrets_paths: + if not path.is_dir(): + raise SettingsError(f'secrets_dir must reference a directory, not a {path_type_label(path)}') + + return super().__call__() + + @classmethod + def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> Path | None: + """ + Find a file within path's directory matching filename, optionally ignoring case. + + Args: + dir_path: Directory path. + file_name: File name. + case_sensitive: Whether to search for file name case sensitively. + + Returns: + Whether file path or `None` if file does not exist in directory. + """ + for f in dir_path.iterdir(): + if f.name == file_name: + return f + elif not case_sensitive and f.name.lower() == file_name.lower(): + return f + return None + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]: + """ + Gets the value for field from secret file and a flag to determine whether value is complex. + + Args: + field: The field. + field_name: The field name. + + Returns: + A tuple that contains the value (`None` if the file does not exist), key, and + a flag to determine whether value is complex. + """ + + for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name): + # paths reversed to match the last-wins behaviour of `env_file` + for secrets_path in reversed(self.secrets_paths): + path = self.find_case_path(secrets_path, env_name, self.case_sensitive) + if not path: + # path does not exist, we currently don't return a warning for this + continue + + if path.is_file(): + return path.read_text().strip(), field_key, value_is_complex + else: + warnings.warn( + f'attempted to load secret file "{path}" but found a {path_type_label(path)} instead.', + stacklevel=4, + ) + + return None, field_key, value_is_complex + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(secrets_dir={self.secrets_dir!r})' diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/toml.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/toml.py new file mode 100644 index 000000000..eaff41da0 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/toml.py @@ -0,0 +1,66 @@ +"""TOML file settings source.""" + +from __future__ import annotations as _annotations + +import sys +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, +) + +from ..base import ConfigFileSourceMixin, InitSettingsSource +from ..types import DEFAULT_PATH, PathType + +if TYPE_CHECKING: + from pydantic_settings.main import BaseSettings + + if sys.version_info >= (3, 11): + import tomllib + else: + tomllib = None + import tomli +else: + tomllib = None + tomli = None + + +def import_toml() -> None: + global tomli + global tomllib + if sys.version_info < (3, 11): + if tomli is not None: + return + try: + import tomli + except ImportError as e: # pragma: no cover + raise ImportError('tomli is not installed, run `pip install pydantic-settings[toml]`') from e + else: + if tomllib is not None: + return + import tomllib + + +class TomlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + """ + A source class that loads variables from a TOML file + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + toml_file: PathType | None = DEFAULT_PATH, + ): + self.toml_file_path = toml_file if toml_file != DEFAULT_PATH else settings_cls.model_config.get('toml_file') + self.toml_data = self._read_files(self.toml_file_path) + super().__init__(settings_cls, self.toml_data) + + def _read_file(self, file_path: Path) -> dict[str, Any]: + import_toml() + with open(file_path, mode='rb') as toml_file: + if sys.version_info < (3, 11): + return tomli.load(toml_file) + return tomllib.load(toml_file) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(toml_file={self.toml_file_path})' diff --git a/venv/Lib/site-packages/pydantic_settings/sources/providers/yaml.py b/venv/Lib/site-packages/pydantic_settings/sources/providers/yaml.py new file mode 100644 index 000000000..82778b4f5 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/providers/yaml.py @@ -0,0 +1,75 @@ +"""YAML file settings source.""" + +from __future__ import annotations as _annotations + +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, +) + +from ..base import ConfigFileSourceMixin, InitSettingsSource +from ..types import DEFAULT_PATH, PathType + +if TYPE_CHECKING: + import yaml + + from pydantic_settings.main import BaseSettings +else: + yaml = None + + +def import_yaml() -> None: + global yaml + if yaml is not None: + return + try: + import yaml + except ImportError as e: + raise ImportError('PyYAML is not installed, run `pip install pydantic-settings[yaml]`') from e + + +class YamlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + """ + A source class that loads variables from a yaml file + """ + + def __init__( + self, + settings_cls: type[BaseSettings], + yaml_file: PathType | None = DEFAULT_PATH, + yaml_file_encoding: str | None = None, + yaml_config_section: str | None = None, + ): + self.yaml_file_path = yaml_file if yaml_file != DEFAULT_PATH else settings_cls.model_config.get('yaml_file') + self.yaml_file_encoding = ( + yaml_file_encoding + if yaml_file_encoding is not None + else settings_cls.model_config.get('yaml_file_encoding') + ) + self.yaml_config_section = ( + yaml_config_section + if yaml_config_section is not None + else settings_cls.model_config.get('yaml_config_section') + ) + self.yaml_data = self._read_files(self.yaml_file_path) + + if self.yaml_config_section: + try: + self.yaml_data = self.yaml_data[self.yaml_config_section] + except KeyError: + raise KeyError( + f'yaml_config_section key "{self.yaml_config_section}" not found in {self.yaml_file_path}' + ) + super().__init__(settings_cls, self.yaml_data) + + def _read_file(self, file_path: Path) -> dict[str, Any]: + import_yaml() + with open(file_path, encoding=self.yaml_file_encoding) as yaml_file: + return yaml.safe_load(yaml_file) or {} + + def __repr__(self) -> str: + return f'{self.__class__.__name__}(yaml_file={self.yaml_file_path})' + + +__all__ = ['YamlConfigSettingsSource'] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/types.py b/venv/Lib/site-packages/pydantic_settings/sources/types.py new file mode 100644 index 000000000..2c00d0e2a --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/types.py @@ -0,0 +1,78 @@ +"""Type definitions for pydantic-settings sources.""" + +from __future__ import annotations as _annotations + +from collections.abc import Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Any, TypeVar, Union + +if TYPE_CHECKING: + from pydantic._internal._dataclasses import PydanticDataclass + from pydantic.main import BaseModel + + PydanticModel = TypeVar('PydanticModel', bound=Union[PydanticDataclass, BaseModel]) +else: + PydanticModel = Any + + +class EnvNoneType(str): + pass + + +class NoDecode: + """Annotation to prevent decoding of a field value.""" + + pass + + +class ForceDecode: + """Annotation to force decoding of a field value.""" + + pass + + +DotenvType = Union[Path, str, Sequence[Union[Path, str]]] +PathType = Union[Path, str, Sequence[Union[Path, str]]] +DEFAULT_PATH: PathType = Path('') + +# This is used as default value for `_env_file` in the `BaseSettings` class and +# `env_file` in `DotEnvSettingsSource` so the default can be distinguished from `None`. +# See the docstring of `BaseSettings` for more details. +ENV_FILE_SENTINEL: DotenvType = Path('') + + +class _CliSubCommand: + pass + + +class _CliPositionalArg: + pass + + +class _CliImplicitFlag: + pass + + +class _CliExplicitFlag: + pass + + +class _CliUnknownArgs: + pass + + +__all__ = [ + 'DEFAULT_PATH', + 'ENV_FILE_SENTINEL', + 'DotenvType', + 'EnvNoneType', + 'ForceDecode', + 'NoDecode', + 'PathType', + 'PydanticModel', + '_CliExplicitFlag', + '_CliImplicitFlag', + '_CliPositionalArg', + '_CliSubCommand', + '_CliUnknownArgs', +] diff --git a/venv/Lib/site-packages/pydantic_settings/sources/utils.py b/venv/Lib/site-packages/pydantic_settings/sources/utils.py new file mode 100644 index 000000000..270a8c170 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/sources/utils.py @@ -0,0 +1,198 @@ +"""Utility functions for pydantic-settings sources.""" + +from __future__ import annotations as _annotations + +from collections import deque +from collections.abc import Mapping, Sequence +from dataclasses import is_dataclass +from enum import Enum +from typing import Any, Optional, cast + +from pydantic import BaseModel, Json, RootModel, Secret +from pydantic._internal._utils import is_model_class +from pydantic.dataclasses import is_pydantic_dataclass +from typing_extensions import get_args, get_origin +from typing_inspection import typing_objects + +from ..exceptions import SettingsError +from ..utils import _lenient_issubclass +from .types import EnvNoneType + + +def _get_env_var_key(key: str, case_sensitive: bool = False) -> str: + return key if case_sensitive else key.lower() + + +def _parse_env_none_str(value: str | None, parse_none_str: str | None = None) -> str | None | EnvNoneType: + return value if not (value == parse_none_str and parse_none_str is not None) else EnvNoneType(value) + + +def parse_env_vars( + env_vars: Mapping[str, str | None], + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, +) -> Mapping[str, str | None]: + return { + _get_env_var_key(k, case_sensitive): _parse_env_none_str(v, parse_none_str) + for k, v in env_vars.items() + if not (ignore_empty and v == '') + } + + +def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: + # If the model is a root model, the root annotation should be used to + # evaluate the complexity. + if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel: + annotation = cast('type[RootModel[Any]]', annotation) + root_annotation = annotation.model_fields['root'].annotation + if root_annotation is not None: # pragma: no branch + annotation = root_annotation + + if any(isinstance(md, Json) for md in metadata): # type: ignore[misc] + return False + + origin = get_origin(annotation) + + # Check if annotation is of the form Annotated[type, metadata]. + if typing_objects.is_annotated(origin): + # Return result of recursive call on inner type. + inner, *meta = get_args(annotation) + return _annotation_is_complex(inner, meta) + + if origin is Secret: + return False + + return ( + _annotation_is_complex_inner(annotation) + or _annotation_is_complex_inner(origin) + or hasattr(origin, '__pydantic_core_schema__') + or hasattr(origin, '__get_pydantic_core_schema__') + ) + + +def _annotation_is_complex_inner(annotation: type[Any] | None) -> bool: + if _lenient_issubclass(annotation, (str, bytes)): + return False + + return _lenient_issubclass( + annotation, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque) + ) or is_dataclass(annotation) + + +def _union_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: + """Check if a union type contains any complex types.""" + return any(_annotation_is_complex(arg, metadata) for arg in get_args(annotation)) + + +def _annotation_contains_types( + annotation: type[Any] | None, + types: tuple[Any, ...], + is_include_origin: bool = True, + is_strip_annotated: bool = False, +) -> bool: + """Check if a type annotation contains any of the specified types.""" + if is_strip_annotated: + annotation = _strip_annotated(annotation) + if is_include_origin is True and get_origin(annotation) in types: + return True + for type_ in get_args(annotation): + if _annotation_contains_types(type_, types, is_include_origin=True, is_strip_annotated=is_strip_annotated): + return True + return annotation in types + + +def _strip_annotated(annotation: Any) -> Any: + if typing_objects.is_annotated(get_origin(annotation)): + return annotation.__origin__ + else: + return annotation + + +def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> Optional[str]: + for type_ in (annotation, get_origin(annotation), *get_args(annotation)): + if _lenient_issubclass(type_, Enum): + if value in tuple(val.value for val in type_): + return type_(value).name + return None + + +def _annotation_enum_name_to_val(annotation: type[Any] | None, name: Any) -> Any: + for type_ in (annotation, get_origin(annotation), *get_args(annotation)): + if _lenient_issubclass(type_, Enum): + if name in tuple(val.name for val in type_): + return type_[name] + return None + + +def _get_model_fields(model_cls: type[Any]) -> dict[str, Any]: + """Get fields from a pydantic model or dataclass.""" + + if is_pydantic_dataclass(model_cls) and hasattr(model_cls, '__pydantic_fields__'): + return model_cls.__pydantic_fields__ + if is_model_class(model_cls): + return model_cls.model_fields + raise SettingsError(f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass') + + +def _get_alias_names( + field_name: str, field_info: Any, alias_path_args: dict[str, str] = {}, case_sensitive: bool = True +) -> tuple[tuple[str, ...], bool]: + """Get alias names for a field, handling alias paths and case sensitivity.""" + from pydantic import AliasChoices, AliasPath + + alias_names: list[str] = [] + is_alias_path_only: bool = True + if not any((field_info.alias, field_info.validation_alias)): + alias_names += [field_name] + is_alias_path_only = False + else: + new_alias_paths: list[AliasPath] = [] + for alias in (field_info.alias, field_info.validation_alias): + if alias is None: + continue + elif isinstance(alias, str): + alias_names.append(alias) + is_alias_path_only = False + elif isinstance(alias, AliasChoices): + for name in alias.choices: + if isinstance(name, str): + alias_names.append(name) + is_alias_path_only = False + else: + new_alias_paths.append(name) + else: + new_alias_paths.append(alias) + for alias_path in new_alias_paths: + name = cast(str, alias_path.path[0]) + name = name.lower() if not case_sensitive else name + alias_path_args[name] = 'dict' if len(alias_path.path) > 2 else 'list' + if not alias_names and is_alias_path_only: + alias_names.append(name) + if not case_sensitive: + alias_names = [alias_name.lower() for alias_name in alias_names] + return tuple(dict.fromkeys(alias_names)), is_alias_path_only + + +def _is_function(obj: Any) -> bool: + """Check if an object is a function.""" + from types import BuiltinFunctionType, FunctionType + + return isinstance(obj, (FunctionType, BuiltinFunctionType)) + + +__all__ = [ + '_annotation_contains_types', + '_annotation_enum_name_to_val', + '_annotation_enum_val_to_name', + '_annotation_is_complex', + '_annotation_is_complex_inner', + '_get_alias_names', + '_get_env_var_key', + '_get_model_fields', + '_is_function', + '_parse_env_none_str', + '_strip_annotated', + '_union_is_complex', + 'parse_env_vars', +] diff --git a/venv/Lib/site-packages/pydantic_settings/utils.py b/venv/Lib/site-packages/pydantic_settings/utils.py new file mode 100644 index 000000000..74c99be6c --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/utils.py @@ -0,0 +1,48 @@ +import sys +import types +from pathlib import Path +from typing import Any, _GenericAlias # type: ignore [attr-defined] + +from typing_extensions import get_origin + +_PATH_TYPE_LABELS = { + Path.is_dir: 'directory', + Path.is_file: 'file', + Path.is_mount: 'mount point', + Path.is_symlink: 'symlink', + Path.is_block_device: 'block device', + Path.is_char_device: 'char device', + Path.is_fifo: 'FIFO', + Path.is_socket: 'socket', +} + + +def path_type_label(p: Path) -> str: + """ + Find out what sort of thing a path is. + """ + assert p.exists(), 'path does not exist' + for method, name in _PATH_TYPE_LABELS.items(): + if method(p): + return name + + return 'unknown' # pragma: no cover + + +# TODO remove and replace usage by `isinstance(cls, type) and issubclass(cls, class_or_tuple)` +# once we drop support for Python 3.10. +def _lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool: # pragma: no cover + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) + except TypeError: + if get_origin(cls) is not None: + # Up until Python 3.10, isinstance(, type) is True + # (e.g. list[int]) + return False + raise + + +if sys.version_info < (3, 10): + _WithArgsTypes = tuple() +else: + _WithArgsTypes = (_GenericAlias, types.GenericAlias, types.UnionType) diff --git a/venv/Lib/site-packages/pydantic_settings/version.py b/venv/Lib/site-packages/pydantic_settings/version.py new file mode 100644 index 000000000..79585b421 --- /dev/null +++ b/venv/Lib/site-packages/pydantic_settings/version.py @@ -0,0 +1 @@ +VERSION = '2.10.1'