breakscope 0.0.1__tar.gz → 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- breakscope-0.1.0/.github/workflows/release.yml +43 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/.gitignore +1 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/PKG-INFO +20 -2
- breakscope-0.1.0/README.md +35 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/breakscope/__init__.py +1 -1
- breakscope-0.1.0/breakscope/changes.py +93 -0
- breakscope-0.1.0/breakscope/cli/main.py +94 -0
- breakscope-0.1.0/breakscope/contracts/__init__.py +19 -0
- breakscope-0.1.0/breakscope/contracts/diff.py +424 -0
- breakscope-0.1.0/breakscope/contracts/loader.py +82 -0
- breakscope-0.1.0/breakscope/contracts/model.py +66 -0
- breakscope-0.1.0/breakscope/contracts/normalize.py +261 -0
- breakscope-0.1.0/breakscope/contracts/resolver.py +101 -0
- breakscope-0.1.0/breakscope/contracts/rules.py +54 -0
- breakscope-0.1.0/breakscope/errors.py +37 -0
- breakscope-0.1.0/breakscope/reports/__init__.py +31 -0
- breakscope-0.1.0/breakscope/reports/json.py +33 -0
- breakscope-0.1.0/breakscope/reports/terminal.py +78 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/docs/ARCHITECTURE-v0.1.md +194 -187
- {breakscope-0.0.1 → breakscope-0.1.0}/docs/ROADMAP.md +2 -2
- breakscope-0.1.0/examples/demo/api/openapi-v1.yaml +58 -0
- breakscope-0.1.0/examples/demo/api/openapi-v2.yaml +52 -0
- breakscope-0.1.0/tests/contracts/__init__.py +0 -0
- breakscope-0.1.0/tests/contracts/__snapshots__/test_rules.ambr +799 -0
- breakscope-0.1.0/tests/contracts/corpus/README.md +7 -0
- breakscope-0.1.0/tests/contracts/corpus/v30-link-example.yaml +203 -0
- breakscope-0.1.0/tests/contracts/corpus/v30-petstore-expanded.yaml +156 -0
- breakscope-0.1.0/tests/contracts/corpus/v30-petstore.yaml +119 -0
- breakscope-0.1.0/tests/contracts/corpus/v31-non-oauth-scopes.yaml +18 -0
- breakscope-0.1.0/tests/contracts/corpus/v31-webhook-example.yaml +34 -0
- breakscope-0.1.0/tests/contracts/make_rule_fixtures.py +215 -0
- breakscope-0.1.0/tests/contracts/rules/endpoint.added/new.yaml +86 -0
- breakscope-0.1.0/tests/contracts/rules/endpoint.added/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/endpoint.removed/new.yaml +63 -0
- breakscope-0.1.0/tests/contracts/rules/endpoint.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.added.optional/new.yaml +86 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.added.optional/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.added.required/new.yaml +87 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.added.required/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.became_required/new.yaml +83 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.became_required/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.enum.value_removed/new.yaml +81 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.enum.value_removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.removed/new.yaml +75 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.type.changed/new.yaml +79 -0
- breakscope-0.1.0/tests/contracts/rules/parameter.type.changed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.body.added.required/new.yaml +83 -0
- breakscope-0.1.0/tests/contracts/rules/request.body.added.required/old.yaml +77 -0
- breakscope-0.1.0/tests/contracts/rules/request.body.became_required/new.yaml +83 -0
- breakscope-0.1.0/tests/contracts/rules/request.body.became_required/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.media_type.removed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.media_type.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.added.required/new.yaml +86 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.added.required/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.became_required/new.yaml +84 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.became_required/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.enum.value_removed/new.yaml +81 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.enum.value_removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.removed/new.yaml +80 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.type.changed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/request.property.type.changed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.enum.value_added/new.yaml +83 -0
- breakscope-0.1.0/tests/contracts/rules/response.enum.value_added/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.enum.value_removed/new.yaml +81 -0
- breakscope-0.1.0/tests/contracts/rules/response.enum.value_removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.media_type.removed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.media_type.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.added/new.yaml +84 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.added/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.became_nullable/new.yaml +83 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.became_nullable/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.became_optional/new.yaml +81 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.became_optional/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.format.changed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.format.changed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.removed/new.yaml +80 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.type.changed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.property.type.changed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.status.removed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/response.status.removed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/schema.union.changed/new.yaml +82 -0
- breakscope-0.1.0/tests/contracts/rules/schema.union.changed/old.yaml +82 -0
- breakscope-0.1.0/tests/contracts/test_contracts.py +226 -0
- breakscope-0.1.0/tests/contracts/test_rules.py +32 -0
- breakscope-0.1.0/tests/test_cli.py +65 -0
- breakscope-0.0.1/README.md +0 -17
- breakscope-0.0.1/breakscope/cli/main.py +0 -44
- breakscope-0.0.1/recoverycode.md +0 -9
- breakscope-0.0.1/tests/test_cli.py +0 -16
- {breakscope-0.0.1 → breakscope-0.1.0}/.github/workflows/tests.yml +0 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/breakscope/__main__.py +0 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/breakscope/cli/__init__.py +0 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/pyproject.toml +0 -0
- {breakscope-0.0.1 → breakscope-0.1.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- name: Check tag matches package version
|
|
18
|
+
run: |
|
|
19
|
+
version=$(python -c "import re; print(re.search(r'__version__ = \"(.+)\"', open('breakscope/__init__.py').read()).group(1))")
|
|
20
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
21
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match breakscope/__init__.py version $version"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
- run: python -m pip install build twine
|
|
25
|
+
- run: python -m build
|
|
26
|
+
- run: python -m twine check dist/*
|
|
27
|
+
- uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish:
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: breakscope
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: See what your API changes will break in your code.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -28,7 +28,25 @@ pip install breakscope
|
|
|
28
28
|
breakscope diff openapi-old.yaml openapi-new.yaml
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
```text
|
|
32
|
+
BREAKING CHANGES: 4
|
|
33
|
+
|
|
34
|
+
🔴 response.property.removed User.name
|
|
35
|
+
A response property was removed.
|
|
36
|
+
affects 3 operations:
|
|
37
|
+
GET /users -> 200
|
|
38
|
+
POST /users -> 201
|
|
39
|
+
GET /users/{userId} -> 200
|
|
40
|
+
|
|
41
|
+
🔴 request.property.became_required POST /users
|
|
42
|
+
[request] `email` is now required
|
|
43
|
+
...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Try it on the demo: `breakscope diff examples/demo/api/openapi-v1.yaml examples/demo/api/openapi-v2.yaml`.
|
|
47
|
+
Use `--format json` for machine-readable output. Exit codes: `0` no breaking changes, `1` breaking changes, `2` error.
|
|
48
|
+
|
|
49
|
+
**Status:** v0.1: contract diff. Tracing changes into your code comes in v0.4. See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/ARCHITECTURE-v0.1.md](docs/ARCHITECTURE-v0.1.md).
|
|
32
50
|
|
|
33
51
|
## License
|
|
34
52
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# BreakScope
|
|
2
|
+
|
|
3
|
+
> **See what your API changes will break — before they reach production.**
|
|
4
|
+
|
|
5
|
+
API contract changes are easy to detect. Knowing what they break isn't.
|
|
6
|
+
BreakScope diffs your OpenAPI specs and traces each breaking change to the exact `file:line` in your Python and TypeScript code that's likely affected.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install breakscope
|
|
10
|
+
breakscope diff openapi-old.yaml openapi-new.yaml
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
BREAKING CHANGES: 4
|
|
15
|
+
|
|
16
|
+
🔴 response.property.removed User.name
|
|
17
|
+
A response property was removed.
|
|
18
|
+
affects 3 operations:
|
|
19
|
+
GET /users -> 200
|
|
20
|
+
POST /users -> 201
|
|
21
|
+
GET /users/{userId} -> 200
|
|
22
|
+
|
|
23
|
+
🔴 request.property.became_required POST /users
|
|
24
|
+
[request] `email` is now required
|
|
25
|
+
...
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Try it on the demo: `breakscope diff examples/demo/api/openapi-v1.yaml examples/demo/api/openapi-v2.yaml`.
|
|
29
|
+
Use `--format json` for machine-readable output. Exit codes: `0` no breaking changes, `1` breaking changes, `2` error.
|
|
30
|
+
|
|
31
|
+
**Status:** v0.1: contract diff. Tracing changes into your code comes in v0.4. See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/ARCHITECTURE-v0.1.md](docs/ARCHITECTURE-v0.1.md).
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""The change model shared by the diff engine, reporters and (later) the impact engine.
|
|
2
|
+
|
|
3
|
+
Nothing in here knows about OpenAPI. Downstream code depends on this module only.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Severity(StrEnum):
|
|
13
|
+
BREAKING = "breaking"
|
|
14
|
+
WARNING = "warning"
|
|
15
|
+
INFO = "info"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def rank(self) -> int:
|
|
19
|
+
return {"breaking": 0, "warning": 1, "info": 2}[self.value]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Direction(StrEnum):
|
|
23
|
+
REQUEST = "request"
|
|
24
|
+
RESPONSE = "response"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class APIChange(BaseModel):
|
|
28
|
+
model_config = ConfigDict(frozen=True)
|
|
29
|
+
|
|
30
|
+
rule: str
|
|
31
|
+
severity: Severity
|
|
32
|
+
method: str | None = None
|
|
33
|
+
path: str | None = None
|
|
34
|
+
direction: Direction | None = None
|
|
35
|
+
status_code: str | None = None
|
|
36
|
+
# Location of the field from the root of the request/response body (or parameter).
|
|
37
|
+
# "[]" marks an array element: ("items", "[]", "name").
|
|
38
|
+
field_path: tuple[str, ...] = ()
|
|
39
|
+
# Nearest named schema (components/schemas/<name>) that owns the field, and the
|
|
40
|
+
# field's path relative to it. User.name is ("name",) whether it was reached
|
|
41
|
+
# through GET /users/{id} or through GET /users -> "[]".
|
|
42
|
+
schema_name: str | None = None
|
|
43
|
+
schema_path: tuple[str, ...] = ()
|
|
44
|
+
old: Any = None
|
|
45
|
+
new: Any = None
|
|
46
|
+
message: str
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def key(self) -> str:
|
|
50
|
+
"""Stable identifier for snapshots, dedupe and ignore lists."""
|
|
51
|
+
parts = [
|
|
52
|
+
self.rule,
|
|
53
|
+
self.method or "",
|
|
54
|
+
self.path or "",
|
|
55
|
+
self.status_code or "",
|
|
56
|
+
".".join(self.field_path),
|
|
57
|
+
]
|
|
58
|
+
return " ".join(p for p in parts if p)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def group_key(self) -> tuple[str, ...]:
|
|
62
|
+
"""Changes to one shared schema field group together across operations."""
|
|
63
|
+
if self.schema_name and self.schema_path:
|
|
64
|
+
return (self.rule, self.schema_name, *self.schema_path)
|
|
65
|
+
return (self.key,)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def subject(self) -> str:
|
|
69
|
+
"""Short human label for the changed thing: `User.name`, `email`, `items[].id`."""
|
|
70
|
+
if self.schema_name and self.schema_path:
|
|
71
|
+
return format_field_path((self.schema_name, *self.schema_path))
|
|
72
|
+
return format_field_path(self.field_path)
|
|
73
|
+
|
|
74
|
+
def sort_key(self) -> tuple[Any, ...]:
|
|
75
|
+
return (
|
|
76
|
+
self.severity.rank,
|
|
77
|
+
self.path or "",
|
|
78
|
+
self.method or "",
|
|
79
|
+
self.direction or "",
|
|
80
|
+
self.status_code or "",
|
|
81
|
+
self.field_path,
|
|
82
|
+
self.rule,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_field_path(path: tuple[str, ...]) -> str:
|
|
87
|
+
out = ""
|
|
88
|
+
for part in path:
|
|
89
|
+
if part == "[]":
|
|
90
|
+
out += "[]"
|
|
91
|
+
else:
|
|
92
|
+
out += f".{part}" if out else part
|
|
93
|
+
return out
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from breakscope import __version__
|
|
9
|
+
from breakscope.changes import Severity
|
|
10
|
+
from breakscope.contracts import diff_files
|
|
11
|
+
from breakscope.errors import BreakScopeError
|
|
12
|
+
from breakscope.reports import json as json_report
|
|
13
|
+
from breakscope.reports import terminal
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="breakscope",
|
|
17
|
+
help="See what your API changes will break in your code.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
EXIT_OK, EXIT_BREAKING, EXIT_ERROR = 0, 1, 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Format(StrEnum):
|
|
25
|
+
terminal = "terminal"
|
|
26
|
+
json = "json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _version(value: bool) -> None:
|
|
30
|
+
if value:
|
|
31
|
+
typer.echo(f"breakscope {__version__}")
|
|
32
|
+
raise typer.Exit()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback()
|
|
36
|
+
def main(
|
|
37
|
+
version: Annotated[
|
|
38
|
+
bool, typer.Option("--version", callback=_version, is_eager=True, help="Show version.")
|
|
39
|
+
] = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def diff(
|
|
46
|
+
old: Annotated[Path, typer.Argument(help="Old OpenAPI spec (YAML or JSON).")],
|
|
47
|
+
new: Annotated[Path, typer.Argument(help="New OpenAPI spec (YAML or JSON).")],
|
|
48
|
+
fmt: Annotated[Format, typer.Option("--format", "-f", help="Output format.")] = Format.terminal,
|
|
49
|
+
min_severity: Annotated[
|
|
50
|
+
Severity, typer.Option("--min-severity", help="Hide changes below this severity.")
|
|
51
|
+
] = Severity.WARNING,
|
|
52
|
+
output: Annotated[
|
|
53
|
+
Path | None, typer.Option("--output", "-o", help="Write the report to a file.")
|
|
54
|
+
] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Show contract changes between two OpenAPI specs.
|
|
57
|
+
|
|
58
|
+
Exit code 0: no breaking changes. 1: breaking changes found. 2: error.
|
|
59
|
+
"""
|
|
60
|
+
err = Console(stderr=True)
|
|
61
|
+
try:
|
|
62
|
+
changes, a, b = diff_files(old, new)
|
|
63
|
+
except BreakScopeError as e:
|
|
64
|
+
err.print(e.render(), markup=False, highlight=False)
|
|
65
|
+
raise typer.Exit(EXIT_ERROR) from None
|
|
66
|
+
|
|
67
|
+
skipped = {
|
|
68
|
+
f"{m} {p} ({src})": reason
|
|
69
|
+
for contract, src in ((a, "old"), (b, "new"))
|
|
70
|
+
for (m, p), reason in contract.skipped.items()
|
|
71
|
+
}
|
|
72
|
+
shown = [c for c in changes if c.severity.rank <= min_severity.rank]
|
|
73
|
+
|
|
74
|
+
if fmt is Format.json:
|
|
75
|
+
text = json_report.render(shown, skipped=skipped)
|
|
76
|
+
if output:
|
|
77
|
+
output.write_text(text, encoding="utf-8")
|
|
78
|
+
else:
|
|
79
|
+
typer.echo(text, nl=False)
|
|
80
|
+
elif output:
|
|
81
|
+
with output.open("w", encoding="utf-8") as fh:
|
|
82
|
+
terminal.render(shown, Console(file=fh, width=100, no_color=True), skipped=skipped)
|
|
83
|
+
else:
|
|
84
|
+
terminal.render(shown, Console(highlight=False), skipped=skipped)
|
|
85
|
+
|
|
86
|
+
breaking = any(c.severity is Severity.BREAKING for c in changes)
|
|
87
|
+
raise typer.Exit(EXIT_BREAKING if breaking else EXIT_OK)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command()
|
|
91
|
+
def analyze() -> None:
|
|
92
|
+
"""Trace API changes into your codebase."""
|
|
93
|
+
typer.echo("analyze: coming in v0.4", err=True)
|
|
94
|
+
raise typer.Exit(EXIT_ERROR)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from breakscope.changes import APIChange
|
|
4
|
+
from breakscope.contracts.diff import diff_contracts
|
|
5
|
+
from breakscope.contracts.loader import load_spec
|
|
6
|
+
from breakscope.contracts.model import Contract
|
|
7
|
+
from breakscope.contracts.normalize import normalize
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_contract(path: Path) -> Contract:
|
|
11
|
+
return normalize(load_spec(path))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def diff_files(old: Path, new: Path) -> tuple[list[APIChange], Contract, Contract]:
|
|
15
|
+
a, b = load_contract(old), load_contract(new)
|
|
16
|
+
return diff_contracts(a, b), a, b
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["Contract", "diff_contracts", "diff_files", "load_contract"]
|