fastapi-breakgate 1.0.1__py3-none-any.whl
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.
- fastapi_breakgate-1.0.1.dist-info/METADATA +191 -0
- fastapi_breakgate-1.0.1.dist-info/RECORD +22 -0
- fastapi_breakgate-1.0.1.dist-info/WHEEL +4 -0
- fastapi_breakgate-1.0.1.dist-info/entry_points.txt +6 -0
- fastapi_breakgate-1.0.1.dist-info/licenses/LICENSE +21 -0
- fastapi_contract/__init__.py +26 -0
- fastapi_contract/__main__.py +4 -0
- fastapi_contract/__version__.py +1 -0
- fastapi_contract/check.py +167 -0
- fastapi_contract/cli.py +295 -0
- fastapi_contract/comment.py +101 -0
- fastapi_contract/config.py +161 -0
- fastapi_contract/diff/__init__.py +5 -0
- fastapi_contract/diff/classifier.py +779 -0
- fastapi_contract/diff/oasdiff.py +160 -0
- fastapi_contract/diff/policy.py +85 -0
- fastapi_contract/doctor.py +251 -0
- fastapi_contract/export.py +283 -0
- fastapi_contract/intentional.py +109 -0
- fastapi_contract/models.py +33 -0
- fastapi_contract/py.typed +0 -0
- fastapi_contract/pytest_plugin.py +98 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastapi-breakgate
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Production merge gate for FastAPI: export live OpenAPI, detect breaking changes, enforce intentional-break protocol.
|
|
5
|
+
Project-URL: Homepage, https://github.com/prakash888kp/fastapi-breakgate
|
|
6
|
+
Project-URL: Documentation, https://github.com/prakash888kp/fastapi-breakgate#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/prakash888kp/fastapi-breakgate
|
|
8
|
+
Project-URL: Issues, https://github.com/prakash888kp/fastapi-breakgate/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/prakash888kp/fastapi-breakgate/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: prakash888kp <prakash888kp@users.noreply.github.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api-compatibility,breaking-changes,ci,contract-testing,fastapi,openapi
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Framework :: FastAPI
|
|
17
|
+
Classifier: Framework :: Pytest
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
28
|
+
Classifier: Topic :: Software Development :: Testing
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: fastapi>=0.110
|
|
32
|
+
Requires-Dist: rich>=13.7
|
|
33
|
+
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
34
|
+
Requires-Dist: typer>=0.12
|
|
35
|
+
Requires-Dist: typing-extensions>=4.8; python_version < '3.11'
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
38
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
39
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
43
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
44
|
+
Provides-Extra: oasdiff
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# fastapi-breakgate
|
|
48
|
+
|
|
49
|
+
**Production merge gate for FastAPI.** Export live OpenAPI in CI, fail on accidental breaking changes, and make intentional breaks an explicit, changelog-backed review event.
|
|
50
|
+
|
|
51
|
+
> Status: **Beta (v1.0.1)** — core gate is solid; dogfood on real apps welcome.
|
|
52
|
+
> Not a fuzzer (that’s Schemathesis). Not a visual OpenAPI browser. A **hard merge gate** with FastAPI-aware DX.
|
|
53
|
+
|
|
54
|
+
> **Note:** The PyPI name is `fastapi-breakgate` because `fastapi-contract` is already taken by an unrelated project. The import package remains `fastapi_contract`.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install fastapi-breakgate
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
CLI commands (either entry point works):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
fastapi-breakgate --help
|
|
66
|
+
fastapi-contract --help
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 5-minute setup
|
|
70
|
+
|
|
71
|
+
### 1. Configure
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
# pyproject.toml
|
|
75
|
+
[tool.fastapi_contract]
|
|
76
|
+
app = "myapp.main:app"
|
|
77
|
+
policy = "additive-friendly" # or "strict"
|
|
78
|
+
fail_on = "breaking"
|
|
79
|
+
snapshot_path = "openapi.snapshot.json"
|
|
80
|
+
intentional_label = "breaking-change"
|
|
81
|
+
intentional_title_marker = "[breaking-change]"
|
|
82
|
+
changelog_required_heading = "### Breaking"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Commit a base snapshot
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
fastapi-breakgate doctor --app myapp.main:app
|
|
89
|
+
fastapi-breakgate snapshot --app myapp.main:app
|
|
90
|
+
git add openapi.snapshot.json && git commit -m "chore: add OpenAPI contract snapshot"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Gate PRs (GitHub Action)
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
# .github/workflows/contract.yml
|
|
97
|
+
name: API contract
|
|
98
|
+
on:
|
|
99
|
+
pull_request:
|
|
100
|
+
jobs:
|
|
101
|
+
contract:
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
- uses: actions/setup-python@v5
|
|
106
|
+
with:
|
|
107
|
+
python-version: "3.12"
|
|
108
|
+
- name: Install project
|
|
109
|
+
run: pip install -e ".[dev]"
|
|
110
|
+
- uses: prakash888kp/fastapi-breakgate/action@v1
|
|
111
|
+
with:
|
|
112
|
+
app: myapp.main:app
|
|
113
|
+
fail-on: breaking
|
|
114
|
+
policy: additive-friendly
|
|
115
|
+
install-command: ""
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or call the CLI directly:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pip install fastapi-breakgate
|
|
122
|
+
fastapi-breakgate check --app myapp.main:app --annotations
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## CLI
|
|
126
|
+
|
|
127
|
+
| Command | Purpose |
|
|
128
|
+
|---------|---------|
|
|
129
|
+
| `fastapi-breakgate doctor` | Diagnose import/export readiness |
|
|
130
|
+
| `fastapi-breakgate export -o openapi.json` | Export live OpenAPI |
|
|
131
|
+
| `fastapi-breakgate snapshot` | Write committed snapshot |
|
|
132
|
+
| `fastapi-breakgate diff --base-file openapi.snapshot.json` | Classify changes |
|
|
133
|
+
| `fastapi-breakgate check` | CI entrypoint (exit 1 on accidental breaks) |
|
|
134
|
+
| `fastapi-breakgate comment -o pr.md` | Render PR markdown |
|
|
135
|
+
|
|
136
|
+
## Intentional breaking changes
|
|
137
|
+
|
|
138
|
+
Accidental breaks fail CI. Planned breaks must complete the protocol:
|
|
139
|
+
|
|
140
|
+
1. PR label `breaking-change` **or** title contains `[breaking-change]`
|
|
141
|
+
2. `CHANGELOG.md` includes `### Breaking` with at least one bullet
|
|
142
|
+
|
|
143
|
+
Then the check passes with a visible warning and SemVer **major** suggestion.
|
|
144
|
+
|
|
145
|
+
## Pytest plugin
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
def test_openapi_contract(fastapi_contract):
|
|
149
|
+
fastapi_contract.assert_compatible()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pytest --fastapi-contract-app=myapp.main:app --fastapi-contract-base=openapi.snapshot.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Policy
|
|
157
|
+
|
|
158
|
+
| Change | `additive-friendly` (default) | `strict` |
|
|
159
|
+
|--------|-------------------------------|----------|
|
|
160
|
+
| Add optional response field | safe | safe |
|
|
161
|
+
| Add response enum value | **safe** | breaking |
|
|
162
|
+
| Remove path / field | breaking | breaking |
|
|
163
|
+
| Add required request field | breaking | breaking |
|
|
164
|
+
|
|
165
|
+
## Export reliability
|
|
166
|
+
|
|
167
|
+
Real apps often need DB/env at import time. Strategy ladder:
|
|
168
|
+
|
|
169
|
+
1. **Live** import with stub/default env + optional `.env.contract`
|
|
170
|
+
2. **Factory** (`create_app()`) when configured/detected
|
|
171
|
+
3. **Snapshot** fallback in `export_mode = "auto"` with an explicit warning
|
|
172
|
+
|
|
173
|
+
Run `fastapi-breakgate doctor` when CI import fails.
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pip install -e ".[dev]"
|
|
179
|
+
export PYTHONPATH=src:examples
|
|
180
|
+
python -m coverage run -m pytest
|
|
181
|
+
python -m coverage report --fail-under=75
|
|
182
|
+
ruff check src tests
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Release / PyPI
|
|
186
|
+
|
|
187
|
+
Tagging `vX.Y.Z` (must match `pyproject.toml`) runs CI tests, publishes to PyPI via Trusted Publishing, and creates a GitHub Release.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
fastapi_contract/__init__.py,sha256=j8qt2RigbXF_9F-doZm7axdLWJUqzz28w0PPvzV9PEs,772
|
|
2
|
+
fastapi_contract/__main__.py,sha256=Du_1KCfuMIJD4QsCmXjK7s7cJv9-6D5rIiAFZw4rgJg,75
|
|
3
|
+
fastapi_contract/__version__.py,sha256=d4QHYmS_30j0hPN8NmNPnQ_Z0TphDRbu4MtQj9cT9e8,22
|
|
4
|
+
fastapi_contract/check.py,sha256=FfuKW7NLDq4qQq32COiAiX9XmyWjTkz03JQGEpy-QjE,5131
|
|
5
|
+
fastapi_contract/cli.py,sha256=6z_l1VloRjVRi9B7AsmpXZ1STJ6pUlE6wcrc2voUwUA,10415
|
|
6
|
+
fastapi_contract/comment.py,sha256=_a0eq2r9CBGPJlsHBohyERasCZm1WDTbH1oUVhhiqek,3550
|
|
7
|
+
fastapi_contract/config.py,sha256=urfETO8TqXhVACbOfwhXDNmEYXntdQ5vcA_jBvuvA1U,6749
|
|
8
|
+
fastapi_contract/doctor.py,sha256=2Cqrh1Lj_HLwzrINuOD7Ky1RY_wor8-MQoim4Kj0e2I,8057
|
|
9
|
+
fastapi_contract/export.py,sha256=y9DaHlTpcK5W6YgOCtUmI9KVUGiCZhGj-J8bRseksbU,9930
|
|
10
|
+
fastapi_contract/intentional.py,sha256=O2xuDyISVVasfh8zQc7ML8STsxfeQwJrZQtglAyi7wg,3214
|
|
11
|
+
fastapi_contract/models.py,sha256=93y2kKw2ofBOz0AgA2w9eow9-dIp-tI9Z7CRSXpLp0U,615
|
|
12
|
+
fastapi_contract/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
fastapi_contract/pytest_plugin.py,sha256=W_EHilInPPKsb9f9IbqWAxu4z7I-eWgA6aXxyY_P4l8,3170
|
|
14
|
+
fastapi_contract/diff/__init__.py,sha256=k6NbibV_zvlMZ7sjCIBWDxwy10rckH8sB1UmNrEoDJc,211
|
|
15
|
+
fastapi_contract/diff/classifier.py,sha256=-EvQYZ-W8rmE6bUSy4Z1-k3owqG8_w9C3q-fYIoI4qw,24296
|
|
16
|
+
fastapi_contract/diff/oasdiff.py,sha256=h5cEMrQrHUW2e1lKj0KOkIIeDQswo0f3IfANLQ-ushw,5914
|
|
17
|
+
fastapi_contract/diff/policy.py,sha256=xBP8M4-0qyYlf-PtmLwOWcoW3aBgaxsh28Qq3I3AyZo,3544
|
|
18
|
+
fastapi_breakgate-1.0.1.dist-info/METADATA,sha256=fo-b_lnpgAtMesvpOBdzKaiXXx92HHOn6tK2yNjEZc4,6126
|
|
19
|
+
fastapi_breakgate-1.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
20
|
+
fastapi_breakgate-1.0.1.dist-info/entry_points.txt,sha256=7i7o8b8whc2AXCJWQEFOTt2naL5HgNpIv9Xns1xTREM,169
|
|
21
|
+
fastapi_breakgate-1.0.1.dist-info/licenses/LICENSE,sha256=-XknleU--HhRvKSoQX-LZcNUtDP-PpMphuIOh0lnAv0,1102
|
|
22
|
+
fastapi_breakgate-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 prakash888kp / fastapi-breakgate contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""fastapi-contract — production merge gate for FastAPI OpenAPI breaking changes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi_contract.__version__ import __version__
|
|
6
|
+
from fastapi_contract.check import CheckResult, run_check
|
|
7
|
+
from fastapi_contract.config import ContractConfig, load_config
|
|
8
|
+
from fastapi_contract.diff.classifier import Change, ChangeSeverity, DiffResult, diff_openapi
|
|
9
|
+
from fastapi_contract.export import ExportResult, export_openapi
|
|
10
|
+
from fastapi_contract.models import FailOn, PolicyName
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"Change",
|
|
15
|
+
"ChangeSeverity",
|
|
16
|
+
"CheckResult",
|
|
17
|
+
"ContractConfig",
|
|
18
|
+
"DiffResult",
|
|
19
|
+
"ExportResult",
|
|
20
|
+
"FailOn",
|
|
21
|
+
"PolicyName",
|
|
22
|
+
"diff_openapi",
|
|
23
|
+
"export_openapi",
|
|
24
|
+
"load_config",
|
|
25
|
+
"run_check",
|
|
26
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.1"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Orchestrate export → diff → intentional protocol → exit decision."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi_contract.comment import render_markdown_report
|
|
11
|
+
from fastapi_contract.config import ContractConfig
|
|
12
|
+
from fastapi_contract.diff.classifier import DiffResult, diff_openapi
|
|
13
|
+
from fastapi_contract.diff.oasdiff import OasdiffNotFoundError, diff_with_oasdiff, find_oasdiff
|
|
14
|
+
from fastapi_contract.export import ExportResult, export_openapi
|
|
15
|
+
from fastapi_contract.intentional import IntentionalStatus, evaluate_intentional
|
|
16
|
+
from fastapi_contract.models import ExportStrategy, FailOn, PolicyName
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CheckResult:
|
|
21
|
+
passed: bool
|
|
22
|
+
exit_code: int
|
|
23
|
+
diff: DiffResult
|
|
24
|
+
candidate: ExportResult
|
|
25
|
+
base: dict[str, Any]
|
|
26
|
+
intentional: IntentionalStatus | None = None
|
|
27
|
+
report_markdown: str = ""
|
|
28
|
+
messages: list[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def accidental_breaking(self) -> bool:
|
|
32
|
+
return bool(self.diff.breaking) and not (
|
|
33
|
+
self.intentional is not None and self.intentional.allowed
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_schema_file(path: Path) -> dict[str, Any]:
|
|
38
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
raise ValueError(f"Schema at {path} must be a JSON object")
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compare_schemas(
|
|
45
|
+
base: dict[str, Any],
|
|
46
|
+
candidate: dict[str, Any],
|
|
47
|
+
*,
|
|
48
|
+
policy: PolicyName,
|
|
49
|
+
prefer_oasdiff: bool = False,
|
|
50
|
+
oasdiff_bin: str | None = None,
|
|
51
|
+
) -> DiffResult:
|
|
52
|
+
use_oasdiff = prefer_oasdiff or bool(oasdiff_bin and find_oasdiff(oasdiff_bin))
|
|
53
|
+
if use_oasdiff:
|
|
54
|
+
try:
|
|
55
|
+
return diff_with_oasdiff(
|
|
56
|
+
base,
|
|
57
|
+
candidate,
|
|
58
|
+
policy=policy,
|
|
59
|
+
oasdiff_bin=oasdiff_bin,
|
|
60
|
+
)
|
|
61
|
+
except OasdiffNotFoundError:
|
|
62
|
+
pass
|
|
63
|
+
except Exception: # noqa: BLE001 - always fall back to Python engine
|
|
64
|
+
pass
|
|
65
|
+
return diff_openapi(base, candidate, policy=policy)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _should_fail(diff: DiffResult, fail_on: FailOn) -> bool:
|
|
69
|
+
if fail_on == FailOn.ANY:
|
|
70
|
+
return bool(diff.changes)
|
|
71
|
+
if fail_on == FailOn.RISKY:
|
|
72
|
+
return bool(diff.breaking or diff.risky)
|
|
73
|
+
return bool(diff.breaking)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_check(
|
|
77
|
+
config: ContractConfig | None = None,
|
|
78
|
+
*,
|
|
79
|
+
cwd: Path | None = None,
|
|
80
|
+
base_schema: dict[str, Any] | None = None,
|
|
81
|
+
base_schema_path: Path | None = None,
|
|
82
|
+
candidate_schema: dict[str, Any] | None = None,
|
|
83
|
+
pr_title: str | None = None,
|
|
84
|
+
pr_labels: list[str] | None = None,
|
|
85
|
+
changelog_text: str | None = None,
|
|
86
|
+
) -> CheckResult:
|
|
87
|
+
"""Run a full contract check.
|
|
88
|
+
|
|
89
|
+
Base schema resolution order:
|
|
90
|
+
1. Explicit ``base_schema``
|
|
91
|
+
2. Explicit ``base_schema_path``
|
|
92
|
+
3. Committed snapshot at ``config.snapshot_path`` (common local/CI mode)
|
|
93
|
+
"""
|
|
94
|
+
cfg = config or ContractConfig()
|
|
95
|
+
root = (cwd or Path.cwd()).resolve()
|
|
96
|
+
messages: list[str] = []
|
|
97
|
+
|
|
98
|
+
if candidate_schema is not None:
|
|
99
|
+
candidate = ExportResult(
|
|
100
|
+
schema=candidate_schema,
|
|
101
|
+
strategy=ExportStrategy.LIVE,
|
|
102
|
+
app_path=cfg.app,
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
candidate = export_openapi(cfg, cwd=root)
|
|
106
|
+
messages.extend(candidate.warnings)
|
|
107
|
+
|
|
108
|
+
if base_schema is not None:
|
|
109
|
+
base = base_schema
|
|
110
|
+
elif base_schema_path is not None:
|
|
111
|
+
base = _load_schema_file(base_schema_path)
|
|
112
|
+
else:
|
|
113
|
+
snap = root / cfg.snapshot_path
|
|
114
|
+
if not snap.is_file():
|
|
115
|
+
raise FileNotFoundError(
|
|
116
|
+
f"Base schema not provided and snapshot missing at {snap}. "
|
|
117
|
+
"Run `fastapi-contract snapshot` on main, or pass --base-file."
|
|
118
|
+
)
|
|
119
|
+
base = _load_schema_file(snap)
|
|
120
|
+
messages.append(f"Using base schema from snapshot {snap}")
|
|
121
|
+
|
|
122
|
+
diff = compare_schemas(
|
|
123
|
+
base,
|
|
124
|
+
candidate.schema,
|
|
125
|
+
policy=cfg.policy,
|
|
126
|
+
prefer_oasdiff=cfg.prefer_oasdiff,
|
|
127
|
+
oasdiff_bin=cfg.oasdiff_bin,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
intentional: IntentionalStatus | None = None
|
|
131
|
+
would_fail = _should_fail(diff, cfg.fail_on)
|
|
132
|
+
if would_fail and diff.breaking:
|
|
133
|
+
intentional = evaluate_intentional(
|
|
134
|
+
cfg,
|
|
135
|
+
pr_title=pr_title,
|
|
136
|
+
pr_labels=pr_labels,
|
|
137
|
+
cwd=root,
|
|
138
|
+
changelog_text=changelog_text,
|
|
139
|
+
)
|
|
140
|
+
if intentional.allowed:
|
|
141
|
+
messages.append("Intentional breaking-change protocol satisfied.")
|
|
142
|
+
failed = False
|
|
143
|
+
exit_code = 0
|
|
144
|
+
else:
|
|
145
|
+
messages.extend(intentional.reasons)
|
|
146
|
+
failed = True
|
|
147
|
+
exit_code = 1
|
|
148
|
+
elif would_fail:
|
|
149
|
+
failed = True
|
|
150
|
+
exit_code = 1
|
|
151
|
+
else:
|
|
152
|
+
failed = False
|
|
153
|
+
exit_code = 0
|
|
154
|
+
|
|
155
|
+
report = render_markdown_report(
|
|
156
|
+
diff, intentional=intentional, failed=failed
|
|
157
|
+
)
|
|
158
|
+
return CheckResult(
|
|
159
|
+
passed=not failed,
|
|
160
|
+
exit_code=exit_code,
|
|
161
|
+
diff=diff,
|
|
162
|
+
candidate=candidate,
|
|
163
|
+
base=base,
|
|
164
|
+
intentional=intentional,
|
|
165
|
+
report_markdown=report,
|
|
166
|
+
messages=messages,
|
|
167
|
+
)
|