mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
* ci(unit): fetch the merge base by sha for the db-and-spend schema check instead of cloning full history * ci(unit): read the schema check arguments through one typed mapping * ci(unit): give the schema check its own step with the ui-unit base fallback and register it with the merge-base step test * ci(unit): keep the setup ceiling at 35 minutes, the install step never took over 33 seconds --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from collections.abc import Iterator, Mapping
|
|
from pathlib import Path
|
|
from types import MappingProxyType
|
|
from typing import Final
|
|
|
|
from pydantic import TypeAdapter
|
|
|
|
Schema = Mapping[str, Mapping[str, str]]
|
|
MODELS: Final = re.compile(r"^[ \t]*model\s+(\w+)\s*\{(.*?)^[ \t]*\}", re.MULTILINE | re.DOTALL)
|
|
|
|
|
|
def parse_model_fields(schema: str) -> Schema:
|
|
def fields(body: str) -> Mapping[str, str]:
|
|
parts: Final = tuple(line.split() for line in body.splitlines())
|
|
return MappingProxyType({part[0]: " ".join(part[1:]) for part in parts if len(part) >= 2})
|
|
|
|
return MappingProxyType({match[1]: fields(match[2]) for match in MODELS.finditer(schema)})
|
|
|
|
|
|
def _field_changes(model: str, field: str, old_type: str, new_fields: Mapping[str, str]) -> Iterator[str]:
|
|
if field not in new_fields:
|
|
yield f"Breaking: Field {model}.{field} was removed"
|
|
return
|
|
new_type: Final = new_fields[field]
|
|
if old_type == new_type:
|
|
return
|
|
if "?" in old_type and "?" not in new_type:
|
|
yield f"Breaking: Field {model}.{field} changed from optional to required"
|
|
if not old_type.startswith(new_type.split("?")[0]):
|
|
yield f"Breaking: Field {model}.{field} changed type from {old_type} to {new_type}"
|
|
|
|
|
|
def _model_changes(model: str, old_fields: Mapping[str, str], new_schema: Schema) -> Iterator[str]:
|
|
if model not in new_schema:
|
|
yield f"Breaking: Model {model} was removed"
|
|
return
|
|
for field, old_type in old_fields.items():
|
|
yield from _field_changes(model, field, old_type, new_schema[model])
|
|
|
|
|
|
def check_breaking_changes(old_schema: Schema, new_schema: Schema) -> tuple[str, ...]:
|
|
def changes() -> Iterator[str]:
|
|
for model, fields in old_schema.items():
|
|
yield from _model_changes(model, fields, new_schema)
|
|
|
|
return tuple(changes())
|
|
|
|
|
|
def _merge_base(base_ref: str) -> str:
|
|
return subprocess.run(
|
|
("git", "merge-base", "HEAD", base_ref), capture_output=True, text=True, check=True
|
|
).stdout.strip()
|
|
|
|
|
|
def main() -> int:
|
|
parser: Final = argparse.ArgumentParser()
|
|
source: Final = parser.add_mutually_exclusive_group(required=True)
|
|
source.add_argument("--base-ref", help="compare against the merge base of HEAD and this ref (needs history)")
|
|
source.add_argument("--base-commit", help="compare against this commit as is (works in a shallow clone)")
|
|
options: Final = TypeAdapter(dict[str, str | None]).validate_python(vars(parser.parse_args()))
|
|
base_commit: Final = options["base_commit"]
|
|
base: Final = base_commit if base_commit else _merge_base(options["base_ref"] or "")
|
|
previous: Final = subprocess.run(
|
|
("git", "show", f"{base}:schema.prisma"), capture_output=True, text=True, check=True
|
|
).stdout
|
|
changes: Final = check_breaking_changes(
|
|
parse_model_fields(previous), parse_model_fields(Path("schema.prisma").read_text())
|
|
)
|
|
if changes:
|
|
sys.stderr.write("\n".join(changes) + "\n")
|
|
return 1
|
|
sys.stdout.write(f"Schema compatibility passed against merge base {base}\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|