From 11d4e523d2cda2e1b212d1d3461ea56d1e425dd4 Mon Sep 17 00:00:00 2001 From: danielporterda Date: Mon, 15 Jun 2026 12:03:44 -0400 Subject: [PATCH] Split Splice source updater module Signed-off-by: danielporterda --- .../generated_reference_sources/__init__.py | 1 + scripts/generated_reference_sources/common.py | 25 +++++ .../splice_openapi.py | 93 ++++++++++++++++ scripts/update_generated_reference_sources.py | 104 +++++------------- ...test_update_generated_reference_sources.py | 35 +++++- 5 files changed, 177 insertions(+), 81 deletions(-) create mode 100644 scripts/generated_reference_sources/__init__.py create mode 100644 scripts/generated_reference_sources/common.py create mode 100644 scripts/generated_reference_sources/splice_openapi.py diff --git a/scripts/generated_reference_sources/__init__.py b/scripts/generated_reference_sources/__init__.py new file mode 100644 index 00000000..5cbc4ed7 --- /dev/null +++ b/scripts/generated_reference_sources/__init__.py @@ -0,0 +1 @@ +"""Generated-reference source update helpers.""" diff --git a/scripts/generated_reference_sources/common.py b/scripts/generated_reference_sources/common.py new file mode 100644 index 00000000..cbf2823d --- /dev/null +++ b/scripts/generated_reference_sources/common.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class SourceUpdate: + source: str + path: Path + field: str + previous: str + current: str + + +def load_json(path: Path) -> dict[str, object]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"Expected JSON object in {path}") + return payload + + +def write_json(path: Path, payload: dict[str, object]) -> None: + path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") diff --git a/scripts/generated_reference_sources/splice_openapi.py b/scripts/generated_reference_sources/splice_openapi.py new file mode 100644 index 00000000..17899a57 --- /dev/null +++ b/scripts/generated_reference_sources/splice_openapi.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Required, TypedDict + +import generate_splice_mintlify_openapi as splice_openapi_generator + +from generated_reference_sources.common import SourceUpdate, load_json, write_json + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SOURCE_KEY = "splice-openapi" +SOURCE_LABEL = "Splice OpenAPI" +DEFAULT_SOURCE_CONFIG = ( + REPO_ROOT / "config" / "mintlify-openapi" / "splice-openapi" / "source-artifacts.json" +) + + +class SpliceOpenApiSpecConfig(TypedDict, total=False): + filename: str + nav_label: str + source: str + directory: str + + +class SpliceOpenApiFamilyConfig(TypedDict, total=False): + group: str + specs: list[SpliceOpenApiSpecConfig] + + +class SpliceOpenApiSourceConfigPayload(TypedDict, total=False): + source: str + release_repo: str + tag_regex: str + min_version: str + publish_version: Required[str] + asset_template: str + nav_dropdown: str + top_level_group_label: str + insert_after_group: str + managed_openapi_root: str + enabled_nav_specs: list[str] + legacy_cleanup_paths: list[str] + families: list[SpliceOpenApiFamilyConfig] + + +@dataclass(frozen=True) +class SpliceOpenApiSourceConfig: + raw: SpliceOpenApiSourceConfigPayload + publish_version: str + + +def parse_source_config(path: Path) -> SpliceOpenApiSourceConfig: + raw_json = load_json(path) + publish_version = raw_json.get("publish_version") + if not isinstance(publish_version, str) or not publish_version: + raise ValueError(f"{path} must define non-empty publish_version") + raw: SpliceOpenApiSourceConfigPayload = {} + raw.update(raw_json) + return SpliceOpenApiSourceConfig(raw=raw, publish_version=publish_version) + + +def latest_version(source_config: SpliceOpenApiSourceConfig) -> str: + releases = splice_openapi_generator.selected_releases( + source_config=source_config.raw, + include_versions=None, + ) + return releases[-1]["version"] + + +def update_source( + *, + source_config_path: Path, + dry_run: bool, +) -> SourceUpdate | None: + source_config = parse_source_config(source_config_path) + current_version = latest_version(source_config) + if source_config.publish_version == current_version: + return None + + update = SourceUpdate( + source=SOURCE_LABEL, + path=source_config_path, + field="publish_version", + previous=source_config.publish_version, + current=current_version, + ) + if not dry_run: + updated_config = dict(source_config.raw) + updated_config["publish_version"] = current_version + write_json(source_config_path, updated_config) + return update diff --git a/scripts/update_generated_reference_sources.py b/scripts/update_generated_reference_sources.py index 5dc458d0..45cee1cf 100644 --- a/scripts/update_generated_reference_sources.py +++ b/scripts/update_generated_reference_sources.py @@ -3,73 +3,15 @@ from __future__ import annotations import argparse -import json import sys -from dataclasses import dataclass from pathlib import Path -from typing import Any -import generate_splice_mintlify_openapi as splice_openapi +from generated_reference_sources import splice_openapi +from generated_reference_sources.common import SourceUpdate -REPO_ROOT = Path(__file__).resolve().parents[1] -DEFAULT_SPLICE_OPENAPI_SOURCE_CONFIG = ( - REPO_ROOT / "config" / "mintlify-openapi" / "splice-openapi" / "source-artifacts.json" -) - - -@dataclass(frozen=True) -class SourceUpdate: - source: str - path: Path - field: str - previous: str - current: str - - -def load_json(path: Path) -> dict[str, Any]: - payload = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(payload, dict): - raise ValueError(f"Expected JSON object in {path}") - return payload - - -def write_json(path: Path, payload: dict[str, Any]) -> None: - path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - - -def latest_splice_openapi_version(source_config: dict[str, Any]) -> str: - releases = splice_openapi.selected_releases( - source_config=source_config, - include_versions=None, - ) - return releases[-1]["version"] - - -def update_splice_openapi_source( - *, - source_config_path: Path, - dry_run: bool, -) -> SourceUpdate | None: - source_config = load_json(source_config_path) - latest_version = latest_splice_openapi_version(source_config) - configured_version = source_config.get("publish_version") - if not isinstance(configured_version, str) or not configured_version: - raise ValueError(f"{source_config_path} must define non-empty publish_version") - if configured_version == latest_version: - return None - - update = SourceUpdate( - source="Splice OpenAPI", - path=source_config_path, - field="publish_version", - previous=configured_version, - current=latest_version, - ) - if not dry_run: - source_config["publish_version"] = latest_version - write_json(source_config_path, source_config) - return update +SOURCE_SPLICE_OPENAPI = splice_openapi.SOURCE_KEY +ALL_SOURCES = (SOURCE_SPLICE_OPENAPI,) def parse_args() -> argparse.Namespace: @@ -79,8 +21,18 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--splice-openapi-source-config", type=Path, - default=DEFAULT_SPLICE_OPENAPI_SOURCE_CONFIG, - help=f"Splice OpenAPI source-artifacts config. Default: {DEFAULT_SPLICE_OPENAPI_SOURCE_CONFIG}", + default=splice_openapi.DEFAULT_SOURCE_CONFIG, + help=f"Splice OpenAPI source-artifacts config. Default: {splice_openapi.DEFAULT_SOURCE_CONFIG}", + ) + parser.add_argument( + "--source", + action="append", + choices=ALL_SOURCES, + dest="sources", + help=( + "Limit updates to one source. Repeat to update multiple sources. " + "By default, all generated-reference sources are checked." + ), ) parser.add_argument( "--dry-run", @@ -95,19 +47,21 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() +def requested_sources(args: argparse.Namespace) -> tuple[str, ...]: + return tuple(dict.fromkeys(args.sources or ALL_SOURCES)) + + def main() -> int: args = parse_args() - updates = [ - update - for update in [ - update_splice_openapi_source( - source_config_path=args.splice_openapi_source_config.resolve(), - dry_run=args.dry_run or args.check, - ) - ] - if update is not None - ] - + sources = requested_sources(args) + updates: list[SourceUpdate] = [] + if SOURCE_SPLICE_OPENAPI in sources: + update = splice_openapi.update_source( + source_config_path=args.splice_openapi_source_config.resolve(), + dry_run=args.dry_run or args.check, + ) + if update is not None: + updates.append(update) if not updates: print("Generated reference source pins are up to date.") return 0 diff --git a/tests/test_update_generated_reference_sources.py b/tests/test_update_generated_reference_sources.py index 07e5d03e..745bbeaf 100644 --- a/tests/test_update_generated_reference_sources.py +++ b/tests/test_update_generated_reference_sources.py @@ -46,12 +46,12 @@ def test_update_splice_openapi_source_updates_stale_publish_version(tmp_path: Pa module = load_script_module() source_config_path = tmp_path / "source-artifacts.json" write_source_config(source_config_path, publish_version="0.5.18") - module.splice_openapi.selected_releases = lambda **_kwargs: [ + module.splice_openapi.splice_openapi_generator.selected_releases = lambda **_kwargs: [ {"version": "0.5.18"}, {"version": "0.6.7"}, ] - update = module.update_splice_openapi_source( + update = module.splice_openapi.update_source( source_config_path=source_config_path, dry_run=False, ) @@ -70,13 +70,13 @@ def test_update_splice_openapi_source_noops_when_current(tmp_path: Path) -> None module = load_script_module() source_config_path = tmp_path / "source-artifacts.json" write_source_config(source_config_path, publish_version="0.6.7") - module.splice_openapi.selected_releases = lambda **_kwargs: [ + module.splice_openapi.splice_openapi_generator.selected_releases = lambda **_kwargs: [ {"version": "0.5.18"}, {"version": "0.6.7"}, ] assert ( - module.update_splice_openapi_source( + module.splice_openapi.update_source( source_config_path=source_config_path, dry_run=False, ) @@ -89,12 +89,12 @@ def test_update_splice_openapi_source_dry_run_does_not_write(tmp_path: Path) -> module = load_script_module() source_config_path = tmp_path / "source-artifacts.json" write_source_config(source_config_path, publish_version="0.5.18") - module.splice_openapi.selected_releases = lambda **_kwargs: [ + module.splice_openapi.splice_openapi_generator.selected_releases = lambda **_kwargs: [ {"version": "0.5.18"}, {"version": "0.6.7"}, ] - update = module.update_splice_openapi_source( + update = module.splice_openapi.update_source( source_config_path=source_config_path, dry_run=True, ) @@ -103,3 +103,26 @@ def test_update_splice_openapi_source_dry_run_does_not_write(tmp_path: Path) -> assert update.previous == "0.5.18" assert update.current == "0.6.7" assert json.loads(source_config_path.read_text(encoding="utf-8"))["publish_version"] == "0.5.18" + + +def test_requested_sources_defaults_to_all_sources() -> None: + module = load_script_module() + + assert module.requested_sources(type("Args", (), {"sources": None})()) == module.ALL_SOURCES + + +def test_requested_sources_preserves_order_and_deduplicates() -> None: + module = load_script_module() + + assert module.requested_sources( + type( + "Args", + (), + { + "sources": [ + "splice-openapi", + "splice-openapi", + ] + }, + )() + ) == ("splice-openapi",)