Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/generated_reference_sources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Generated-reference source update helpers."""
25 changes: 25 additions & 0 deletions scripts/generated_reference_sources/common.py
Original file line number Diff line number Diff line change
@@ -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")
93 changes: 93 additions & 0 deletions scripts/generated_reference_sources/splice_openapi.py
Original file line number Diff line number Diff line change
@@ -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
104 changes: 29 additions & 75 deletions scripts/update_generated_reference_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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",
Expand All @@ -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
Expand Down
35 changes: 29 additions & 6 deletions tests/test_update_generated_reference_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand All @@ -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,
)
Expand All @@ -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",)