diff --git a/.chainlit/config.toml b/.chainlit/config.toml index be10a3b4..b3c33cec 100644 --- a/.chainlit/config.toml +++ b/.chainlit/config.toml @@ -53,7 +53,7 @@ edit_message = true [UI] # Name of the assistant. -name = "React-to-me" +name = "React-to-Me" # default_theme = "dark" diff --git a/Dockerfile b/Dockerfile index 04550105..226dbbfc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,45 +1,47 @@ -# Use an official Python runtime as a parent image -FROM python:3.12-slim +# Use official python-slim image +ARG PYTHON_VERSION=3.12 +FROM python:${PYTHON_VERSION}-slim -# Set the working directory in the container WORKDIR /app -# Set environment variables for the virtual environment path -ENV VENV_PATH="/app/.venv" +# Create non-root user and group. The image used to run everything as root; a +# compromise in chainlit or any dependency then owned the container. +RUN \ + groupadd -g 1000 appgroup && \ + useradd -m -u 3001 -g appgroup appuser && \ + chown -R appuser /app && \ + chmod -R 700 /app +USER appuser:appgroup + +# Install Python dependencies +COPY --chown=appuser --chmod=400 poetry.lock /app/ +COPY --chown=appuser --chmod=700 pyproject.toml /app/ +ARG POETRY_VERSION=1.8.4 +ENV POETRY_VENV="/home/appuser/.poetry" +RUN \ + python -m venv $POETRY_VENV && \ + $POETRY_VENV/bin/pip install -U pip setuptools && \ + $POETRY_VENV/bin/pip install poetry~=$POETRY_VERSION && \ + $POETRY_VENV/bin/poetry config virtualenvs.in-project true && \ + $POETRY_VENV/bin/poetry install --no-root --without dev && \ + rm -rf $POETRY_VENV + +# NLTK data, at build time. BM25 tokenises with +# word_tokenize(..., language="english"), which needs punkt_tab -- without it +# every retrieval either downloads it on first use or fails. It lands in +# appuser's home because the download runs as appuser. +RUN /app/.venv/bin/python -m nltk.downloader punkt_tab + +# Copy essential application files +COPY --chown=appuser --chmod=700 .chainlit/ /app/.chainlit/ +COPY --chown=appuser --chmod=400 bin/ /app/bin/ +COPY --chown=appuser --chmod=400 public/ /app/public/ +COPY --chown=appuser --chmod=700 src/ /app/src/ +COPY --chown=appuser --chmod=400 chainlit.md /app/ +COPY --chown=appuser --chmod=400 config_default.yml /app/ +COPY --chown=appuser --chmod=400 LICENSE /app/ -# Set PYTHONPATH environment variable to include the src directory -ENV PYTHONPATH="/app/src" -# Install system dependencies -# libpq5 is for python package psycopg -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - && rm -rf /var/lib/apt/lists/* - -# Copy the requirements file into the container -COPY pyproject.toml poetry.lock ./ - -# Install specific versions of filelock, virtualenv, and poetry -RUN pip install filelock==3.15.4 virtualenv==20.26.6 poetry==1.8.4 - -# Set poetry to create virtual environment in the project directory -RUN poetry config virtualenvs.in-project true - -# Install dependencies without dev dependencies -RUN poetry install --no-root --without dev - -# Download NLTK data -RUN poetry run python -m nltk.downloader punkt_tab - -# Adjust PATH to include the virtual environment's bin directory -ENV PATH="${VENV_PATH}/bin:${PATH}" - -# Copy the rest of the application code into the container -COPY . . - -# Ensure the virtual environment is activated in the shell ENV PATH="/app/.venv/bin:$PATH" - -# Make all files in the bin directory executable -RUN chmod +x bin/* +ENV PYTHONPATH="/app/src" CMD ["uvicorn", "bin.chat-fastapi:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/bin/chat-chainlit.py b/bin/chat-chainlit.py index 9c408065..70b9d4a8 100644 --- a/bin/chat-chainlit.py +++ b/bin/chat-chainlit.py @@ -22,7 +22,12 @@ from util.config_yml import Config, TriggerEvent from util.logging import logging from util.orcid_provider import ORCIDOAuthProvider -from util.secrets import SECRET_NAMES, load_secrets_to_environ, mounted_secrets +from util.secrets import ( + SECRET_NAMES, + get_db_uri, + load_secrets_to_environ, + mounted_secrets, +) load_dotenv() # Before anything reads os.environ. Docker secrets, where mounted, take @@ -45,13 +50,19 @@ llm_graph = AgentGraph(profiles) POSTGRES_CHAINLIT_DB = os.getenv("POSTGRES_CHAINLIT_DB") -POSTGRES_USER = os.getenv("POSTGRES_USER") -POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD") S3_BUCKET = os.getenv("S3_BUCKET") S3_CHAINLIT_PREFIX = os.getenv("S3_CHAINLIT_PREFIX") -if POSTGRES_CHAINLIT_DB and POSTGRES_USER and POSTGRES_PASSWORD: - CHAINLIT_DB_URI = f"postgresql+psycopg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@postgres:5432/{POSTGRES_CHAINLIT_DB}?sslmode=disable" +# Once, not per call. Under Vault every call to get_db_uri mints a fresh +# short-lived credential, so calling it inside get_data_layer -- which chainlit +# invokes per session -- would issue a new lease for every visitor and leave +# them all outstanding until they expire. SQLAlchemy needs its dialect named. +CHAINLIT_DB_URI = get_db_uri(POSTGRES_CHAINLIT_DB, driver="psycopg") + +if CHAINLIT_DB_URI: + # A local so the narrowing survives into get_data_layer below: mypy does not + # carry a module global's narrowed type into a nested function. + _chainlit_db_uri: str = CHAINLIT_DB_URI storage_client: PrefixedS3StorageClient | None if S3_BUCKET and S3_CHAINLIT_PREFIX: @@ -62,7 +73,7 @@ @cl.data_layer def get_data_layer() -> BaseDataLayer: return SQLAlchemyDataLayer( - conninfo=CHAINLIT_DB_URI, + conninfo=_chainlit_db_uri, storage_provider=storage_client, ) diff --git a/bin/chat-fastapi.py b/bin/chat-fastapi.py index a984cb7b..c7169cee 100644 --- a/bin/chat-fastapi.py +++ b/bin/chat-fastapi.py @@ -11,14 +11,25 @@ from fastapi import FastAPI, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse +from util.secrets import get_secret, load_secrets_to_environ + load_dotenv() +load_secrets_to_environ( + [ + "CHAINLIT_AUTH_SECRET", + "OAUTH_AUTH0_CLIENT_SECRET", + "OAUTH_GOOGLE_CLIENT_SECRET", + "OPENAI_API_KEY", + "TAVILY_API_KEY", + ] +) app = FastAPI() CHAINLIT_URI = os.getenv("CHAINLIT_URI") CHAINLIT_URL = os.getenv("CHAINLIT_URL") -CLOUDFLARE_SECRET_KEY = os.getenv("CLOUDFLARE_SECRET_KEY") +CLOUDFLARE_SECRET_KEY = get_secret("CLOUDFLARE_SECRET_KEY") CLOUDFLARE_SITE_KEY = os.getenv("CLOUDFLARE_SITE_KEY") ERROR_PAGE_TEMPLATE = Template( diff --git a/bin/export_nologin_usage.py b/bin/export_nologin_usage.py index e755627c..ecce2a93 100644 --- a/bin/export_nologin_usage.py +++ b/bin/export_nologin_usage.py @@ -6,9 +6,28 @@ import psycopg from dotenv import load_dotenv +from util.secrets import get_db_uri + load_dotenv() -LANGGRAPH_NOLOGIN_DB_URI = f"postgresql://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@postgres:5432/{os.getenv('POSTGRES_LANGGRAPH_DB')}_no_login?sslmode=disable" + +def langgraph_nologin_db_uri() -> str: + """Resolve the database URI, or stop with a message saying why. + + A function, not a module constant: this used to resolve at import + time and raise SystemExit when no database was configured, which + broke CI's "can every entry point be imported" check -- the runner + has no Postgres. Importing a script should do nothing; running it + should fail loudly. + """ + db_name = os.getenv("POSTGRES_LANGGRAPH_DB") + uri = get_db_uri(f"{db_name}_no_login" if db_name else None) + if uri is None: + raise SystemExit( + "POSTGRES_LANGGRAPH_DB is not set, or no Postgres password is available. " + "This script exports from the database; it cannot run without one." + ) + return uri def build_query() -> str: @@ -32,7 +51,7 @@ def main(records_dir: Path) -> None: query: str = build_query() - with psycopg.connect(LANGGRAPH_NOLOGIN_DB_URI) as conn, conn.cursor() as cur: + with psycopg.connect(langgraph_nologin_db_uri()) as conn, conn.cursor() as cur: cur.execute(query) header = [col.name for col in cur.description] if cur.description else None records = cur.fetchall() diff --git a/bin/export_records.py b/bin/export_records.py index f7d62dc9..36332172 100644 --- a/bin/export_records.py +++ b/bin/export_records.py @@ -6,9 +6,28 @@ import psycopg from dotenv import load_dotenv +from util.secrets import get_db_uri + load_dotenv() -CHAINLIT_DB_URI = f"postgresql://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@postgres:5432/{os.getenv('POSTGRES_CHAINLIT_DB')}?sslmode=disable" + +def chainlit_db_uri() -> str: + """Resolve the database URI, or stop with a message saying why. + + A function, not a module constant: this used to resolve at import + time and raise SystemExit when no database was configured, which + broke CI's "can every entry point be imported" check -- the runner + has no Postgres. Importing a script should do nothing; running it + should fail loudly. + """ + db_name = os.getenv("POSTGRES_CHAINLIT_DB") + uri = get_db_uri(f"{db_name}" if db_name else None) + if uri is None: + raise SystemExit( + "POSTGRES_CHAINLIT_DB is not set, or no Postgres password is available. " + "This script exports from the database; it cannot run without one." + ) + return uri def build_query() -> str: @@ -60,7 +79,7 @@ def main(records_dir: Path) -> None: since_timestamp: str | None = last_record_timestamp(records_dir) query: str = build_query() - with psycopg.connect(CHAINLIT_DB_URI) as conn, conn.cursor() as cur: + with psycopg.connect(chainlit_db_uri()) as conn, conn.cursor() as cur: cur.execute(query, {"since_timestamp": since_timestamp or ""}) header = [col.name for col in cur.description] if cur.description else None records = cur.fetchall() diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 00000000..0c582c3a --- /dev/null +++ b/compose.yaml @@ -0,0 +1,167 @@ +services: + + chainlit: + image: ${CHAINLIT_IMAGE} + build: . + ports: + - target: 8000 + published: 8000 + mode: host + environment: + CHAINLIT_ROOT_PATH: ${CHAINLIT_ROOT_PATH} + CHAINLIT_URI: ${CHAINLIT_URI} + CHAINLIT_URL: ${CHAINLIT_URL} + CLOUDFLARE_SITE_KEY: ${CLOUDFLARE_SITE_KEY} + LOG_LEVEL: ${LOG_LEVEL} + OAUTH_AUTH0_CLIENT_ID: ${OAUTH_AUTH0_CLIENT_ID} + OAUTH_AUTH0_DOMAIN: ${OAUTH_AUTH0_DOMAIN} + OAUTH_GOOGLE_CLIENT_ID: ${OAUTH_GOOGLE_CLIENT_ID} + POSTGRES_CHAINLIT_DB: ${POSTGRES_CHAINLIT_DB} + POSTGRES_LANGGRAPH_DB: ${POSTGRES_LANGGRAPH_DB} + S3_BUCKET: ${S3_BUCKET} + S3_CHAINLIT_PREFIX: ${S3_CHAINLIT_PREFIX} + UVICORN_LOG_LEVEL: ${LOG_LEVEL} + # The following should use Docker Secrets when possible: + CHAINLIT_AUTH_SECRET: ${CHAINLIT_AUTH_SECRET} + CLOUDFLARE_SECRET_KEY: ${CLOUDFLARE_SECRET_KEY} + OAUTH_AUTH0_CLIENT_SECRET: ${OAUTH_AUTH0_CLIENT_SECRET} + OAUTH_GOOGLE_CLIENT_SECRET: ${OAUTH_GOOGLE_CLIENT_SECRET} + OPENAI_API_KEY: ${OPENAI_API_KEY} + TAVILY_API_KEY: ${TAVILY_API_KEY} + # Postgres access when not using Vault (for development) + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + secrets: + - CHAINLIT_AUTH_SECRET + - CLOUDFLARE_SECRET_KEY + - OAUTH_AUTH0_CLIENT_SECRET + - OAUTH_GOOGLE_CLIENT_SECRET + - OPENAI_API_KEY + - TAVILY_API_KEY + volumes: + - ./config.yml:/app/config.yml:ro + - ./embeddings:/app/embeddings + - ./records:/app/records + - postgres-socket:/sockets/postgres + - vault-postgres-app-token:/tokens/postgres-app:ro + - vault-socket:/sockets/vault + + chainlit-guest: + image: ${CHAINLIT_IMAGE} + ports: + - target: 8000 + published: 8001 + mode: host + environment: + CHAINLIT_URI: ${CHAINLIT_URI_NO_LOGIN} + CHAINLIT_URI_LOGIN: ${CHAINLIT_URI} + CHAINLIT_URL: ${CHAINLIT_URL} + CLOUDFLARE_SITE_KEY: ${CLOUDFLARE_SITE_KEY} + LOG_LEVEL: ${LOG_LEVEL} + POSTGRES_CHAINLIT_DB: ${POSTGRES_CHAINLIT_DB} + POSTGRES_LANGGRAPH_DB: ${POSTGRES_LANGGRAPH_DB} + UVICORN_LOG_LEVEL: ${LOG_LEVEL} + # The following should use Docker Secrets when possible: + CLOUDFLARE_SECRET_KEY: ${CLOUDFLARE_SECRET_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + TAVILY_API_KEY: ${TAVILY_API_KEY} + # Postgres access when not using Vault (for development) + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + secrets: + - CLOUDFLARE_SECRET_KEY + - OPENAI_API_KEY + - TAVILY_API_KEY + volumes: + - ./config.yml:/app/config.yml:ro + - ./embeddings:/app/embeddings + - postgres-socket:/sockets/postgres + - vault-postgres-app-token:/tokens/postgres-app:ro + - vault-socket:/sockets/vault + + postgres: + image: postgres:17-alpine + user: 70:1000 + healthcheck: + test: pg_isready + environment: + LANG: ${LOCALE}.utf8 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_INITDB_ARGS: >- + --auth-host reject + --auth-local scram-sha-256 + --locale-provider icu + --icu-locale ${LOCALE} + POSTGRES_HOST_AUTH_METHOD: reject + TZ: ${TIMEZONE} + volumes: + - ./docker/postgres/config:/config:ro + - ./docker/postgres/initdb:/docker-entrypoint-initdb.d:ro + - postgres-data:/var/lib/postgresql/data + - postgres-socket:/var/run/postgresql + command: -c config_file=/config/postgresql.conf + + vault: + image: hashicorp/vault:1.19 + healthcheck: + test: vault status + retries: 999 + cap_add: + - IPC_LOCK + environment: + LESSSECURE: 1 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + VAULT_ADDR: unix:///sockets/vault/vault.sock + VAULT_LOCAL_CONFIG: | # > /vault/config/local.json + { + "api_addr": "unix:///sockets/vault/vault.sock", + "disable_clustering": "true", + "listener": [{ + "unix": { + "address": "/sockets/vault/vault.sock", + "socket_mode": "660", + "socket_user": "100", + "socket_group": "1000" + } + }], + "storage": { + "file": { + "path": "/vault/file" + } + }, + "default_lease_ttl": "1h", + "max_lease_ttl": "24h" + } + volumes: + - ./docker/vault/config:/config:ro + - postgres-socket:/sockets/postgres + - vault-data:/vault/file + - vault-postgres-app-token:/tokens/postgres-app + - vault-socket:/sockets/vault + entrypoint: /usr/bin/dumb-init -- + command: > + sh -c " + chown -R vault:vault /sockets/vault && + docker-entrypoint.sh vault server -config=/vault/config/local.json + " + + +secrets: + CHAINLIT_AUTH_SECRET: + external: true + CLOUDFLARE_SECRET_KEY: + external: true + OAUTH_AUTH0_CLIENT_SECRET: + external: true + OAUTH_GOOGLE_CLIENT_SECRET: + external: true + OPENAI_API_KEY: + external: true + TAVILY_API_KEY: + external: true + + +volumes: + postgres-data: + postgres-socket: + vault-data: + vault-postgres-app-token: + vault-socket: diff --git a/docker/init-docker-secrets.sh b/docker/init-docker-secrets.sh new file mode 100755 index 00000000..bee33888 --- /dev/null +++ b/docker/init-docker-secrets.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +# Check if a file path is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +yaml_file="$1" + +# Parse compose.yaml to initialize blank secrets +awk ' +BEGIN { inside_secrets = 0; parent_key = ""; } +/^secrets:/ { inside_secrets = 1; next; } +inside_secrets && /^[[:space:]]{2}[A-Za-z0-9_]+:/ { + parent_key = $1; + sub(/:$/, "", parent_key); +} +inside_secrets && /^[[:space:]]{4}external:[[:space:]]*true/ { + print parent_key; +} +' "$yaml_file" | while read -r secret_name; do + echo "Creating blank Docker Secret: $secret_name" + echo -n " " | docker secret create "$secret_name" - +done diff --git a/docker/postgres/config/postgresql.conf b/docker/postgres/config/postgresql.conf new file mode 100644 index 00000000..9c369a11 --- /dev/null +++ b/docker/postgres/config/postgresql.conf @@ -0,0 +1,845 @@ +# ----------------------------- +# PostgreSQL configuration file +# ----------------------------- +# +# This file consists of lines of the form: +# +# name = value +# +# (The "=" is optional.) Whitespace may be used. Comments are introduced with +# "#" anywhere on a line. The complete list of parameter names and allowed +# values can be found in the PostgreSQL documentation. +# +# The commented-out settings shown in this file represent the default values. +# Re-commenting a setting is NOT sufficient to revert it to the default value; +# you need to reload the server. +# +# This file is read on server startup and when the server receives a SIGHUP +# signal. If you edit the file on a running system, you have to SIGHUP the +# server for the changes to take effect, run "pg_ctl reload", or execute +# "SELECT pg_reload_conf()". Some parameters, which are marked below, +# require a server shutdown and restart to take effect. +# +# Any parameter can also be given as a command-line option to the server, e.g., +# "postgres -c log_connections=on". Some parameters can be changed at run time +# with the "SET" SQL command. +# +# Memory units: B = bytes Time units: us = microseconds +# kB = kilobytes ms = milliseconds +# MB = megabytes s = seconds +# GB = gigabytes min = minutes +# TB = terabytes h = hours +# d = days + + +#------------------------------------------------------------------------------ +# FILE LOCATIONS +#------------------------------------------------------------------------------ + +# The default values of these variables are driven from the -D command-line +# option or PGDATA environment variable, represented here as ConfigDir. + +#data_directory = 'ConfigDir' # use data in another directory + # (change requires restart) +#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file + # (change requires restart) +#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file + # (change requires restart) + +# If external_pid_file is not explicitly set, no extra PID file is written. +#external_pid_file = '' # write an extra PID file + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONNECTIONS AND AUTHENTICATION +#------------------------------------------------------------------------------ + +# - Connection Settings - + +listen_addresses = '' + # comma-separated list of addresses; + # defaults to 'localhost'; use '*' for all + # (change requires restart) +#port = 5432 # (change requires restart) +#max_connections = 100 # (change requires restart) +#reserved_connections = 0 # (change requires restart) +#superuser_reserved_connections = 3 # (change requires restart) +unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories + # (change requires restart) +unix_socket_group = '1000' # (change requires restart) +unix_socket_permissions = 0660 # begin with 0 to use octal notation + # (change requires restart) +#bonjour = off # advertise server via Bonjour + # (change requires restart) +#bonjour_name = '' # defaults to the computer name + # (change requires restart) + +# - TCP settings - +# see "man tcp" for details + +#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; + # 0 selects the system default +#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; + # 0 selects the system default +#tcp_keepalives_count = 0 # TCP_KEEPCNT; + # 0 selects the system default +#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; + # 0 selects the system default + +#client_connection_check_interval = 0 # time between checks for client + # disconnection while running queries; + # 0 for never + +# - Authentication - + +#authentication_timeout = 1min # 1s-600s +#password_encryption = scram-sha-256 # scram-sha-256 or md5 +#scram_iterations = 4096 + +# GSSAPI using Kerberos +#krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab' +#krb_caseins_users = off +#gss_accept_delegation = off + +# - SSL - + +#ssl = off +#ssl_ca_file = '' +#ssl_cert_file = 'server.crt' +#ssl_crl_file = '' +#ssl_crl_dir = '' +#ssl_key_file = 'server.key' +#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers +#ssl_prefer_server_ciphers = on +#ssl_ecdh_curve = 'prime256v1' +#ssl_min_protocol_version = 'TLSv1.2' +#ssl_max_protocol_version = '' +#ssl_dh_params_file = '' +#ssl_passphrase_command = '' +#ssl_passphrase_command_supports_reload = off + + +#------------------------------------------------------------------------------ +# RESOURCE USAGE (except WAL) +#------------------------------------------------------------------------------ + +# - Memory - + +#shared_buffers = 128MB # min 128kB + # (change requires restart) +#huge_pages = try # on, off, or try + # (change requires restart) +#huge_page_size = 0 # zero for system default + # (change requires restart) +#temp_buffers = 8MB # min 800kB +#max_prepared_transactions = 0 # zero disables the feature + # (change requires restart) +# Caution: it is not advisable to set max_prepared_transactions nonzero unless +# you actively intend to use prepared transactions. +#work_mem = 4MB # min 64kB +#hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem +#maintenance_work_mem = 64MB # min 64kB +#autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem +#logical_decoding_work_mem = 64MB # min 64kB +#max_stack_depth = 2MB # min 100kB +#shared_memory_type = mmap # the default is the first option + # supported by the operating system: + # mmap + # sysv + # windows + # (change requires restart) +#dynamic_shared_memory_type = posix # the default is usually the first option + # supported by the operating system: + # posix + # sysv + # windows + # mmap + # (change requires restart) +#min_dynamic_shared_memory = 0MB # (change requires restart) +#vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring; + # 0 to disable vacuum buffer access strategy; + # range 128kB to 16GB + +# SLRU buffers (change requires restart) +#commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto) +#multixact_offset_buffers = 16 # memory for pg_multixact/offsets +#multixact_member_buffers = 32 # memory for pg_multixact/members +#notify_buffers = 16 # memory for pg_notify +#serializable_buffers = 32 # memory for pg_serial +#subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto) +#transaction_buffers = 0 # memory for pg_xact (0 = auto) + +# - Disk - + +#temp_file_limit = -1 # limits per-process temp file space + # in kilobytes, or -1 for no limit + +#max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated + # for NOTIFY / LISTEN queue + +# - Kernel Resources - + +#max_files_per_process = 1000 # min 64 + # (change requires restart) + +# - Cost-Based Vacuum Delay - + +#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) +#vacuum_cost_page_hit = 1 # 0-10000 credits +#vacuum_cost_page_miss = 2 # 0-10000 credits +#vacuum_cost_page_dirty = 20 # 0-10000 credits +#vacuum_cost_limit = 200 # 1-10000 credits + +# - Background Writer - + +#bgwriter_delay = 200ms # 10-10000ms between rounds +#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables +#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round +#bgwriter_flush_after = 0 # measured in pages, 0 disables + +# - Asynchronous Behavior - + +#backend_flush_after = 0 # measured in pages, 0 disables +#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching +#maintenance_io_concurrency = 10 # 1-1000; 0 disables prefetching +#io_combine_limit = 128kB # usually 1-32 blocks (depends on OS) +#max_worker_processes = 8 # (change requires restart) +#max_parallel_workers_per_gather = 2 # limited by max_parallel_workers +#max_parallel_maintenance_workers = 2 # limited by max_parallel_workers +#max_parallel_workers = 8 # number of max_worker_processes that + # can be used in parallel operations +#parallel_leader_participation = on + + +#------------------------------------------------------------------------------ +# WRITE-AHEAD LOG +#------------------------------------------------------------------------------ + +# - Settings - + +#wal_level = replica # minimal, replica, or logical + # (change requires restart) +#fsync = on # flush data to disk for crash safety + # (turning this off can cause + # unrecoverable data corruption) +#synchronous_commit = on # synchronization level; + # off, local, remote_write, remote_apply, or on +#wal_sync_method = fsync # the default is the first option + # supported by the operating system: + # open_datasync + # fdatasync (default on Linux and FreeBSD) + # fsync + # fsync_writethrough + # open_sync +#full_page_writes = on # recover from partial page writes +#wal_log_hints = off # also do full page writes of non-critical updates + # (change requires restart) +#wal_compression = off # enables compression of full-page writes; + # off, pglz, lz4, zstd, or on +#wal_init_zero = on # zero-fill new WAL files +#wal_recycle = on # recycle WAL files +#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers + # (change requires restart) +#wal_writer_delay = 200ms # 1-10000 milliseconds +#wal_writer_flush_after = 1MB # measured in pages, 0 disables +#wal_skip_threshold = 2MB + +#commit_delay = 0 # range 0-100000, in microseconds +#commit_siblings = 5 # range 1-1000 + +# - Checkpoints - + +#checkpoint_timeout = 5min # range 30s-1d +#checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0 +#checkpoint_flush_after = 0 # measured in pages, 0 disables +#checkpoint_warning = 30s # 0 disables +#max_wal_size = 1GB +#min_wal_size = 80MB + +# - Prefetching during recovery - + +#recovery_prefetch = try # prefetch pages referenced in the WAL? +#wal_decode_buffer_size = 512kB # lookahead window used for prefetching + # (change requires restart) + +# - Archiving - + +#archive_mode = off # enables archiving; off, on, or always + # (change requires restart) +#archive_library = '' # library to use to archive a WAL file + # (empty string indicates archive_command should + # be used) +#archive_command = '' # command to use to archive a WAL file + # placeholders: %p = path of file to archive + # %f = file name only + # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' +#archive_timeout = 0 # force a WAL file switch after this + # number of seconds; 0 disables + +# - Archive Recovery - + +# These are only used in recovery mode. + +#restore_command = '' # command to use to restore an archived WAL file + # placeholders: %p = path of file to restore + # %f = file name only + # e.g. 'cp /mnt/server/archivedir/%f %p' +#archive_cleanup_command = '' # command to execute at every restartpoint +#recovery_end_command = '' # command to execute at completion of recovery + +# - Recovery Target - + +# Set these only when performing a targeted recovery. + +#recovery_target = '' # 'immediate' to end recovery as soon as a + # consistent state is reached + # (change requires restart) +#recovery_target_name = '' # the named restore point to which recovery will proceed + # (change requires restart) +#recovery_target_time = '' # the time stamp up to which recovery will proceed + # (change requires restart) +#recovery_target_xid = '' # the transaction ID up to which recovery will proceed + # (change requires restart) +#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed + # (change requires restart) +#recovery_target_inclusive = on # Specifies whether to stop: + # just after the specified recovery target (on) + # just before the recovery target (off) + # (change requires restart) +#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID + # (change requires restart) +#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' + # (change requires restart) + +# - WAL Summarization - + +#summarize_wal = off # run WAL summarizer process? +#wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never + + +#------------------------------------------------------------------------------ +# REPLICATION +#------------------------------------------------------------------------------ + +# - Sending Servers - + +# Set these on the primary and on any standby that will send replication data. + +#max_wal_senders = 10 # max number of walsender processes + # (change requires restart) +#max_replication_slots = 10 # max number of replication slots + # (change requires restart) +#wal_keep_size = 0 # in megabytes; 0 disables +#max_slot_wal_keep_size = -1 # in megabytes; -1 disables +#wal_sender_timeout = 60s # in milliseconds; 0 disables +#track_commit_timestamp = off # collect timestamp of transaction commit + # (change requires restart) + +# - Primary Server - + +# These settings are ignored on a standby server. + +#synchronous_standby_names = '' # standby servers that provide sync rep + # method to choose sync standbys, number of sync standbys, + # and comma-separated list of application_name + # from standby(s); '*' = all +#synchronized_standby_slots = '' # streaming replication standby server slot + # names that logical walsender processes will wait for + +# - Standby Servers - + +# These settings are ignored on a primary server. + +#primary_conninfo = '' # connection string to sending server +#primary_slot_name = '' # replication slot on sending server +#hot_standby = on # "off" disallows queries during recovery + # (change requires restart) +#max_standby_archive_delay = 30s # max delay before canceling queries + # when reading WAL from archive; + # -1 allows indefinite delay +#max_standby_streaming_delay = 30s # max delay before canceling queries + # when reading streaming WAL; + # -1 allows indefinite delay +#wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name + # is not set +#wal_receiver_status_interval = 10s # send replies at least this often + # 0 disables +#hot_standby_feedback = off # send info from standby to prevent + # query conflicts +#wal_receiver_timeout = 60s # time that receiver waits for + # communication from primary + # in milliseconds; 0 disables +#wal_retrieve_retry_interval = 5s # time to wait before retrying to + # retrieve WAL after a failed attempt +#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery +#sync_replication_slots = off # enables slot synchronization on the physical standby from the primary + +# - Subscribers - + +# These settings are ignored on a publisher. + +#max_logical_replication_workers = 4 # taken from max_worker_processes + # (change requires restart) +#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers +#max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers + + +#------------------------------------------------------------------------------ +# QUERY TUNING +#------------------------------------------------------------------------------ + +# - Planner Method Configuration - + +#enable_async_append = on +#enable_bitmapscan = on +#enable_gathermerge = on +#enable_hashagg = on +#enable_hashjoin = on +#enable_incremental_sort = on +#enable_indexscan = on +#enable_indexonlyscan = on +#enable_material = on +#enable_memoize = on +#enable_mergejoin = on +#enable_nestloop = on +#enable_parallel_append = on +#enable_parallel_hash = on +#enable_partition_pruning = on +#enable_partitionwise_join = off +#enable_partitionwise_aggregate = off +#enable_presorted_aggregate = on +#enable_seqscan = on +#enable_sort = on +#enable_tidscan = on +#enable_group_by_reordering = on + +# - Planner Cost Constants - + +#seq_page_cost = 1.0 # measured on an arbitrary scale +#random_page_cost = 4.0 # same scale as above +#cpu_tuple_cost = 0.01 # same scale as above +#cpu_index_tuple_cost = 0.005 # same scale as above +#cpu_operator_cost = 0.0025 # same scale as above +#parallel_setup_cost = 1000.0 # same scale as above +#parallel_tuple_cost = 0.1 # same scale as above +#min_parallel_table_scan_size = 8MB +#min_parallel_index_scan_size = 512kB +#effective_cache_size = 4GB + +#jit_above_cost = 100000 # perform JIT compilation if available + # and query more expensive than this; + # -1 disables +#jit_inline_above_cost = 500000 # inline small functions if query is + # more expensive than this; -1 disables +#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if + # query is more expensive than this; + # -1 disables + +# - Genetic Query Optimizer - + +#geqo = on +#geqo_threshold = 12 +#geqo_effort = 5 # range 1-10 +#geqo_pool_size = 0 # selects default based on effort +#geqo_generations = 0 # selects default based on effort +#geqo_selection_bias = 2.0 # range 1.5-2.0 +#geqo_seed = 0.0 # range 0.0-1.0 + +# - Other Planner Options - + +#default_statistics_target = 100 # range 1-10000 +#constraint_exclusion = partition # on, off, or partition +#cursor_tuple_fraction = 0.1 # range 0.0-1.0 +#from_collapse_limit = 8 +#jit = on # allow JIT compilation +#join_collapse_limit = 8 # 1 disables collapsing of explicit + # JOIN clauses +#plan_cache_mode = auto # auto, force_generic_plan or + # force_custom_plan +#recursive_worktable_factor = 10.0 # range 0.001-1000000 + + +#------------------------------------------------------------------------------ +# REPORTING AND LOGGING +#------------------------------------------------------------------------------ + +# - Where to Log - + +#log_destination = 'stderr' # Valid values are combinations of + # stderr, csvlog, jsonlog, syslog, and + # eventlog, depending on platform. + # csvlog and jsonlog require + # logging_collector to be on. + +# This is used when logging to stderr: +#logging_collector = off # Enable capturing of stderr, jsonlog, + # and csvlog into log files. Required + # to be on for csvlogs and jsonlogs. + # (change requires restart) + +# These are only used if logging_collector is on: +#log_directory = 'log' # directory where log files are written, + # can be absolute or relative to PGDATA +#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, + # can include strftime() escapes +#log_file_mode = 0600 # creation mode for log files, + # begin with 0 to use octal notation +#log_rotation_age = 1d # Automatic rotation of logfiles will + # happen after that time. 0 disables. +#log_rotation_size = 10MB # Automatic rotation of logfiles will + # happen after that much log output. + # 0 disables. +#log_truncate_on_rotation = off # If on, an existing log file with the + # same name as the new log file will be + # truncated rather than appended to. + # But such truncation only occurs on + # time-driven rotation, not on restarts + # or size-driven rotation. Default is + # off, meaning append to existing files + # in all cases. + +# These are relevant when logging to syslog: +#syslog_facility = 'LOCAL0' +#syslog_ident = 'postgres' +#syslog_sequence_numbers = on +#syslog_split_messages = on + +# This is only relevant when logging to eventlog (Windows): +# (change requires restart) +#event_source = 'PostgreSQL' + +# - When to Log - + +#log_min_messages = warning # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic + +#log_min_error_statement = error # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic (effectively off) + +#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements + # and their durations, > 0 logs only + # statements running at least this number + # of milliseconds + +#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements + # and their durations, > 0 logs only a sample of + # statements running at least this number + # of milliseconds; + # sample fraction is determined by log_statement_sample_rate + +#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding + # log_min_duration_sample to be logged; + # 1.0 logs all such statements, 0.0 never logs + + +#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements + # are logged regardless of their duration; 1.0 logs all + # statements from all transactions, 0.0 never logs + +#log_startup_progress_interval = 10s # Time between progress updates for + # long-running startup operations. + # 0 disables the feature, > 0 indicates + # the interval in milliseconds. + +# - What to Log - + +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +#debug_pretty_print = on +#log_autovacuum_min_duration = 10min # log autovacuum activity; + # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#log_checkpoints = on +#log_connections = off +#log_disconnections = off +#log_duration = off +#log_error_verbosity = default # terse, default, or verbose messages +#log_hostname = off +#log_line_prefix = '%m [%p] ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %b = backend type + # %p = process ID + # %P = process ID of parallel group leader + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %Q = query ID (0 if none or not computed) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = off # log lock waits >= deadlock_timeout +#log_recovery_conflict_waits = off # log standby recovery conflict waits + # >= deadlock_timeout +#log_parameter_max_length = -1 # when logging statements, limit logged + # bind-parameter values to N bytes; + # -1 means print in full, 0 disables +#log_parameter_max_length_on_error = 0 # when logging an error, limit logged + # bind-parameter values to N bytes; + # -1 means print in full, 0 disables +#log_statement = 'none' # none, ddl, mod, all +#log_replication_commands = off +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files +#log_timezone = 'GMT' + +# - Process Title - + +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) +#update_process_title = on + + +#------------------------------------------------------------------------------ +# STATISTICS +#------------------------------------------------------------------------------ + +# - Cumulative Query and Index Statistics - + +#track_activities = on +#track_activity_query_size = 1024 # (change requires restart) +#track_counts = on +#track_io_timing = off +#track_wal_io_timing = off +#track_functions = none # none, pl, all +#stats_fetch_consistency = cache # cache, none, snapshot + + +# - Monitoring - + +#compute_query_id = auto +#log_statement_stats = off +#log_parser_stats = off +#log_planner_stats = off +#log_executor_stats = off + + +#------------------------------------------------------------------------------ +# AUTOVACUUM +#------------------------------------------------------------------------------ + +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses + # (change requires restart) +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts + # before vacuum; -1 disables insert + # vacuums +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of inserts over table + # size before insert vacuum +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit + + +#------------------------------------------------------------------------------ +# CLIENT CONNECTION DEFAULTS +#------------------------------------------------------------------------------ + +# - Statement Behavior - + +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names +#row_security = on +#default_table_access_method = 'heap' +#default_tablespace = '' # a tablespace name, '' uses the default +#default_toast_compression = 'pglz' # 'pglz' or 'lz4' +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace +#check_function_bodies = on +#default_transaction_isolation = 'read committed' +#default_transaction_read_only = off +#default_transaction_deferrable = off +#session_replication_role = 'origin' +#statement_timeout = 0 # in milliseconds, 0 is disabled +#transaction_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#idle_session_timeout = 0 # in milliseconds, 0 is disabled +#vacuum_freeze_table_age = 150000000 +#vacuum_freeze_min_age = 50000000 +#vacuum_failsafe_age = 1600000000 +#vacuum_multixact_freeze_table_age = 150000000 +#vacuum_multixact_freeze_min_age = 5000000 +#vacuum_multixact_failsafe_age = 1600000000 +#bytea_output = 'hex' # hex, escape +#xmlbinary = 'base64' +#xmloption = 'content' +#gin_pending_list_limit = 4MB +#createrole_self_grant = '' # set and/or inherit +#event_triggers = on + +# - Locale and Formatting - + +#datestyle = 'iso, mdy' +#intervalstyle = 'postgres' +#timezone = 'GMT' +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 1 # min -15, max 3; any value >0 actually + # selects precise output mode +#client_encoding = sql_ascii # actually, defaults to database + # encoding + +# These settings are initialized by initdb, but they can be changed. +#lc_messages = '' # locale for system error message + # strings +#lc_monetary = 'C' # locale for monetary formatting +#lc_numeric = 'C' # locale for number formatting +#lc_time = 'C' # locale for time formatting + +#icu_validation_level = warning # report ICU locale validation + # errors at the given level + +# default configuration for text search +#default_text_search_config = 'pg_catalog.simple' + +# - Shared Library Preloading - + +#local_preload_libraries = '' +#session_preload_libraries = '' + +shared_preload_libraries = 'auth_delay' # (change requires restart) +auth_delay.milliseconds = 500 + +#jit_provider = 'llvmjit' # JIT library to use + +# - Other Defaults - + +#dynamic_library_path = '$libdir' +#gin_fuzzy_search_limit = 0 + + +#------------------------------------------------------------------------------ +# LOCK MANAGEMENT +#------------------------------------------------------------------------------ + +#deadlock_timeout = 1s +#max_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 + + +#------------------------------------------------------------------------------ +# VERSION AND PLATFORM COMPATIBILITY +#------------------------------------------------------------------------------ + +# - Previous PostgreSQL Versions - + +#array_nulls = on +#backslash_quote = safe_encoding # on, off, or safe_encoding +#escape_string_warning = on +#lo_compat_privileges = off +#quote_all_identifiers = off +#standard_conforming_strings = on +#synchronize_seqscans = on + +# - Other Platforms and Clients - + +#transform_null_equals = off +#allow_alter_system = on + + +#------------------------------------------------------------------------------ +# ERROR HANDLING +#------------------------------------------------------------------------------ + +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) +#recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+) + + +#------------------------------------------------------------------------------ +# CONFIG FILE INCLUDES +#------------------------------------------------------------------------------ + +# These options allow settings to be loaded from files other than the +# default postgresql.conf. Note that these are directives, not variable +# assignments, so they can usefully be given more than once. + +#include_dir = '...' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '...' # include file only if it exists +#include = '...' # include file + + +#------------------------------------------------------------------------------ +# CUSTOMIZED OPTIONS +#------------------------------------------------------------------------------ + +# Add settings for extensions here diff --git a/docker/postgres/initdb/0-create-dbs.sql b/docker/postgres/initdb/0-create-dbs.sql new file mode 100644 index 00000000..f2221741 --- /dev/null +++ b/docker/postgres/initdb/0-create-dbs.sql @@ -0,0 +1,2 @@ +CREATE DATABASE chainlit; +CREATE DATABASE langgraph; diff --git a/docker/vault/config/1-init-vault.sh b/docker/vault/config/1-init-vault.sh new file mode 100755 index 00000000..6edf7d69 --- /dev/null +++ b/docker/vault/config/1-init-vault.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +# Check if $1 and $2 are provided, if not, prompt the user +if [ -z "${1:-}" ]; then + read -p "Enter the number of key shares (total # of keys generated): " key_shares +else + key_shares="$1" +fi + +if [ -z "${2:-}" ]; then + read -p "Enter the key threshold (minimum # of keys to unseal): " key_threshold +else + key_threshold="$2" +fi + +# Run the vault operator init command and pipe the output to less, capturing errors +vault operator init -key-shares="$key_shares" -key-threshold="$key_threshold" 2>&1 | less diff --git a/docker/vault/config/4-setup-postgres.sh b/docker/vault/config/4-setup-postgres.sh new file mode 100755 index 00000000..5e35def5 --- /dev/null +++ b/docker/vault/config/4-setup-postgres.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +# Create "postgres-app" role +vault policy write postgres-app /config/postgres-app-policy.hcl +vault token create -field=token -policy=postgres-app | vault login -token-only - > /tokens/postgres-app/token + +# Create "postgres-admin" vault token and login with it +vault policy write postgres-admin /config/postgres-admin-policy.hcl +export VAULT_TOKEN=$( + vault token create -field=token -policy=postgres-admin | vault login -token-only - +) + +# Enable database secrets engine with Postgres DB +vault secrets enable database +vault write database/config/postgres \ + plugin_name=postgresql-database-plugin \ + connection_url="postgresql://{{username}}:{{password}}@postgres?host=/sockets/postgres" \ + allowed_roles="*" \ + username="postgres" \ + password=$POSTGRES_PASSWORD + +# Rotate $POSTGRES_PASSWORD out of use ASAP +vault write -force database/rotate-root/postgres + +# Create the database role for postgres-app +vault write database/roles/postgres-app \ + db_name=postgres \ + creation_statements=@/config/app-roles.sql \ + default_ttl=1h \ + max_ttl=24h diff --git a/docker/vault/config/README.md b/docker/vault/config/README.md new file mode 100644 index 00000000..dcffe9af --- /dev/null +++ b/docker/vault/config/README.md @@ -0,0 +1,71 @@ +# Using Vault + +The files in this directory are to be used inside of a **`hashicorp/vault`** + Docker container. + +This project's `compose.yaml` mounts this directory to `/config`. + + +### README Overview + +- [First-time setup](#first-time-setup) + + Start here for new deployments. +- [Unsealing Vault (Nth-time setup)](#unsealing-vault-nth-time-setup) + + +- [What is Vault?](#what-is-vault) + + +## First-time setup + +These steps are only required for an initial deployment of Vault for Postgres + credential-management usage. + +1. Initialize the vault root token and unseal keys. + - Copy the token and keys somewhere safe for now. + - 🛑 _**Treat these as you would a sensitive password! + Especially the root token!**_ 🛑 +```sh +/config/1-init-vault.sh # follow the prompts +``` + +2. Vault will always startup in a **sealed** state. Unseal it. + - Repeat the command with different keys until the unseal threshold is reached. + - This is required every time the container is restarted. +```sh +vault operator unseal # paste an unseal key when prompted +``` + +3. Login using the root token (just this once). +```sh +export VAULT_TOKEN=$(vault login -token-only) # paste the root token when prompted +``` + +4. Setup Vault policies, database secrets engine, and Postgres roles. +```sh +/config/4-setup-postgres.sh +``` + +5. Distribute unseal keys to team members. Do not store them all in one place. + - Consider deleting (or at least encrypting) the root token. + - ⚠️ _**Losing a quorum of unseal keys and the root token means a loss of all + Vault contents, including Postgres access.**_ ⚠️ + + +## Unsealing Vault (Nth-time setup) + +Use the `vault operator unseal` command with unseal keys (repeatedly for the + threshold number of unseal keys), as described in the + [above](#first-time-setup) section. + + +## What is Vault? + +[Hashicorp Vault](https://developer.hashicorp.com/vault) is a secrets manager +for protecting sensitive information such as +- authentication tokens, +- API keys, and +- database credentials. + +Vault also features a +[database secrets engine](https://developer.hashicorp.com/vault/docs/secrets/databases) +which we leverage to issue time-limited credentials to our application. diff --git a/docker/vault/config/app-roles.sql b/docker/vault/config/app-roles.sql new file mode 100644 index 00000000..19a36c68 --- /dev/null +++ b/docker/vault/config/app-roles.sql @@ -0,0 +1,15 @@ +CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; + +-- Role can connect and create within chainlit and langgraph +GRANT + CREATE, + CONNECT +ON DATABASE chainlit, langgraph TO "{{name}}"; + +GRANT + SELECT, + INSERT, + UPDATE, + DELETE, + REFERENCES +ON ALL TABLES IN SCHEMA public TO "{{name}}"; diff --git a/docker/vault/config/postgres-admin-policy.hcl b/docker/vault/config/postgres-admin-policy.hcl new file mode 100644 index 00000000..26da16fb --- /dev/null +++ b/docker/vault/config/postgres-admin-policy.hcl @@ -0,0 +1,7 @@ +path "sys/mounts/database" { + capabilities = [ "create", "update" ] +} + +path "database/*" { + capabilities = [ "create", "update" ] +} diff --git a/docker/vault/config/postgres-app-policy.hcl b/docker/vault/config/postgres-app-policy.hcl new file mode 100644 index 00000000..3a4ed365 --- /dev/null +++ b/docker/vault/config/postgres-app-policy.hcl @@ -0,0 +1,3 @@ +path "database/creds/postgres-app" { + capabilities = [ "read" ] +} diff --git a/env_template b/env_template index 26c4cb4c..9ec2303d 100644 --- a/env_template +++ b/env_template @@ -34,3 +34,9 @@ TAVILY_API_KEY= # 400 for anything else. Set this only for a model that rule does not yet know # about; see resolve_temperature in src/agent/graph.py. #LLM_TEMPERATURE= + +# Locale and timezone for the Postgres container's initdb (ICU collation). +LOCALE=en_US +TIMEZONE=America/Toronto +# The guest instance's public URI, served alongside the authenticated one. +CHAINLIT_URI_NO_LOGIN= diff --git a/poetry.lock b/poetry.lock index f7b92589..181b642f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -514,17 +514,17 @@ files = [ [[package]] name = "boto3" -version = "1.43.90" +version = "1.43.91" description = "The AWS SDK for Python" optional = false python-versions = ">=3.10" files = [ - {file = "boto3-1.43.90-py3-none-any.whl", hash = "sha256:aaaa1216d65ddb3dcf86bf9d93cfa436af05cbcd1e1f8b2847f2b83116012186"}, - {file = "boto3-1.43.90.tar.gz", hash = "sha256:4b669742d5b45b8fd20ca50ac414a4e4cf995ebb8f280d21be28676e71c97594"}, + {file = "boto3-1.43.91-py3-none-any.whl", hash = "sha256:5ff948cfac8bff72227930e0894a8542731e1998634d1ffd8a074c20a90af919"}, + {file = "boto3-1.43.91.tar.gz", hash = "sha256:98643e500883bf6fcd13d04bb19da983bf97e5f1c600fc11470ce45437fa96b4"}, ] [package.dependencies] -botocore = ">=1.43.90,<1.44.0" +botocore = ">=1.43.91,<1.44.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.19.0,<0.20.0" @@ -533,13 +533,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.43.90" +version = "1.43.91" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.10" files = [ - {file = "botocore-1.43.90-py3-none-any.whl", hash = "sha256:65f3394ef07314e45c92a90120988531d651136390d464a94938a92f665893f7"}, - {file = "botocore-1.43.90.tar.gz", hash = "sha256:a139ed601e8b8fb1d730022355fe2b284b8c15cfe9ac0f100254a35d2273e1d3"}, + {file = "botocore-1.43.91-py3-none-any.whl", hash = "sha256:f96363d4caf50bce45fe2fdc2d4d86b3bea7f5cd805169d53c1371c2dd4772b6"}, + {file = "botocore-1.43.91.tar.gz", hash = "sha256:0f12bceb8c5d90a0c60f326e883bf674ded8c7d7c18ee0b559843b3a6c52f2ad"}, ] [package.dependencies] @@ -552,13 +552,13 @@ crt = ["awscrt (==0.36.0)"] [[package]] name = "build" -version = "1.6.0" +version = "1.6.1" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.10" files = [ - {file = "build-1.6.0-py3-none-any.whl", hash = "sha256:f7aaf1ebbb79178a02ba248bb524f2176b256017e17e8e4bd4289c7b38cc2bad"}, - {file = "build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af"}, + {file = "build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7"}, + {file = "build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53"}, ] [package.dependencies] @@ -1978,6 +1978,23 @@ files = [ {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] +[[package]] +name = "hvac" +version = "2.4.0" +description = "HashiCorp Vault API client" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "hvac-2.4.0-py3-none-any.whl", hash = "sha256:008db5efd8c2f77bd37d2368ea5f713edceae1c65f11fd608393179478649e0f"}, + {file = "hvac-2.4.0.tar.gz", hash = "sha256:e0056ad9064e7923e874e6769015b032580b639e29246f5ab1044f7959c1c7e0"}, +] + +[package.dependencies] +requests = ">=2.27.1,<3.0.0" + +[package.extras] +parser = ["pyhcl (>=0.4.4,<0.5.0)"] + [[package]] name = "idna" version = "3.19" @@ -2518,13 +2535,13 @@ ollama = ">=0.6.1,<1.0.0" [[package]] name = "langchain-openai" -version = "1.6.1" +version = "1.6.2" description = "An integration package connecting OpenAI and LangChain" optional = false python-versions = "<4.0.0,>=3.10.0" files = [ - {file = "langchain_openai-1.6.1-py3-none-any.whl", hash = "sha256:0642a96662777512de94830b156cb61b89db7e92a04f26f1f70da34c38ff2d72"}, - {file = "langchain_openai-1.6.1.tar.gz", hash = "sha256:a09c329d6d9c5b5bf7025c0d5a5cc8e7ec8d784d662861809e1ec1cce1cd3d26"}, + {file = "langchain_openai-1.6.2-py3-none-any.whl", hash = "sha256:368e34ab3bdd30af422371b2f2f9346f21e79beae8b0823c253633819d1fd2f8"}, + {file = "langchain_openai-1.6.2.tar.gz", hash = "sha256:bec6d190e1e47e774c2be8728bc93457cef1ab2ec3295c277d8daefab434e880"}, ] [package.dependencies] @@ -2647,13 +2664,13 @@ websockets = ">=14,<17" [[package]] name = "langsmith" -version = "0.12.3" +version = "0.12.4" description = "Client library to connect to the LangSmith Observability and Evaluation Platform." optional = false python-versions = ">=3.10" files = [ - {file = "langsmith-0.12.3-py3-none-any.whl", hash = "sha256:1d3f9f1eb4b2aab8797c9d58e5af158c5695980eef42ce33d0a5b869c167e088"}, - {file = "langsmith-0.12.3.tar.gz", hash = "sha256:d81724b6af67dbbd0c022de14c2dc487c553fd274f5758d465180e92fc69212e"}, + {file = "langsmith-0.12.4-py3-none-any.whl", hash = "sha256:b2edaa49baeec0c7347a84ca0f755039dcff9e9e52f0b9dc26423ec2a98a4dab"}, + {file = "langsmith-0.12.4.tar.gz", hash = "sha256:f486435323eeb0c525087cc694f0b7cd92e255f340db08db643204c789f4f1c4"}, ] [package.dependencies] @@ -5891,20 +5908,20 @@ files = [ [[package]] name = "pydantic" -version = "2.14.0b1" +version = "2.14.0b2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.10" files = [ - {file = "pydantic-2.14.0b1-py3-none-any.whl", hash = "sha256:c7803eac0d891142724e06f9a53264feb1b4e3dfdf89890cbe7eb819795f6b07"}, - {file = "pydantic-2.14.0b1.tar.gz", hash = "sha256:d974b3fe7e6ad2a3a5718842d4fc3f5f78142d431a8f73d2d08bc498acf39c8a"}, + {file = "pydantic-2.14.0b2-py3-none-any.whl", hash = "sha256:4556c932fb3dbb7e9ac773bee241b2eb7058a2f9ff1d850d45d7d11cb6e54b68"}, + {file = "pydantic-2.14.0b2.tar.gz", hash = "sha256:bb3b4bdb0c69e0eb349e43f07eddfd755547cae3aab3df34cc4c95caa44a5077"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.48.0" -typing-extensions = ">=4.15.0" -typing-inspection = ">=0.4.2" +pydantic-core = "2.49.0" +typing-extensions = ">=4.16.0" +typing-inspection = ">=0.4.4" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -5912,152 +5929,152 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.48.0" +version = "2.49.0" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.10" files = [ - {file = "pydantic_core-2.48.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ef32e1fc6daeb6b9f0112e121911096d28563dbac2d9bb6f4294cadbdbf5004d"}, - {file = "pydantic_core-2.48.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9129dc3ec5b8936078d512707a70dc591f536991d013bc952699714a41f73b73"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e2253856265218d43afb673823f1c0d3863b9bbaa26ca0c0861402385f7e49"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:227a0439fc99197defc197dc0bac8c72474c681a808a6870cd62bfd4b0a4ab56"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0775550fa7bf15ed97dba9ef37be1cbbb6f5fd79a53a51c78d4e8823e456f575"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78c66e6a8a9981dfe93c0732ebf51af9f3296255c72a5d9ec29159df2176abff"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e161be86326c9a8e263bf6ba9f26d1d48b68aa90ce07bbcd6b2c8c7565200a88"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:85689e2ed2edc5bd2e207135400e1d9cb9448dba11f31b6df1529b65059d8624"}, - {file = "pydantic_core-2.48.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:156155db186b956a4e957d1a31d5d0d4f621a27867516e67c198111dcbb694eb"}, - {file = "pydantic_core-2.48.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:69a2d08a38b454e6ee16824e15dfac4035de929c41378df021d8a7a67ee99e16"}, - {file = "pydantic_core-2.48.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1dac2c7bcb41e8f8b2b7e9937348f7ed94e63ba9d544b2c724b5e4adf800905b"}, - {file = "pydantic_core-2.48.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c77b4335446405f056287f34d3145b0f79c49aa2e4b93023c8708f1c3f50688"}, - {file = "pydantic_core-2.48.0-cp310-cp310-win32.whl", hash = "sha256:b9027262f14fb58e36f0fd617c285bf10b2b3ea31043771bb4f1b9a27fd795a0"}, - {file = "pydantic_core-2.48.0-cp310-cp310-win_amd64.whl", hash = "sha256:f21799be4afd02eb9fd2309dc076a5717fffee48b4d5536383f2281db0607de7"}, - {file = "pydantic_core-2.48.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1634b4b2687380472d314ee0dce7a98c056da3f6071fed2df52e4f70936f1f4e"}, - {file = "pydantic_core-2.48.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbad852aa108d223e45d91bf032d5ae303295227177ebcc0ef9c343d88db6aa5"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:024f06afbc680c6eb6edb6db02754e7bfa453e7fd943da05a77cceaf24a408ad"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3645e574266173554d243a414f9200df4f82350fed68e66402f2b1fd5fd5f09a"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bea268173cc91fa4bf5b4ac80a9b9425a8aa8634c8d2cd94208c4dd2cf67af6e"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0e1f53098ae6af238c5f1db3e25adc967625cbd0fdf5b05bf0034367d996d01"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4841c9fbb551c273cb5459526fdd296e30ae585b2b6a50bc4b1364b7df9707e"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:ab0f7e68c5b8776dd47bbc383d8b80a2362bcc193b7d1e5b10589415709d92cc"}, - {file = "pydantic_core-2.48.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:614a50093ce079ec2b381ef9b8ff10354eb420ee7b29df21b40e653bab9390b6"}, - {file = "pydantic_core-2.48.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:096519257817ce2ca02dd1cda1c7b48e936efa6a55358b592dce771312f07a0d"}, - {file = "pydantic_core-2.48.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a54e4802cebf85f95a22807450e0b23363af2fb815f1749343e47799eee56045"}, - {file = "pydantic_core-2.48.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9199c71f1ea4697edad7ad4712baff7988b4a69ff386a50b38e30b0d85755fc5"}, - {file = "pydantic_core-2.48.0-cp311-cp311-win32.whl", hash = "sha256:3b3a39816174eaabc092282d5501dffed4ab6f07e8ce9f866af171926f19e308"}, - {file = "pydantic_core-2.48.0-cp311-cp311-win_amd64.whl", hash = "sha256:e7431b7327c3875504abeb0f1fcd699f8c5b1b982345c6cba792546b65cee4c8"}, - {file = "pydantic_core-2.48.0-cp311-cp311-win_arm64.whl", hash = "sha256:186e24f569522570480f97b9372bef575875a35c4910ab4e1f31806bcefd93d6"}, - {file = "pydantic_core-2.48.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:86ab2c8185deea63d9e82406bfd82a8255ae82eaf8cfe8d30caede4b9859be2c"}, - {file = "pydantic_core-2.48.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:998fe237ad91f8b6a216a344a70cc566557b3c905c4c12c319426425476ddc5a"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0692d545312b1849111d7a801515dba648cc1d40f4d1a8c3c7355c92736b0af3"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f3f7ab3e97aa6d3d659043391b38c3cf41812c93b49692ed969d5beb66ceb29"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e918f1ef44f350b3c03570b1f9b0f2f97e7dfc843fd295951af3db6df6e639d"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8198d74ccef5e243c3662bf7b9f847d4afec93bd08d5db8ef1a16dadaec71a5a"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3463fb9e857362b896f345f6c4349d20b817865a05eb570bf838d7c406536412"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:7156d0bcc38c8084291b613696b3d23d24e4b8dc7a7a1070c5baa5907936e531"}, - {file = "pydantic_core-2.48.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3af1e349ecf55a66c53be2fec30fc5a528664ad3d0434aaef5b7b8475fdbd9a"}, - {file = "pydantic_core-2.48.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:794aab60bbb5a92bb856961b294e149140d81cdf364206c55832d32588137b3b"}, - {file = "pydantic_core-2.48.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:a52f3eafe91530b9b4b4016a20aabfce4d456be837d2ea7186eaf23ee3320d72"}, - {file = "pydantic_core-2.48.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2cff5642d8c477a79f7b7d05b77620d860cf80e4a0d1cdff72e08c598c12dd3a"}, - {file = "pydantic_core-2.48.0-cp312-cp312-win32.whl", hash = "sha256:c967fab606fc0cccea553c783a47136333bbc0bc27ed4c401460f2cdf4341147"}, - {file = "pydantic_core-2.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:403c28347ac5a0fb4b96b52dea1f5c23ce4515a67c9dc1232ffdc31b88988c3c"}, - {file = "pydantic_core-2.48.0-cp312-cp312-win_arm64.whl", hash = "sha256:f9620adcc4f0aa76b3e2357a9649b45c17aa2834d960f502224c8477db8a3261"}, - {file = "pydantic_core-2.48.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:742e93d9bcb63d9e5a0fb26e82f175ebf183bc861f8ee18b2b5d7ed09d831803"}, - {file = "pydantic_core-2.48.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:482c3707518a7163c8148686cd18d27c8f11f89cafa43a6834b42505d266b117"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a5e0f879c9fee3b94473f92860119b6c87779214e8545a1334f346ce5adc2e"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fde25b657fc1d960ab48f9282f6d01bcbe691a80f67aaa08055cf0842e35d5c"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b630303e2c555395d633c699c7b51b7a3428cee40382b1409698e0feec35f519"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2378f8e2f3cf79c449dce50d203111673b2f229769845338f40ae7cfce4b5693"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a9b575ed1193508c0a2226946e5cb4e091528aa7756100355773e86f3c2d9a3"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:1a9f44fac958777c1743c4a239d156c12b85e05c1cc4a040d9b227459bdb19e6"}, - {file = "pydantic_core-2.48.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fad9aabe217d8a62cd1c46afa6b48134da8e5f6eb148bb228fac5886b9a40407"}, - {file = "pydantic_core-2.48.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ddea29d75d8c65dc6c2b3e8b9a178eb27e0093ded129f5bdf0b7c6654dbf90e1"}, - {file = "pydantic_core-2.48.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:85c1006a8ae5b5ddfd908cdc98b01db9c0eb3a012c116e9054f66d4f9a6dd210"}, - {file = "pydantic_core-2.48.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:94ed5b7ac4522a462aa44a3e830abb2ea18c1c6fc633d9fef222f63ae12d610a"}, - {file = "pydantic_core-2.48.0-cp313-cp313-win32.whl", hash = "sha256:52a860e1f01bced6685d016881338fc74d5beb491aed35a08f64d929d7c894a0"}, - {file = "pydantic_core-2.48.0-cp313-cp313-win_amd64.whl", hash = "sha256:29f6cf2dfff9206a949eeba1de29578e2199c078d3f2e25f47253e15da44fd4e"}, - {file = "pydantic_core-2.48.0-cp313-cp313-win_arm64.whl", hash = "sha256:41eaed1185feacdd39cfa02fbb46ea3e15493cedd70a901c6a1aa437cf1afec4"}, - {file = "pydantic_core-2.48.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:34fdd956d95ea79cbd58dd29afc7ebf658d32ec20b738a5a9110eac3dcc42a66"}, - {file = "pydantic_core-2.48.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:aead7fc064c84c0a57f57ea373b971d33ae0df05b1866c669d7b71c0083b00f2"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e94dc01965be40ccf1fa71833fd85d7df588a49b64a929b2d5178208a895ddb"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9244be8b15f2edd25e8724b91f0ef406a73fef29a3ba9cdf3a19acc04c135613"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e615698372a8c93f91fae46ee23dc93c4cc52ba97662d2fa31829e63892d744"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f2dccc4c7b4c50854ffe2786c192a7a4521d851fd42dcb3ca697bcebe0dff6e"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6643b3df515bb47c8e46c07f4530cdc7f52ffcc1845d437011dbb9198200148a"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:612c4541a52fc9173df90d62cbd931a2c2f87b9f2eebe5e71b2f2d89410ee6b6"}, - {file = "pydantic_core-2.48.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbb4d9f81063600c13b86a45b9413a2c4148ddfe534108a57dad47c72377f27"}, - {file = "pydantic_core-2.48.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:43bec89e5624b0a3b9f765cb7c2f62959547362c48d82471f4da34af00612ac7"}, - {file = "pydantic_core-2.48.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9906053654da2b5270ffe3699868b5bb3f39b4cb74b085cf6cbbde329094102c"}, - {file = "pydantic_core-2.48.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4dfc6b534178202a3e43e88d73fdf5c85c151df8e920c68ea9d4890fd212cada"}, - {file = "pydantic_core-2.48.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:4fc45a49334c54541cbc97bf416d9300b4b1d3b2840dfb079b1123dc3f9ef5a6"}, - {file = "pydantic_core-2.48.0-cp314-cp314-win32.whl", hash = "sha256:def7b0c70a95a9348fcfbedf7dba9c97328b422c38ee894b8b0bb48663e408cd"}, - {file = "pydantic_core-2.48.0-cp314-cp314-win_amd64.whl", hash = "sha256:2eed50d5dcd57e319e88e117294815eae1c65d2eef316d7d87eb5398d649833d"}, - {file = "pydantic_core-2.48.0-cp314-cp314-win_arm64.whl", hash = "sha256:69ebfa782ca474ed305227a04d370c9d7f5a7f5039e7802505145d3e8ad8e309"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1aa7dfad60b42bbaa6b7be7e28f386be9b742483bfeaf26d310b466092f271be"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:75df20fe8ddab1153017cd791c965674b509b6fafbdf5aaef2e6793a65715694"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dc59ad81e0e1c3209c2040e24ec99a772155f7dcc62b09829422d4bc5ad3133"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf1ad7d2d3793bd15843180da3f29d1d475661f70788a696c0faac8abc1dffb"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e94566a1c7eb35b9494ad72e3bfce0ef9bff451aa883ce7b73083ac0e05b572f"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:643332ae4b638daaa4130089ea32039dbbc9883cc21c555771cb12a3b10f392a"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c950716666760f5b5bb4c115f1466adb9b6a117d28c4a0489049e258a88d01"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:fb7f8a79ac0c4fbd0849ac2431106659c0e2c3cfbfdd18810f2305a05661398b"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:782e344102a11e25be4f416d9f14f0cc2710f255339ed95b0d1fb026892cca47"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:5619f21f760a7dfa6fd24f7d73b15d47c7327114d43f683791244a9b7e9127c9"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:51ac6763fb9043d00343ffa11c1c46f06d73ce4f3491e64d82729519dec4489f"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c066ec851c4a742f7053e615a25794c30c4304f6ab1f6f8b7eb69e06a23758ec"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-win32.whl", hash = "sha256:9310814fc611a1d40875c8f07cd4dcd156202d0fc2d9210fbf1cb4d130575e0d"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-win_amd64.whl", hash = "sha256:54bdb4c47e063c109722fb723da0a306414700cde573ea59a6f0dd620d434f67"}, - {file = "pydantic_core-2.48.0-cp314-cp314t-win_arm64.whl", hash = "sha256:157e1f7ad13864127d12b5909e2c3b02cc64ae041bd3fa9239ab292aff4ddfd1"}, - {file = "pydantic_core-2.48.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:951d5f7754eb81381eae252af74f6c751b954b1f36e4e7d3b3b1f7a020ce0c6a"}, - {file = "pydantic_core-2.48.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c0f553fb3ace879b900e217285f83ab39bccdd0bf91238670b20b825d801037c"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd33c885e553a5bd83a210b341026817374dc192cf62bd169b969d732a155cd1"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f6b45257e2682fc2ea42d2ffeb1fb92630329a496214c2c438523f9b614cbc3"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc088260cc20d901d5c380e9c881ceb644cdb9c69580674e1c25a38d4417eb5b"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f144e303ad0eaf41fd596a78a7706542eb97ba81c13c8fd51c2af5233b73590"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd51d8521aa76356350f05951e45a9bd05a342e31c5d3ec437bfb5d327ade14d"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:b1f382f0e0ec8623ea4f3f477ab22e67d6ba684e25f49a39a01b121dcf528bf3"}, - {file = "pydantic_core-2.48.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69c17bce5458c97b1c34d2ea2b388de6e27285d18d8b5eb14195d7de5dded37e"}, - {file = "pydantic_core-2.48.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:a9e540e17c61ecadc2825cfe097efe1e618f67613c331ff6e35a30f7d0d71180"}, - {file = "pydantic_core-2.48.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:729e27d5877d3a61f829d43042293ace2b725c1e66f367a187c05d11280dab83"}, - {file = "pydantic_core-2.48.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:83785d4c37e362ab1efdaf4067cd4a2ee1f7cc380ac7a7fad04a9542c697027f"}, - {file = "pydantic_core-2.48.0-cp315-cp315-win32.whl", hash = "sha256:2ac14fb788fc51fb2cd044817bab9c98abd352fe3e4424463da805d18a0478fb"}, - {file = "pydantic_core-2.48.0-cp315-cp315-win_amd64.whl", hash = "sha256:1778e349e1bcaf0b1850d4b9cf0a1ff4edea36bc11b69b560c6ef644e60a9e85"}, - {file = "pydantic_core-2.48.0-cp315-cp315-win_arm64.whl", hash = "sha256:eaa9e2d0ef1bb345d6870f72ab12b2a5522231e38bfaddc0b903b5af00537267"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4e36b7ba6c7713d17d768b21e06763cb6440eaf3798cec4ac22e4c685c73d6cb"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bb833596eced78da91d24e7d84150085aaca21825e2267f098c972f4a790a1a6"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de1c807c15fd6403ef651c5ae6324a640d92cec6b39a5af58c57f4e031f05859"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2216aae7b54dc12c8f6228dc2ca92dc758f4f8906f2fb5565063e2499361b910"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837426357feb1f52c42b24e611728b394a6f4ae4ac7c461ed55be201bcbc811f"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb8807da40e346c5ae65846e721d96ef84776e103b5ad6923a95843ba0e17eac"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55bb2a177d33bbf22e441f34928183230018518578360cd782bbe17e3a877b5e"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:d77765d9c9bc37edc2ffb98e724a3e57b08932bef5407ba04f00bc59854af113"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d058233dba56eb3de2eae96cb0acacc521417ed34de72507c9360202cef32ace"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:95c0e79985e0c1af0f769384f369412652f06e2e3d5a679223d15820f4881107"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:2384954580b564a3ae0ffc8bf37c794c75c22dbb3cac0a9f941b9ad1de3e4e03"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:c40ba3924215e6c1616e897dc4342942db38c0f21a3bf50d7f6adfe3d9de344c"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-win32.whl", hash = "sha256:e88a758fc73c5e5e031c33b11f114e112ec0d5b5fa537db7d8d5e8c44c3c1841"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1992edde312b15554d048176de39d7dbd59b3d637a96d71457e5251d089f7662"}, - {file = "pydantic_core-2.48.0-cp315-cp315t-win_arm64.whl", hash = "sha256:1514098b2d6d91e94840b90fa801e717893255c7468061faf38728ac27063d55"}, - {file = "pydantic_core-2.48.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:40a035f7a82ffaccd9599617bb0470425328be77e7937efe5e050d5b692a75e3"}, - {file = "pydantic_core-2.48.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:10dfc23a238a3385c9a2c0a63ff7ca5ec42adb52c2c1c2f56b4c3d888fa37307"}, - {file = "pydantic_core-2.48.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddfb1090d09ae7fd9e377ae570de535792d7cec088fb7d283ea6ac67fc1f24f0"}, - {file = "pydantic_core-2.48.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062289bfd4f99f40bab32f725fdcdad9244119c06d41d62da019a3f4827cdc2d"}, - {file = "pydantic_core-2.48.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:36196b58c314af63cb1350f57ebfbed122678f2727e8ad3f41015b9d57e5dc94"}, - {file = "pydantic_core-2.48.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:83a9509271a3a1314fc2a928d98b3b9910250e42a9a409b852454a4c6fee4710"}, - {file = "pydantic_core-2.48.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f751b64b99cbb1596ae6e9a044610a1dd716f07f65ef4dfcb8693458d66b14"}, - {file = "pydantic_core-2.48.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f1d556f02ae80aab0626639eb1aed304e7d37e52791839b39d7c7ae0acfbfbd"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:44ad0cd44c9051b0ba47aee87a4a70ee2339f5b4490428a021cee3627bd9bf8b"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ed47cb6383542456456bd0166df05cfaa0139c363635cbc74a69976d37218367"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b6a6d9fa9d52b2d2097ca4e5a40759b76e0021c0d68b36863d0538c95e1090"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e022eb9d12dd5b1b1a3a053bcb71e1bb7ef762965c4a18acca8bdedc6f5379bd"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d7be132e08826f4d8da5c0395c4aa6b4f6a13bd9f75f1615711c9e75d5dea019"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:bc86b6a4a7f1f6f6e51841da08997cc37186a701ea0788810ef61e3c79fb6d29"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:93072e2104eec33679477b6ba923e90fde59cedf2a525dad869b685c640b44f0"}, - {file = "pydantic_core-2.48.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:19b9d3bb1a2ef6c692729e47f015327d6f9d2387edcc36fa2f57851e98166cb7"}, - {file = "pydantic_core-2.48.0.tar.gz", hash = "sha256:8714f70dafdffea0a5596cc88eddbdc71f5856563947970dcbd0f1ced61ed05f"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" + {file = "pydantic_core-2.49.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68c53d957497e4001e4104c5fb64e90239461b128de0d5151bb8bc8053d70b4a"}, + {file = "pydantic_core-2.49.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad272f556b10a7eb68b6240ea5777f59d89d59b780987459225ab8650681b373"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2aa0c16c7cd70865e3e13ccb5b48a73a847c98ccf2804692d812bb85a9fd3832"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6dac978538b8c4f61680da02fe11da8d04c3629eb16ddb49da797636256c9403"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6b36f37436093e2ad2471118e9fa13055fc4dcd452ed4ced148c8d73684b9"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc3d0cf03cee06dcb33a0ed73bb706d15c799684510dd1f8d4ce86cc03fbda91"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2005d526103ea0057f5e51d0b9c469234e890d8d8a76c4ec474696e4cab2449a"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d3eca18e218b10bb96d8d5cb441968ed4ccbcd83c89076f3c86cd5545e0b12ab"}, + {file = "pydantic_core-2.49.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4db357431b1949628cd7dd326b92a5fcea1d693fa96848b3d2272d83b9f136e6"}, + {file = "pydantic_core-2.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7392a37a455f71e64a7cf70da1df89ece89f46c9163c4f7b8988e4de4012fb7f"}, + {file = "pydantic_core-2.49.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:a4af8feff8bf9970b9cb7be1fd1613dcedf3a49233de37dee9198c962d30f661"}, + {file = "pydantic_core-2.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:106a0a7b51b293662ff5380255bf8f4e215d7d6afa8e6588f3b9511621bf6b4f"}, + {file = "pydantic_core-2.49.0-cp310-cp310-win32.whl", hash = "sha256:458986e1d8ba85ba4b5c369c881a1f49ae9cc5b0c00231e13736b0c6557c1a3d"}, + {file = "pydantic_core-2.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:592827666d336312d5fd3976b42be19501c51db69c420660e283b5d31aeb9733"}, + {file = "pydantic_core-2.49.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c6a30b314e8d68a29c7c612ed91ea0f83c8b84f2d83750a7584d325ecca6bda3"}, + {file = "pydantic_core-2.49.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:39adbfec1383df3ac7d8ae0df72e91b0fbf503f9786754ff8d0cb6cae0ae523c"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9c34cc98ce7776d4a1468dd281eb39a3af6fbb75faac5d69bd4c0ef22a2621"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7e577bc4d08eb25c06f32f19726e3b2bbbb01a719873efbd4514ef7dc372729"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39b16867c2d305bb6997ecbaa09a6edec79be1f7dfa51e12778abd8d32275911"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b26764004a7bd6450769c47a470b0ce8984146baae7d0063ca70bfdbbb0e56dc"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc3472058534dec5fe414cd7de626ecbc20330effe05de66cbfe36c6aaeb0d78"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:890b0dabdac056908c45b79e07e6dfb51c1dbd7e769d7a4878cdfaf7d1e9ec76"}, + {file = "pydantic_core-2.49.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:477cdaf34c8ff40a17183397ea16be355c89b58a6939128ede251d75581ccde2"}, + {file = "pydantic_core-2.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:813c1243fd93ee720edbfe771d9bf0cc5bc6d9bddc0acafb4c0085796471c2a4"}, + {file = "pydantic_core-2.49.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:aec1570d4986f4ab000b0adac485c4b6bf5520b12305bae3defbe6a5e4816d55"}, + {file = "pydantic_core-2.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a3e9b4167b5d7a355863846494b046af5071cbfc4f30c8554634b2796eb163f"}, + {file = "pydantic_core-2.49.0-cp311-cp311-win32.whl", hash = "sha256:3a147c3f4ff17f6ebd1120dab11b9bb7db318303dfd8d1a853fe167694f6bc9e"}, + {file = "pydantic_core-2.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:dc35dfc110dd744e709824ea56453ca2ff8e4f8175c950485d4916f19689b4c6"}, + {file = "pydantic_core-2.49.0-cp311-cp311-win_arm64.whl", hash = "sha256:70507b191705d0ef220f85c915395109b439b8c9802a5e97645ac0e44288dc81"}, + {file = "pydantic_core-2.49.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:53794f62d97aa49b6b86b4114e06ae0ea0f4cdc72c29dfa76180e6ffa95a50f0"}, + {file = "pydantic_core-2.49.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f63e2bae46f4ba6353869c6441d81c027fce74a2edac1b20bfac2c0098581a27"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3928d009bdedbfe7eff17f1ec75871612b91786106c529137ea58d70770d02cf"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0cf7c39364028404ef953adaa126aaa037a20bdbb4c073c7f3b88814c30a34"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8846c1f9c02ec0b73ccaaa1fccecf98f323f2b230a7c6cb19c3917f35d38c4b7"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d1433f3d022d1f946bda84bf50b83d9f0f77599e6bfe96c53b384a760b116fd"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a807d09a516f93614bbc8e83401afdbe7325373d36ec54727f3f5147a57dd52"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:7f2ff0f42e14b2694d99743dd8c808faa9fad989236e3b6bb6f0e19ab036d9b0"}, + {file = "pydantic_core-2.49.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdd922cbe6ba23c3c827f75ca692f881885009c83c7d0fc1fe0520d80ca36b33"}, + {file = "pydantic_core-2.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:119505798feabc587642e03579a9acff7da6ef6816a70ef2de50960037b4b0c8"}, + {file = "pydantic_core-2.49.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:45137bb8182ae8b8631550d54d82d654db7e8bcbff2a8ebce5e56c85e25fc7ab"}, + {file = "pydantic_core-2.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c8b4390b0e79235acb8c4d2337e76efdf00fd17936b19227726ec63f23840acb"}, + {file = "pydantic_core-2.49.0-cp312-cp312-win32.whl", hash = "sha256:dcdc5148510e5ada01ce89d52f86cd50a04dfcdc7f689d2607e72cbfe307e594"}, + {file = "pydantic_core-2.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ab1f5555c083b9bc6aae809beb00aef77e81a10dbbd16a6da9ad239620468cd"}, + {file = "pydantic_core-2.49.0-cp312-cp312-win_arm64.whl", hash = "sha256:1af1d427be9a0bc55fd20015a1f61ffb3bcb7ddec83cae03f27b1a833893688c"}, + {file = "pydantic_core-2.49.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4e460a2709fc62802409c2ace7935d76087f317f907884da660e580a9bcda06"}, + {file = "pydantic_core-2.49.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c5e9b40518489f5bf6bf8e2cb4f2ea7ae935fb063c4d18d2593d52db136ed7b1"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52984e1335adb925c6089a44d167668a1f8cc6bbbca25657936e51922c0d07bc"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60c30f0977f6bf6d74c81f5c797f862724cf59ad5d7ff37f3eab9595994f6261"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a4b87881bcdde33caa8d1900cef4166ea34575f1926c390b8f486ce911cba82"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062c2a4b2de23bfa10418fafe63d8873c9f95780dcd5bf6ae66537b44e40f3ed"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60550432855bc323ce9aa813e14e0bf2b806de59727aab850a1c83c0fd7dc0c1"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0bb4a60b0abdb41c237e28fd8c86e223d2994143ed863d0168c0b03b759d2620"}, + {file = "pydantic_core-2.49.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c33ecdec5779328131e575ec133ea4a134370058c9a8f6b7c80761c5f992389b"}, + {file = "pydantic_core-2.49.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2b72dc8f8dba268bcb040c2cb715e96de9a69299d1d0b60ca4e12517f10b8834"}, + {file = "pydantic_core-2.49.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:64804fc948c723dbf763a40d0747e7a73807667148831a836e5b856259247837"}, + {file = "pydantic_core-2.49.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4a801a8410e16e22a231aef577127fcf02c85ee237a1b73cb0790173be700b3e"}, + {file = "pydantic_core-2.49.0-cp313-cp313-win32.whl", hash = "sha256:3cdd59f86a24c7ae250ecb02d94f2ce764ba32b139c24029c23637fcb7670f36"}, + {file = "pydantic_core-2.49.0-cp313-cp313-win_amd64.whl", hash = "sha256:92503a642b207cf9d085f4f79f9b66c972208262bb1b12bca95c3a0c6deddea3"}, + {file = "pydantic_core-2.49.0-cp313-cp313-win_arm64.whl", hash = "sha256:f45ddfdf69dac79736c3365f4b73a5f849f488c427535bc426d685a6101c68ff"}, + {file = "pydantic_core-2.49.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:96a548e9243889fec7622abb7e10d362a33f755ffff5092874f3dc0e8e05cc10"}, + {file = "pydantic_core-2.49.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7cab96cc03d0bdb31b4d1130f65004f496558b267073df3fe65d4981276819a3"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa610f5abbe154fb35eca0b060351b9f8b6e5e7de63a0ab8a3f14f9304c01f3"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c3f75d3b7a818d57ae45693358508d1d0e809ac2dae05bf5bbd3c687bc75110"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea008d686f2c3b0fb9a306a4ea81201344cd4e1a93026017d469b1211721dd46"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db9453ac96ef4e0e9da1ce1b62e5595af1ad2d55bd1dcdfef595d0515d2d3995"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82f8bd51e8d3baebf622b52469bdc316c6c1c6e48102d33c76611f37f5924582"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:7e912019d9262611ea419a0e440e552b5d41f3671d2d88b96a99ef67b6d3ad71"}, + {file = "pydantic_core-2.49.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:98e0f4f424fed81b72345b8a249ab8a6bf40f2faf5caa96b8bede344a755ec61"}, + {file = "pydantic_core-2.49.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:13b5a234f4f0332f509396c8127c92b90674e4f2636d9d67a7be1a8537e2439c"}, + {file = "pydantic_core-2.49.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:0014dd75b23f336314956bed232bda1b0138be774bb66746f9c01f7a651d6532"}, + {file = "pydantic_core-2.49.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6aef64b4b948db5c243d184a92326a4666b0467d9f6503f68d048b23ba18a928"}, + {file = "pydantic_core-2.49.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:62535ce2294441c986da1aeb743f7255ac99f3e0a3119bd8c44b36406d9f3721"}, + {file = "pydantic_core-2.49.0-cp314-cp314-win32.whl", hash = "sha256:db47b8fa2f99ade35e3d3d05ca62245e7431c9c8528578aaf3a937602b87a314"}, + {file = "pydantic_core-2.49.0-cp314-cp314-win_amd64.whl", hash = "sha256:46836736bca8a7ebab55d0ea76cd04e89bae0ea3d6dcfdedcd1bb1b1244a4239"}, + {file = "pydantic_core-2.49.0-cp314-cp314-win_arm64.whl", hash = "sha256:bbc789b2b36baba17377ab63f3dcbbaa8e3a72e5f2963ee24ff4aeb7e9a9bb91"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d629945094190237ec0b3a6d7656ec13b6d2711ac62998005adf5a4ebfe7b836"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4fe53ec1ed34aa8e3db349305c38202711ef3009ae5ee9956e69032aeab246d5"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e9956faf9840dbdce5a88c2a68fd28c3cdb16eac41ac5f85831291762021805"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40ca7e5bddc63dea88aa3f9778d75dffe725d79d167398dde33f41f10034a0eb"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801d35337e6f732db4598e5a7cccb235be6bbac6ec03734e6bd731412e2fbbc1"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f45d6552e58e13015105bea5b8c6d012ac547e1b3c5d653b094daacc8c7de1d6"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a716ed58512bd33bd9736e245db15acfec542e54d10623b9faa42632404efcc8"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ab7a65a669a03400507b4e573119ba3b26e156797526e71669476de8605bbe1d"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab51a50fbe2bd45c649dc0e8624fbef5cb9553e046ad1cf7fb658b6c8b0273de"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:813081b90803d858f6917f46b16fc4b47de679a2e76058570a4a46344305b650"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:e2b3e73c7a6cf2ed5ac8ab57aece1feff46c709c60cc88602bebd29f733ad340"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ee165057e76dc133b18726f2283036d3cccdbe70613b940d73c729d51b011b81"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-win32.whl", hash = "sha256:7d75adfd02d96906d33ba637e1d873078d46a531ac4efe2e373220c4505c8caf"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ce79aaa77a6fc6f2784f3c87933aca2e61b6cec5a89d91c730fc051379f181a5"}, + {file = "pydantic_core-2.49.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dbf8be064709ef364b68acc7b753dbe044ca01d2875afdad50222ae3ce7bc374"}, + {file = "pydantic_core-2.49.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1acfae18c0fc6855e4586d2b7cc10393e921f883c56c1ea4eb75115a7b344552"}, + {file = "pydantic_core-2.49.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:adae5a2f5f7c5d6307c360cc1f1cc3b1d91961b23c25c44dee5066ea039bddfc"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6ce996aeae20af75fb1f0b69201bf0fc2936783533e0f1f171ab7cda6e5ed14"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:19310addacce11fd11f181eda18f2f190cdce0150b0808399756e1a530430c17"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51a7e44c0fc802de672cd34bc3fa283bdf6a5c0e975413d124181573e3b9a28b"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:376340e6682b4d9ea85c72636014c9487826f55fc93228aa308920e6db440a4e"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7123c611f3933dc66a8e587c6c255acdaac20aeb1b2505ac01fe59c1f9b7b83e"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:6165e75e29c361b8d4077a19c3a9a98f93d019522f952c13a6c8ff680a40fa0b"}, + {file = "pydantic_core-2.49.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b9ade32c16df4ad98c72fa08a6558a06534f00d3dabffc2abec8fe6d3c3e242"}, + {file = "pydantic_core-2.49.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:5bf86181aab670eb9c3b1a3e84342bc934134b50b9c08f39aa56e66fd3c4f6d2"}, + {file = "pydantic_core-2.49.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:73407e73aca3383df974bb69a02ae426371f477cee0bc868361014fc5a86dd0b"}, + {file = "pydantic_core-2.49.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:58cd25e42316673b5614d6bb0ccee0ec15d9dc60182ab967807dc0e222578ce2"}, + {file = "pydantic_core-2.49.0-cp315-cp315-win32.whl", hash = "sha256:2f9df5909999efe8f84aa28ef3249b12c9801911a8a7be8f386792220c831af4"}, + {file = "pydantic_core-2.49.0-cp315-cp315-win_amd64.whl", hash = "sha256:24ce45e6967bb2ac68f1f1c199dec05399d32bc826342f455035afcbda5ab3f5"}, + {file = "pydantic_core-2.49.0-cp315-cp315-win_arm64.whl", hash = "sha256:531d351eaaece0a6bcc535d80bdbaf9d0db78ad4752ff737216c70dfea7887e5"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:b1cda2c0c0c646d36745425eff94f78ef3df58489c78316181a6caa8cdfd9cc2"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:a5de408dc8b8da86665d402746c84f8258874410c11fead98f4ab6a74db29b96"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8507908c84da92eacb366586a455080a237003497d19b0738c4d5b187a3cbb"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04106a4dfb7c40a0a61834c25f43f121fdb494978657015c9e3847124bd5813a"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69bdb8c9ad333b29118f1e85f79c476d739c1ccd14d9d0042d6b4b710dfd2698"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35473d3949d065d78c0c039de13ea60bbf6b9fc9d274f62d14500544bbf7374f"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3faf66e8ff6c3c86cc5bfab0b61541a389b30540b5e06aa62c0562d3403be098"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:321c2a9f7c99afb97773fdf003df9a54569b17c2d067bb04109571c307a9c599"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ff90e1f4162377af38b0381c7f95353228206e76aa8c3eabf9945f1409461f"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:80c23a141ff99bfe21749f668784ecacf0e0347740bb541bbbe03523450edcd7"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:bdc94de8d11ebeb704d6696641164a8ad4640616f407569d41330eb9390408bd"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:2b2af00c6d1736f5a18e812252fd48a756814424b519d0ed876feb253ea2cdc4"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-win32.whl", hash = "sha256:e7a6af029d80758314e9363a36652944002faff7f1cf6c5add11f5ab15ef3170"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-win_amd64.whl", hash = "sha256:38f28fe80d8b0d193c83b4f93f1e2a70ce8bbc1e107ccff43853d6eecfe1c683"}, + {file = "pydantic_core-2.49.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4d8a90e645670ac9e2e59a9188b3c30b4aff56229535050c5b355cbbe7711e49"}, + {file = "pydantic_core-2.49.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:0b93de748aa4a13ec72384ecca7e9fa52e2983b154b5f878621097bb25ac71ec"}, + {file = "pydantic_core-2.49.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:a1bc9d4ae658b3309e354a6cf70e300ca69d680f05235ec8945f4e2d8c05e246"}, + {file = "pydantic_core-2.49.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a8c26d1a86d4f078d363e6b97608db340deb6336dd2bbbf45b8f0f88c1db4d1"}, + {file = "pydantic_core-2.49.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd7218d785928c2c57b5f4c3682745f106b4192b7647372c1e6d1109d309c81"}, + {file = "pydantic_core-2.49.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:59c1594065e4a95a055c74e659e81d098ab028e439fdcd1ed5d4d3d97a3472b9"}, + {file = "pydantic_core-2.49.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b0e0c56cb113441a93ce198cf8b515edb5f670905cbe66f35c26d78bd9bb30a"}, + {file = "pydantic_core-2.49.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c658588d0f4d59a668e8f0e854672a528a92018ad379b40886723a429ca9273"}, + {file = "pydantic_core-2.49.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:327648d97e9d64e1a4a9be16e882715fa6fb2fe83c405f577018beedb0e699d5"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:68639bdfc4475b1497debc0f70f01c04b2544730a819ebbde6113172e0a1a13e"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:cbe55c9da4521be16f79c91618d7c3f9471d9302d6495d6f52cf97c36d054f49"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5597c5695fbc75cf5d624b7600eebfb0e1f2280e4e7a1aa3ecc2636ce5156d64"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e2d59dc834612f4846d823d29d90d9de5527efdb46b51fa661e9dcfe853e8211"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f391937c761ae69e5416b8900a8441b78baa334349e068be9d72ebb3006d62aa"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3c6cba4051e8b473151e2f745e70d2f5e9e24389c38be835d5dca90a5ff31389"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4052ef252d9225562f73c6235a8606ea3735f7f24a07c6dc08af3567eb2031b"}, + {file = "pydantic_core-2.49.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a7826bda0f21625499db4c9b46a60025f2c83f7b6139c777a21e9c2c5e1a67c5"}, + {file = "pydantic_core-2.49.0.tar.gz", hash = "sha256:67440399d87a2c160ddff9dc013d12812785cfde18b648a295d57043f034a9fb"}, +] + +[package.dependencies] +typing-extensions = ">=4.16.0" [[package]] name = "pydantic-settings" @@ -6449,141 +6466,141 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" -version = "2026.9.3" +version = "2026.9.10" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" files = [ - {file = "regex-2026.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d416ed4058ea38f69884f3db523b0414c0094d262dde63694aa254e32a433e6"}, - {file = "regex-2026.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:107319f437fc382e1e445d0abf8923db07f76e5fb3245ac5b09604e5dc8d4f63"}, - {file = "regex-2026.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05e9f7d16b42686fb38b1702071a7359469ba89e9d516e2ba5228e077dcac524"}, - {file = "regex-2026.9.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39d8d5490ba4b8f26ef2aef72b775b1e7fc1a5ebaf28d11e637eb27b0b6a3048"}, - {file = "regex-2026.9.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4381151a29a7b9307842ff444609c4b9a402775fbe9afb3ec3e34ec0396bbae"}, - {file = "regex-2026.9.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0efb99024ad5ba9198ffa816156b3319c3164b5a8d1e35940d92fdc9158b8d9f"}, - {file = "regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ab7ad8b557d1f21e8b4a97a2c1a2ac82cf10c92d6a875c90f038ca9ed85dc5b"}, - {file = "regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2a36e654181ecb996241bae256d28f26b03d019d6ef65c61dcf44396ab07c548"}, - {file = "regex-2026.9.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d36e9097a1bc6eebe8858216ccd2326f561662de4e50c27533452491c1cf1685"}, - {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c407afee6ea4caa313e815814e76d8447fcd32f81e3331d7937c2d33ab80c1b"}, - {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:82c27460ff2ea683159a204db2f9ce0f9549dec7070aa0809fbc36226bac1816"}, - {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5e49096c90364317897b8d9fe97e2f86063dc0fc0bcd352017ea6fdd703ff25d"}, - {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:11f9b104fa7b23f736b50fe1980a1422eb4641865e82614a25413deabccae575"}, - {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:92e06a5ecabd6e8352da58b9201ae241a1e7382c1419defa9d2f64ad4ea6abf2"}, - {file = "regex-2026.9.3-cp310-cp310-win32.whl", hash = "sha256:6ff5a98456194621f757d6226f88035c00ebbeab0a9095dc9b5a9ec5cc94b50b"}, - {file = "regex-2026.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:14f8969a92ed847f78e8eed81c6a0384a9e8058a56eb659f83ddb59879c2e4e3"}, - {file = "regex-2026.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:32a32d2664e602b20e4b9c11234cb32f2e985323e453b2b396ea4ae686d82052"}, - {file = "regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65"}, - {file = "regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4"}, - {file = "regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e"}, - {file = "regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328"}, - {file = "regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71"}, - {file = "regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e"}, - {file = "regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01"}, - {file = "regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141"}, - {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc"}, - {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b"}, - {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124"}, - {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f"}, - {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce"}, - {file = "regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420"}, - {file = "regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc"}, - {file = "regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8"}, - {file = "regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77"}, - {file = "regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596"}, - {file = "regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e"}, - {file = "regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687"}, - {file = "regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753"}, - {file = "regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151"}, - {file = "regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce"}, - {file = "regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c"}, - {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32"}, - {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14"}, - {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275"}, - {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089"}, - {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b"}, - {file = "regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3"}, - {file = "regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36"}, - {file = "regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136"}, - {file = "regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280"}, - {file = "regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b"}, - {file = "regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9"}, - {file = "regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08"}, - {file = "regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211"}, - {file = "regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c"}, - {file = "regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a"}, - {file = "regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28"}, - {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e"}, - {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90"}, - {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b"}, - {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9"}, - {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec"}, - {file = "regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20"}, - {file = "regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd"}, - {file = "regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f"}, - {file = "regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886"}, - {file = "regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6"}, - {file = "regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7"}, - {file = "regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944"}, - {file = "regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b"}, - {file = "regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8"}, - {file = "regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec"}, - {file = "regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69"}, - {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7"}, - {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c"}, - {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc"}, - {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69"}, - {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380"}, - {file = "regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd"}, - {file = "regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846"}, - {file = "regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16"}, - {file = "regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16"}, - {file = "regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d"}, - {file = "regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595"}, - {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17"}, - {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970"}, - {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3"}, - {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5"}, - {file = "regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181"}, - {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef"}, - {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae"}, - {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88"}, - {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177"}, - {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e"}, - {file = "regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f"}, - {file = "regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16"}, - {file = "regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5"}, - {file = "regex-2026.9.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9"}, - {file = "regex-2026.9.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361"}, - {file = "regex-2026.9.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab"}, - {file = "regex-2026.9.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5"}, - {file = "regex-2026.9.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be"}, - {file = "regex-2026.9.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e"}, - {file = "regex-2026.9.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb"}, - {file = "regex-2026.9.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f"}, - {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c"}, - {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27"}, - {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7"}, - {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253"}, - {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20"}, - {file = "regex-2026.9.3-cp315-cp315-win32.whl", hash = "sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae"}, - {file = "regex-2026.9.3-cp315-cp315-win_amd64.whl", hash = "sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57"}, - {file = "regex-2026.9.3-cp315-cp315-win_arm64.whl", hash = "sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950"}, - {file = "regex-2026.9.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4"}, - {file = "regex-2026.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff"}, - {file = "regex-2026.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76"}, - {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96"}, - {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35"}, - {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa"}, - {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23"}, - {file = "regex-2026.9.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec"}, - {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de"}, - {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f"}, - {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544"}, - {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092"}, - {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69"}, - {file = "regex-2026.9.3-cp315-cp315t-win32.whl", hash = "sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a"}, - {file = "regex-2026.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803"}, - {file = "regex-2026.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2"}, - {file = "regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db"}, + {file = "regex-2026.9.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7dcad477c49c4c626a6c4fcd71b39a971aa217060cc40a6569fd24edcc0fa509"}, + {file = "regex-2026.9.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d414c411c06fe0009eac33488fb1591c66b5c2673e342e452e7bb2fe63da8194"}, + {file = "regex-2026.9.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f2f43bf4e47ff7ce9e585558706d698c6204d0f80bf2207766382ed817c8e9f4"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d12a625a37c89c2b09303402a06942f55f071b95a7916a49c17034c3d47cd5"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef4ce69ff97fbb44b46751cfea5e859ad0b66d1a50abf34954f0645f51e81671"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:044bd4639b6bb409ec9e5d8b7accd57e02b4c4a4e2eafde916f8ae8006b3e40b"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d95d7d9538b5b726dd6fcd7b6117a71e6565202f6d64f5845fb4d8f203f533"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faa911fbbcf8ac90bda0e0657d60768e3390954ef0588211d63a22add1cb1cd1"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ecb2e7acb18f8cc4a67f0ad986c0af291ea4dd385d0614ba9bc09d7f8bbb478c"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4971776b4f2bd7fd9a83eceb2cb2592cbe2924f639fe8045e6a9de5ba4bfcf25"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f8bdec659a8fa7af51a32b224b3b7c02bc415d54ffd35187b1d224176b17d607"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3540734dbe241ebb3b87d5713781f6749a3e4d45480f506aa5fb5cbb0c37d249"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87f5f75c109f08f5c602d68e1af54cead8165189c727b6ac946b30b9833a3ba4"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88b02aa8d0ec9b6189fe933d425775882271c23700ac11fd26d1779b0f56fde3"}, + {file = "regex-2026.9.10-cp310-cp310-win32.whl", hash = "sha256:13c52fc377792675f604a207a2ae5958c080f6854f7698d40d9ff034d95b1e76"}, + {file = "regex-2026.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:e6b99181d184d0f5c7b36b8d12b94d1e9499cce6246594331f9edc5d2ea9fceb"}, + {file = "regex-2026.9.10-cp310-cp310-win_arm64.whl", hash = "sha256:abbfc1c33bf8efddcc43844aba61e036d74a918680dc3ce8ce2538b004eda0f9"}, + {file = "regex-2026.9.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:debc629e98b95abaea1cf3057ca296151f348c697c9b8a59d18013adb302c0dd"}, + {file = "regex-2026.9.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d772586951d7d6a5d162d48f414065e483b1c81ab38fd8ed97c78b05883421a"}, + {file = "regex-2026.9.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b43456de605c8ee77eb75f07bc1ee44ba27f9cee22207deb77d495e954b7d953"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0a8b4928823bc8b217a1ab7bf3d90598909dec9a70fbbfe9a52cc4eca55990"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b91c37551bf39d75116c02b146956f65b9aa0337a4a652f4ae186983789d4001"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b9ba3b2765cdfe18f0f561a69f78a69701f2896654a81c711108d35d14e5099"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0acee94b480dd853e39434aa9a575f95385b1b4b8fa3feae56db363ca5cad782"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a41693eb3fc4b92e6127d113813c6c395237f7edd3224abf67609af48c690d11"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6888065672b341e5246f391ec16dc258a29218ac784172fd67c30d941544755b"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8c07021a4faa3f092869adbd1f35cdc7a592276c807aeebc3ceb8ff1a638f0b4"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:58c01f7b81079cf0817ba831ff4d9eff5d28be4a3ac76c353e6f09bd63f4c386"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f0e2e5d23448b660d60a6ed85c46cc03b4b48bd276b8f4041d4a5fe2a4a0626b"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94d096369b7cd96d15343fef5257fe39eff9d0e8758b92a0e15e358b92cdb2fc"}, + {file = "regex-2026.9.10-cp311-cp311-win32.whl", hash = "sha256:7f8f10015866608fe4c043cec2e4fe4c39a94bb50e45091de4cdf4004b9ae4b0"}, + {file = "regex-2026.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ce7c118cb102975f974585688357a717ffbf9dddd64ab0bb1bc93eb5b367cf95"}, + {file = "regex-2026.9.10-cp311-cp311-win_arm64.whl", hash = "sha256:1e954e246466d5a1a78f563ce8364b5d7cb19e7adb0ccdec8f9c9610083187bc"}, + {file = "regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db"}, + {file = "regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a"}, + {file = "regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1"}, + {file = "regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348"}, + {file = "regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86"}, + {file = "regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae"}, + {file = "regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca"}, + {file = "regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da"}, + {file = "regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643"}, + {file = "regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5"}, + {file = "regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4"}, + {file = "regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7"}, + {file = "regex-2026.9.10-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd6bd89b9fc06018d35851cab0240adb7dd84d51941b19f6574ac90cd54e3ae5"}, + {file = "regex-2026.9.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ef4c0a9dfdc90581b90b1b95a8c3d1557f8ff8f5a2a53536d26314de699d1468"}, + {file = "regex-2026.9.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14caa05ce39ec70437af5aac8814c50ee6628f4a90353871c059692f448a164f"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3264132d576847ab5f88bb83e7debe67854bf165b3ea613bd467312b6099536a"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cef9f3d14796500ea834c41dbe688f1f6b23c7024dc23e8a794d7ebaf5d71d0"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d278ad30ec83b6b9202685b0f80b741a51ea3ca7f0595ebda96e7628b6398876"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb018cfe7144585fc83882405906ff84994a2d154afc2509ecc7752c51f864"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fd555fc9abef50c530869690b2daca054c8811a7aff632d11f9a7b2590b2742"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8d5c4518235a2ec1611e57af85fa488d529c1106aacff12adadcedf8687012cd"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:175cf49ce7a994c88b8f15e3cb17cdb66a48ebb2d36de736b8205033db950f89"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b71649169a9fcf30b395ee01047fa7ad6654a4c900ca75b23c04dedcce6a1f8c"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8ba1f78bd4fef2d8f84b894ec28ac3481afe6cc07aaa253ad4717ef7b3fe6bcb"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:217e98ba5fc8908ed8ffd4ebac04753a0c831067cbfb495b9821b94cc61eaa76"}, + {file = "regex-2026.9.10-cp314-cp314-win32.whl", hash = "sha256:b298cdc33c5cc6969ff07f0fba19cc73e0fd8576373c50935feadaca2f6b4405"}, + {file = "regex-2026.9.10-cp314-cp314-win_amd64.whl", hash = "sha256:c32818b28bcd153b25b63038348a9fe9b9fbcddb60df43f204c3ab55eeb57f77"}, + {file = "regex-2026.9.10-cp314-cp314-win_arm64.whl", hash = "sha256:75242f44a3e283106077be4ab717bc535e4701c9d54ad69e195945c22f137a1d"}, + {file = "regex-2026.9.10-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2dd9286093c71afc8f55ef035c5b9d2776641fd72c6535f1febc92d0b0be9666"}, + {file = "regex-2026.9.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:71879292c9c7ac67b1680345b16daba1be937cb027362cfa04e68f65db2dcfdd"}, + {file = "regex-2026.9.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ccd139b2061132e7b265cfb4b4721baeb9f8928b81415304abf1ec7e3181c26"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7327795089ddb44912dce1434e1d7244be2e9fb48fcc2d6782936af7a3062db"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff4d7b14ea19e50c8d9d6d83f45bd9b45cbb624c07ac1fa54db0a019049abed7"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f0407474ffac8e5e89d93ca41d60891e29f0ab8423eb66ff292d850a86a0843"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ad10a135fa0b4e4a462a61d07c6654d7518cfdb5cb8da08f9ff7d61384af1fe"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9fbd2e5d8002dc49a6129fb321ec51c57a025e752ed525ddce0ba9223c4350a7"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4a761ea45f2ad74c575ef5850ea514cef97302a552d3c7c9d1a1a870d4661d6c"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:75aa39d3f4f1650eea84e46b0d8cefe77dd5478c10e3d0aaf0b0f00493475a7a"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f5c629df03adec31ee505dda3c8988f106c9390e4cbd343600036eb8b3d6724f"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3a66e40a1a20de96a2fee00ed67e11012b62d85b277688258677fd19997addb7"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:968c1e33edd9a104d1bf24c8d476c72de7e3839ae7f894b37e9e4f4739fdeeca"}, + {file = "regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873"}, + {file = "regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf"}, + {file = "regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07"}, + {file = "regex-2026.9.10-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6afcad14310f1311d077553ed374b42a5e538f85a8c884b4e38e52de091c8077"}, + {file = "regex-2026.9.10-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3fb4ae8cf83ef4e9addd43b2da31a9f45be816a8036fae8af59c8998b72718e2"}, + {file = "regex-2026.9.10-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f7d4656e17ab736e9415a6442a345bfc97bb8b7dcce47884bb74a37f70f08d0c"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ba3bab0c45079735f55ac61526774de1d84bc4a0333cc554e1a4ab74913924"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff6b3267318661dfddf6b3628663e00e5946bd0a5c8fa678537a1401f0388f91"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030fa9e23624e39b3b94e46b90a5abd1a1678eb2f58fcdd3fd6c27526bf91c7e"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fbc8314436353e097c050e11b01a6c11433579437ed0579730157676ef59e2f"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e6c0b5ec6ddee4032247585dc491b0fa58627745b66a705728703a3f0331231"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bf29611e5376fec8f795879bb5c6153a76c3a292573d173c26784042b01eb840"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8855f08c17895a26fbf5f19ed829722e19b34a96629e49a43c92974924026b"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94c5ce3bc41d226b4eb89ca3f842b2e28c031487fb1f34eb2153d98235831325"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:9ce239acb15843ab03976626af810a4424b0409689ec2bbc52088ab5479ab487"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c22df8dd6373bbe3898e77429ffc85594300e39d752fd0e68a31e59d37899376"}, + {file = "regex-2026.9.10-cp315-cp315-win32.whl", hash = "sha256:1aa309ab7ba89a62d6cf70dbd38d4176440bce3c7001ab86256704cf4c18c6eb"}, + {file = "regex-2026.9.10-cp315-cp315-win_amd64.whl", hash = "sha256:58da726d3e766c0b3f5a3997dfaf0275898a1107b8191cdd6b0437fe45fd817d"}, + {file = "regex-2026.9.10-cp315-cp315-win_arm64.whl", hash = "sha256:75f9297b16fcb588a1f8d8a55dabef3c0c20b0c7bac43c87ceaaaf1a825c12f4"}, + {file = "regex-2026.9.10-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1270cdec69248592bbe38a0b263ed58d907b891bd2b93703e225c317e421bda1"}, + {file = "regex-2026.9.10-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:681ed38664b64c6617d3c3c332018d1948c77e139c5ea667c1886efa671e426f"}, + {file = "regex-2026.9.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8e127d9a80cbf1c3276bb465c6d047e8705e97b58c2b8f2f0c0a69c336b44b37"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:990797e765d89a423880052c68b61c31afe701de94a8c060f61c40605ca6c727"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5e4a6e0734a685d13b9685622bb503bdbb2927f8b0df025a5085f0ea067475b"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63bb62cf62217dc38c8a6b2b61b165b0e4eb8fa93b0aba12139251c0986a8fa3"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb76a9c4e07a6a47849726af0ed14c41741a182f097f134a8cf29c1bc0f4dde8"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31e4df2b11d48f61d511019bc1ee9b477055f17c352b68fe72db7a98b14d603c"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:cf377960d2ac37d987394a9dbaa75e91338c41a46d41e1d25e90125e7b3ee2dc"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8fbd9cb30c68c1686b94029b9ef845d5870d3d65baf66cb126b676849b9d72b"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:53e182b6b04d0011909b47d51a2d72d908de07c7b1c7f16b3adda2204d723bc1"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:0aa7589394230e0f0a422ab6b90841ff12c87e855e7aaf75d192a54a5f124548"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1b891f77554bff991804cee24b78b40789f7d5993a24c7907bc7025fd2a70c8d"}, + {file = "regex-2026.9.10-cp315-cp315t-win32.whl", hash = "sha256:5bef622850cf760154719d4e0d74b0a855962432995168e250069899ae12fe8f"}, + {file = "regex-2026.9.10-cp315-cp315t-win_amd64.whl", hash = "sha256:07b45ba5c94b8fcb30cb6c56a11f715c57533a3017964504322ea52690a27b72"}, + {file = "regex-2026.9.10-cp315-cp315t-win_arm64.whl", hash = "sha256:f70b9f0e39c2dba1d9da6bf7ef7c377cad7277f8440e9a69be05ede529ff024c"}, + {file = "regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d"}, ] [[package]] @@ -6639,6 +6656,21 @@ files = [ [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "requests-unixsocket2" +version = "1.0.1" +description = "Use requests to talk HTTP via a UNIX domain socket" +optional = false +python-versions = ">=3.9" +files = [ + {file = "requests_unixsocket2-1.0.1-py3-none-any.whl", hash = "sha256:bdb2ba5bcd9d2f3c1dd1e099e5dba8e1d50b9eeef7985edd72f957da22304b92"}, + {file = "requests_unixsocket2-1.0.1.tar.gz", hash = "sha256:87953038ae42befb6efbdf504d6dda2554a0b0d13b65e42f7319793b5527a303"}, +] + +[package.dependencies] +requests = ">=2.32.3,<3" +urllib3 = ">=2.4.0,<3.0" + [[package]] name = "rich" version = "14.3.4" @@ -8819,4 +8851,4 @@ cffi = ["cffi (>=1.17,<2.0)", "cffi (>=2.0.0b)"] [metadata] lock-version = "2.0" python-versions = ">=3.12, <4" -content-hash = "bea6f436e269885b6a20cba9e71881d67aef8df2b70e68416a71ede7eba71d6b" +content-hash = "47a6be981f7d2f74564186e3c74effcbc27897c02df95a689640586a71303e4c" diff --git a/public/avatars/React-to-Me.jpg b/public/avatars/React-to-Me.jpg new file mode 100644 index 00000000..be5c2d27 Binary files /dev/null and b/public/avatars/React-to-Me.jpg differ diff --git a/pyproject.toml b/pyproject.toml index d7af2f5c..1a29ced6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,11 @@ pydantic = "^2.10.5" pyyaml = "^6.0.2" tavily-python = "^0.5.0" openpyxl = "^3.1.5" +# Vault, for short-lived Postgres credentials. Only imported when a Vault token +# is actually mounted (see util/secrets.py), so a deployment without Vault +# carries the dependency but never loads it. +hvac = "^2.3.0" +requests-unixsocket2 = "^1.0.0" nltk = "^3.9.1" beautifulsoup4 = "^4.12.0" lxml = "^6.1.0" diff --git a/src/agent/graph.py b/src/agent/graph.py index 7716f539..e1d19dc0 100644 --- a/src/agent/graph.py +++ b/src/agent/graph.py @@ -19,8 +19,11 @@ from agent.profiles.base import InputState, OutputState from util.embedding_environment import EmbeddingEnvironment from util.logging import logging +from util.secrets import get_db_uri -LANGGRAPH_DB_URI = f"postgresql://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@postgres:5432/{os.getenv('POSTGRES_LANGGRAPH_DB')}?sslmode=disable" +# Built at call time, not import time: with Vault the password is a short-lived +# credential issued on demand, and a module-level f-string would pin whatever +# was valid when the module was first imported. if not os.getenv("POSTGRES_LANGGRAPH_DB"): logging.warning("POSTGRES_LANGGRAPH_DB undefined; falling back to MemorySaver.") @@ -218,8 +221,11 @@ async def initialize(self) -> dict[str, CompiledStateGraph]: async def create_checkpointer(self) -> BaseCheckpointSaver[str]: if not os.getenv("POSTGRES_LANGGRAPH_DB"): return MemorySaver() + conninfo = get_db_uri(os.getenv("POSTGRES_LANGGRAPH_DB")) + if conninfo is None: + return MemorySaver() self.pool = AsyncConnectionPool( - conninfo=LANGGRAPH_DB_URI, + conninfo=conninfo, max_size=20, open=False, timeout=30, diff --git a/src/util/secrets.py b/src/util/secrets.py index 7fa1d6ab..16a04ef2 100644 --- a/src/util/secrets.py +++ b/src/util/secrets.py @@ -16,13 +16,36 @@ """ import os +import urllib.parse from collections.abc import Iterable from pathlib import Path +from time import sleep + +from util.logging import logging # Where the Docker engine mounts secrets inside a container. Absent outside one, # which is the whole fallback path. DOCKER_SECRETS = Path("/run/secrets") +# Postgres is reached over a unix socket, never TCP: the server is started with +# `--auth-host reject`, so there is no network path to the database at all. +POSTGRES_SOCKET = "/sockets/postgres/" + +# Vault, when deployed, issues short-lived Postgres credentials and writes a +# token for this application to a shared volume. Absent means "no Vault", which +# is the development path. +VAULT_SOCKET = "/sockets/vault/vault.sock" +VAULT_TOKEN_FILE = Path("/tokens/postgres-app/token") +VAULT_ROLE = "postgres-app" +VAULT_URI = "http+unix://" + urllib.parse.quote(VAULT_SOCKET, safe="") + +# Vault starts sealed and is unsealed by an operator. Waiting is correct; +# waiting forever is not -- the original of this looped on VaultDown with no +# bound, so a Vault that never came up left the container running and silent +# instead of failing. Roughly five minutes, then say so and stop. +VAULT_UNSEAL_ATTEMPTS = 30 +VAULT_UNSEAL_INTERVAL_SECONDS = 10 + def get_secret(name: str, default: str | None = None) -> str | None: """Return a secret, preferring the mounted file over the environment. @@ -104,3 +127,76 @@ def load_secrets_to_environ(names: Iterable[str]) -> None: "CHAINLIT_AUTH_SECRET", "LITERAL_API_KEY", ) + + +def _vault_credentials() -> tuple[str, str]: + """Ask Vault for a fresh Postgres username and password. + + Never logs the response. The original of this called + `logging.warning(response)` with Vault's reply, which contains exactly the + credentials it has just issued. + """ + import hvac + import hvac.exceptions + import requests_unixsocket + + token = VAULT_TOKEN_FILE.read_text().strip() + client = hvac.Client( + url=VAULT_URI, token=token, session=requests_unixsocket.Session() + ) + + for attempt in range(1, VAULT_UNSEAL_ATTEMPTS + 1): + try: + issued = client.secrets.database.generate_credentials(name=VAULT_ROLE) + return issued["data"]["username"], issued["data"]["password"] + except hvac.exceptions.VaultDown: + logging.warning( + f"Vault is sealed; waiting for an operator to unseal it " + f"({attempt}/{VAULT_UNSEAL_ATTEMPTS})" + ) + except Exception as exc: + # The message, never the response body. + logging.error( + f"Vault error while issuing credentials: {type(exc).__name__}" + ) + sleep(VAULT_UNSEAL_INTERVAL_SECONDS) + + raise RuntimeError( + f"Vault did not issue Postgres credentials after " + f"{VAULT_UNSEAL_ATTEMPTS} attempts. It is configured " + f"({VAULT_TOKEN_FILE} exists) but not usable -- unseal it, or remove the " + "token to fall back to POSTGRES_PASSWORD." + ) + + +def get_db_uri(db_name: str | None, *, driver: str | None = None) -> str | None: + """Build a Postgres URI over the unix socket, preferring Vault credentials. + + `driver` selects a SQLAlchemy dialect (`psycopg`); omit it for a plain libpq + URI, which is what psycopg and langgraph want. + + Returns None when `db_name` is empty or no password can be found, which is + how every caller decides whether database-backed features are configured at + all. + """ + if not db_name: + return None + + if VAULT_TOKEN_FILE.exists(): + username, password = _vault_credentials() + else: + username = os.getenv("POSTGRES_USER", "postgres") + found = get_secret("POSTGRES_PASSWORD") + if found is None: + return None + password = found + + scheme = f"postgresql+{driver}" if driver else "postgresql" + return ( + # safe="" so a "/" is escaped too. quote() leaves it alone by default, + # and Vault-issued passwords are random punctuation -- an unescaped + # slash silently truncates the URI at the database name. + f"{scheme}://{urllib.parse.quote(username, safe='')}" + f":{urllib.parse.quote(password, safe='')}" + f"@/{db_name}?host={POSTGRES_SOCKET}" + ) diff --git a/tests/util/test_secrets.py b/tests/util/test_secrets.py index 20dfbadf..622821c4 100644 --- a/tests/util/test_secrets.py +++ b/tests/util/test_secrets.py @@ -134,3 +134,63 @@ def test_the_secret_names_are_ones_the_deployment_actually_uses() -> None: if n not in declared and n not in {"CHAINLIT_AUTH_SECRET", "LITERAL_API_KEY"} ] assert not unknown, f"not in env_template: {unknown}" + + +def test_db_uri_uses_the_socket_and_never_tcp( + mounted: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Postgres is started with no TCP listener at all, so a host:port URI + cannot connect. Verified against a real container: `psql -h 127.0.0.1` + gives "Connection refused" while the socket URI below returns a row.""" + monkeypatch.setattr(secrets, "VAULT_TOKEN_FILE", Path("/nonexistent")) + monkeypatch.setenv("POSTGRES_USER", "postgres") + monkeypatch.setenv("POSTGRES_PASSWORD", "pw") + + uri = secrets.get_db_uri("chainlit") + + assert uri == "postgresql://postgres:pw@/chainlit?host=/sockets/postgres/" + assert "5432" not in uri + assert "@postgres:" not in uri, "no host:port form anywhere" + + +def test_db_uri_names_the_dialect_when_asked( + mounted: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """SQLAlchemy needs postgresql+psycopg; psycopg and langgraph need it absent.""" + monkeypatch.setattr(secrets, "VAULT_TOKEN_FILE", Path("/nonexistent")) + monkeypatch.setenv("POSTGRES_USER", "postgres") + monkeypatch.setenv("POSTGRES_PASSWORD", "pw") + uri = secrets.get_db_uri("chainlit", driver="psycopg") + assert uri is not None + assert uri.startswith("postgresql+psycopg://") + + +def test_db_uri_escapes_credentials( + mounted: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Vault-issued passwords contain punctuation; an unescaped one silently + truncates the URI at the first '@' or '/'.""" + monkeypatch.setattr(secrets, "VAULT_TOKEN_FILE", Path("/nonexistent")) + monkeypatch.setenv("POSTGRES_USER", "user@host") + monkeypatch.setenv("POSTGRES_PASSWORD", "p@ss/word") + + uri = secrets.get_db_uri("chainlit") + assert uri is not None + assert "user%40host" in uri + assert "p%40ss%2Fword" in uri + + +@pytest.mark.parametrize("db_name", [None, ""]) +def test_no_database_configured_returns_none( + db_name: str | None, mounted: Path +) -> None: + """How every caller decides whether database-backed features exist.""" + assert secrets.get_db_uri(db_name) is None + + +def test_no_password_anywhere_returns_none( + mounted: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(secrets, "VAULT_TOKEN_FILE", Path("/nonexistent")) + monkeypatch.delenv("POSTGRES_PASSWORD", raising=False) + assert secrets.get_db_uri("chainlit") is None