Source code for ursa.store.config
"""Pydantic config models for the Ursa object-store layer.
A YAML config maps named roles (`default`, `raw`, `catalog`,
`polaris_cache`, ...) to backend configs. M1 ships R2 + Local; S3 / GCS
land as additive discriminator values once they have a real backend
implementation.
Credentials never appear in YAML. The `r2` backend resolves credentials
at construction time via `constellation_utils.secrets.r2_*()`. The YAML
selects *which* of those four accessors to call via the `creds` field;
the bucket name rides along on the resolved `R2Secrets` object so the
config never names buckets either — the cred selector IS the bucket
selector.
"""
from __future__ import annotations
from pathlib import Path
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field
__all__ = [
"R2StoreConfig",
"LocalStoreConfig",
"ObjectStoreConfig",
"UrsaConfig",
"R2Creds",
]
# The four credential accessors exposed by constellation_utils.secrets.
# Each one resolves to a 1Password item that is bucket-scoped, so the
# choice of cred name fully determines which bucket the store reads/writes.
R2Creds = Literal["raw_rw", "raw_ro", "assets_rw", "assets_ro"]
[docs]
class _Base(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
[docs]
class R2StoreConfig(_Base):
"""Cloudflare R2 backend, S3-protocol via obstore.
`creds` selects the constellation-utils accessor (and therefore the
bucket — `*_rw` accessors return RW credentials, `*_ro` return RO).
The bucket name comes from the resolved credential object; we never
repeat it in YAML.
Mapping (see `docs/source/architecture.md`):
- `assets_rw` -> `constellation-assets[-test]` RW (Ursa-managed data)
- `assets_ro` -> `constellation-assets[-test]` RO (downstream readers)
- `raw_rw` -> `constellation-data[-test]` RW (data-engine uploader)
- `raw_ro` -> `constellation-data[-test]` RO (Ursa reading raw)
"""
backend: Literal["r2"]
creds: R2Creds
prefix: str = ""
[docs]
class LocalStoreConfig(_Base):
"""Local filesystem backend backed by `obstore.store.LocalStore`.
`root` is combined with `prefix` and resolved on construction; the
`LocalStore` then confines all keys under that directory.
"""
backend: Literal["local"]
root: Path
prefix: str = ""
ObjectStoreConfig = Annotated[
R2StoreConfig | LocalStoreConfig,
Field(discriminator="backend"),
]
[docs]
class UrsaConfig(_Base):
"""Top-level config. `stores` maps role names to backend configs.
`get_store(role)` defaults `role` to `"default"`; configs that omit a
`default` entry must pass `role=` explicitly to `get_store()`.
"""
stores: dict[str, ObjectStoreConfig]