| """Typed dataset registry for STRATA acquisition. |
| |
| The registry is the single, version-controlled source of truth describing which |
| datasets STRATA uses, where they legitimately come from, their licensing tier, |
| and which languages they cover. It is intentionally declarative: acquisition |
| code in :mod:`strata.data.acquire` interprets these specs, but no download URLs |
| or credentials live in Python source. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
| from pathlib import Path |
|
|
| import yaml |
|
|
| |
| |
| |
|
|
| |
| TIER_OPEN = "open" |
| TIER_GATED_AUTO = "gated_auto" |
| TIER_GATED_MANUAL = "gated_manual" |
| TIER_LICENSE_REQUIRED = "license_required" |
|
|
| LICENSE_TIERS = frozenset( |
| {TIER_OPEN, TIER_GATED_AUTO, TIER_GATED_MANUAL, TIER_LICENSE_REQUIRED} |
| ) |
|
|
| |
| KIND_HF_SNAPSHOT = "hf_snapshot" |
| KIND_HF_FILES = "hf_files" |
| KIND_HTTP = "http" |
| KIND_GITHUB_ARCHIVE = "github_archive" |
| KIND_LICENSE_REQUIRED = "license_required" |
|
|
| SOURCE_KINDS = frozenset( |
| { |
| KIND_HF_SNAPSHOT, |
| KIND_HF_FILES, |
| KIND_HTTP, |
| KIND_GITHUB_ARCHIVE, |
| KIND_LICENSE_REQUIRED, |
| } |
| ) |
|
|
| |
| |
| LANG_MULTI = "multi" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class DatasetSpec: |
| """Declarative description of one dataset.""" |
|
|
| id: str |
| title: str |
| layer: str |
| languages: tuple[str, ...] |
| license: str |
| license_tier: str |
| source: dict[str, object] |
| homepage: str = "" |
| notes: str = "" |
| optional: bool = False |
|
|
| def __post_init__(self) -> None: |
| self.validate() |
|
|
| @property |
| def kind(self) -> str: |
| return str(self.source.get("kind", "")) |
|
|
| def covers_language(self, language: str) -> bool: |
| return language in self.languages or LANG_MULTI in self.languages |
|
|
| def validate(self) -> None: |
| if not self.id or not self.id.replace("_", "").replace("-", "").isalnum(): |
| raise ValueError( |
| f"dataset id must be non-empty alphanumeric/_/-, got {self.id!r}" |
| ) |
| if self.license_tier not in LICENSE_TIERS: |
| raise ValueError( |
| f"[{self.id}] invalid license_tier {self.license_tier!r}; " |
| f"expected one of {sorted(LICENSE_TIERS)}" |
| ) |
| if not self.languages: |
| raise ValueError(f"[{self.id}] languages must be non-empty") |
| kind = self.kind |
| if kind not in SOURCE_KINDS: |
| raise ValueError( |
| f"[{self.id}] invalid source kind {kind!r}; " |
| f"expected one of {sorted(SOURCE_KINDS)}" |
| ) |
| _validate_source(self.id, kind, self.source) |
| if kind == KIND_LICENSE_REQUIRED and self.license_tier != TIER_LICENSE_REQUIRED: |
| raise ValueError( |
| f"[{self.id}] source kind {KIND_LICENSE_REQUIRED} requires " |
| f"license_tier {TIER_LICENSE_REQUIRED}" |
| ) |
|
|
|
|
| def _require(dataset_id: str, source: dict[str, object], *keys: str) -> None: |
| for key in keys: |
| if not source.get(key): |
| raise ValueError( |
| f"[{dataset_id}] source kind {source.get('kind')!r} requires " |
| f"non-empty {key!r}" |
| ) |
|
|
|
|
| def _validate_source(dataset_id: str, kind: str, source: dict[str, object]) -> None: |
| if kind == KIND_HF_SNAPSHOT: |
| _require(dataset_id, source, "repo_id") |
| elif kind == KIND_HF_FILES: |
| _require(dataset_id, source, "repo_id", "prefixes") |
| if not isinstance(source["prefixes"], list): |
| raise ValueError(f"[{dataset_id}] hf_files.prefixes must be a list") |
| elif kind == KIND_HTTP: |
| urls = source.get("urls") |
| if not isinstance(urls, list) or not urls: |
| raise ValueError(f"[{dataset_id}] http.urls must be a non-empty list") |
| for item in urls: |
| if not isinstance(item, dict) or not item.get("url"): |
| raise ValueError(f"[{dataset_id}] each http url needs a 'url' field") |
| elif kind == KIND_GITHUB_ARCHIVE: |
| _require(dataset_id, source, "repo") |
| elif kind == KIND_LICENSE_REQUIRED: |
| _require(dataset_id, source, "acquisition") |
|
|
|
|
| |
| |
| |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class Registry: |
| version: int |
| languages_core: tuple[str, ...] |
| datasets: tuple[DatasetSpec, ...] |
| description: str = "" |
|
|
| def by_id(self, dataset_id: str) -> DatasetSpec: |
| for spec in self.datasets: |
| if spec.id == dataset_id: |
| return spec |
| raise KeyError(f"unknown dataset id: {dataset_id!r}") |
|
|
| def ids(self) -> list[str]: |
| return [spec.id for spec in self.datasets] |
|
|
| def layers(self) -> list[str]: |
| seen: dict[str, None] = {} |
| for spec in self.datasets: |
| seen.setdefault(spec.layer, None) |
| return list(seen) |
|
|
| def filter( |
| self, |
| *, |
| ids: list[str] | None = None, |
| layers: list[str] | None = None, |
| languages: list[str] | None = None, |
| tiers: list[str] | None = None, |
| include_optional: bool = True, |
| ) -> list[DatasetSpec]: |
| """Return specs matching all provided filters (AND semantics). |
| |
| ``ids`` matching is explicit and always includes optional datasets, so |
| an operator can request an optional dataset by name. |
| """ |
|
|
| id_set = set(ids) if ids else None |
| layer_set = set(layers) if layers else None |
| tier_set = set(tiers) if tiers else None |
|
|
| selected: list[DatasetSpec] = [] |
| for spec in self.datasets: |
| if id_set is not None and spec.id not in id_set: |
| continue |
| if layer_set is not None and spec.layer not in layer_set: |
| continue |
| if tier_set is not None and spec.license_tier not in tier_set: |
| continue |
| if languages and not any(spec.covers_language(lang) for lang in languages): |
| continue |
| explicitly_named = id_set is not None and spec.id in id_set |
| if spec.optional and not include_optional and not explicitly_named: |
| continue |
| selected.append(spec) |
| return selected |
|
|
|
|
| def load_registry(path: Path) -> Registry: |
| """Load and validate a registry YAML file.""" |
|
|
| if not path.exists(): |
| raise FileNotFoundError(f"registry file not found: {path}") |
| data = yaml.safe_load(path.read_text(encoding="utf-8")) |
| if not isinstance(data, dict): |
| raise ValueError(f"registry {path} must be a mapping at the top level") |
|
|
| raw_datasets = data.get("datasets") |
| if not isinstance(raw_datasets, list) or not raw_datasets: |
| raise ValueError(f"registry {path} must contain a non-empty 'datasets' list") |
|
|
| specs: list[DatasetSpec] = [] |
| seen_ids: set[str] = set() |
| for raw in raw_datasets: |
| if not isinstance(raw, dict): |
| raise ValueError(f"registry {path}: each dataset entry must be a mapping") |
| spec = _spec_from_dict(raw) |
| if spec.id in seen_ids: |
| raise ValueError(f"registry {path}: duplicate dataset id {spec.id!r}") |
| seen_ids.add(spec.id) |
| specs.append(spec) |
|
|
| return Registry( |
| version=int(data.get("version", 1)), |
| languages_core=tuple(data.get("languages_core", []) or []), |
| datasets=tuple(specs), |
| description=str(data.get("description", "")), |
| ) |
|
|
|
|
| def _spec_from_dict(raw: dict[str, object]) -> DatasetSpec: |
| missing = [key for key in ("id", "layer", "license_tier", "source") if key not in raw] |
| if missing: |
| raise ValueError(f"dataset entry missing required keys: {missing} in {raw!r}") |
| source = raw["source"] |
| if not isinstance(source, dict): |
| raise ValueError(f"[{raw.get('id')}] source must be a mapping") |
| languages = raw.get("languages", []) |
| if not isinstance(languages, list): |
| raise ValueError(f"[{raw.get('id')}] languages must be a list") |
| return DatasetSpec( |
| id=str(raw["id"]), |
| title=str(raw.get("title", raw["id"])), |
| layer=str(raw["layer"]), |
| languages=tuple(str(lang) for lang in languages), |
| license=str(raw.get("license", "unknown")), |
| license_tier=str(raw["license_tier"]), |
| source=dict(source), |
| homepage=str(raw.get("homepage", "")), |
| notes=str(raw.get("notes", "")), |
| optional=bool(raw.get("optional", False)), |
| ) |
|
|
|
|
| |
| __all__ = [ |
| "DatasetSpec", |
| "Registry", |
| "load_registry", |
| "LICENSE_TIERS", |
| "SOURCE_KINDS", |
| "TIER_OPEN", |
| "TIER_GATED_AUTO", |
| "TIER_GATED_MANUAL", |
| "TIER_LICENSE_REQUIRED", |
| "KIND_HF_SNAPSHOT", |
| "KIND_HF_FILES", |
| "KIND_HTTP", |
| "KIND_GITHUB_ARCHIVE", |
| "KIND_LICENSE_REQUIRED", |
| "LANG_MULTI", |
| ] |
|
|