data-contract-registry 0.1.1__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.
- data_contract_registry-0.1.1/.github/workflows/ci.yml +41 -0
- data_contract_registry-0.1.1/.github/workflows/publish.yml +35 -0
- data_contract_registry-0.1.1/.gitignore +12 -0
- data_contract_registry-0.1.1/LICENSE +21 -0
- data_contract_registry-0.1.1/PKG-INFO +246 -0
- data_contract_registry-0.1.1/README.md +204 -0
- data_contract_registry-0.1.1/examples/contract.yaml +30 -0
- data_contract_registry-0.1.1/pyproject.toml +83 -0
- data_contract_registry-0.1.1/src/data_contract_registry/__init__.py +54 -0
- data_contract_registry-0.1.1/src/data_contract_registry/app.py +228 -0
- data_contract_registry-0.1.1/src/data_contract_registry/audit_stream.py +84 -0
- data_contract_registry-0.1.1/src/data_contract_registry/compatibility.py +197 -0
- data_contract_registry-0.1.1/src/data_contract_registry/from_decision_card.py +49 -0
- data_contract_registry-0.1.1/src/data_contract_registry/models.py +148 -0
- data_contract_registry-0.1.1/src/data_contract_registry/registry.py +166 -0
- data_contract_registry-0.1.1/tests/__init__.py +0 -0
- data_contract_registry-0.1.1/tests/conftest.py +24 -0
- data_contract_registry-0.1.1/tests/test_app.py +251 -0
- data_contract_registry-0.1.1/tests/test_audit_stream.py +140 -0
- data_contract_registry-0.1.1/tests/test_compatibility.py +134 -0
- data_contract_registry-0.1.1/tests/test_from_decision_card.py +52 -0
- data_contract_registry-0.1.1/tests/test_models.py +66 -0
- data_contract_registry-0.1.1/tests/test_registry.py +93 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: test (py${{ matrix.python-version }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
cache: pip
|
|
25
|
+
|
|
26
|
+
- name: Install
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: ruff check src tests
|
|
33
|
+
|
|
34
|
+
- name: Format check
|
|
35
|
+
run: ruff format --check src tests
|
|
36
|
+
|
|
37
|
+
- name: Type check
|
|
38
|
+
run: mypy src
|
|
39
|
+
|
|
40
|
+
- name: Test
|
|
41
|
+
run: pytest -v
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
name: Build + publish wheel + sdist
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/data-contract-registry
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
- name: Install build tools
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
python -m pip install build
|
|
28
|
+
- name: Build distributions
|
|
29
|
+
run: python -m build
|
|
30
|
+
- name: Inspect distributions
|
|
31
|
+
run: ls -la dist/
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
34
|
+
with:
|
|
35
|
+
attestations: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miz Causevic / Kinetic Gain
|
|
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,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: data-contract-registry
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Schema registry for data contracts: semver versioning, compatibility checks (backward/forward/full), ownership, freshness SLAs. The 'you can't promote it without an approved contract' pattern for data pipelines. Optional audit-stream-py integration via AUDIT_STREAM_URL.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mizcausevic-dev/data-contract-registry
|
|
6
|
+
Project-URL: Repository, https://github.com/mizcausevic-dev/data-contract-registry
|
|
7
|
+
Project-URL: Issues, https://github.com/mizcausevic-dev/data-contract-registry/issues
|
|
8
|
+
Project-URL: Author Site, https://kineticgain.com/
|
|
9
|
+
Author-email: Miz Causevic <miz@kineticgain.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: data-contract,data-quality,fastapi,kinetic-gain,schema-registry
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: pydantic>=2.7
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Provides-Extra: api
|
|
30
|
+
Requires-Dist: fastapi>=0.115; extra == 'api'
|
|
31
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'api'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: fastapi>=0.115; extra == 'dev'
|
|
34
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
35
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
39
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'dev'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# data-contract-registry
|
|
44
|
+
|
|
45
|
+
[](https://github.com/mizcausevic-dev/data-contract-registry/actions/workflows/ci.yml)
|
|
46
|
+
[](https://www.python.org/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
**Schema registry for data contracts.** Semver versioning, compatibility checks (backward / forward / full), declared owners, freshness SLAs. The "you can't promote a new dataset version without an approved contract" pattern, lifted from API governance and aimed at data pipelines.
|
|
50
|
+
|
|
51
|
+
The headline endpoint is `POST /contracts` — register a new version, get back a deterministic compatibility report or a 422 with every breaking change called out by field name and kind.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Why
|
|
56
|
+
|
|
57
|
+
The thing that gets data teams paged at 2am isn't a missing test. It's a producer who quietly removed `ltv` because "we never use it anymore" while three downstream dashboards still join on it. Schema registries (Confluent, Buf, etc.) solved this for streaming and gRPC; data pipelines need the same hardness in a shape that fits the things data teams actually argue about:
|
|
58
|
+
|
|
59
|
+
- **owners** — who do I page when this dataset goes stale
|
|
60
|
+
- **freshness SLA** — when does "stale" become "broken"
|
|
61
|
+
- **primary key** — changing it is a `MAJOR`, not a `MINOR`
|
|
62
|
+
- **enum drift** — adding a value is fine; removing one is a backward-compatibility break
|
|
63
|
+
- **deprecation policy** — flag a version with the URI of the migration plan; don't delete it
|
|
64
|
+
|
|
65
|
+
This package is the smallest thing that does all of those.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install data-contract-registry
|
|
73
|
+
# with the FastAPI surface:
|
|
74
|
+
pip install "data-contract-registry[api]"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Python 3.11+. Runtime deps: `pydantic` + `PyYAML`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Library quickstart
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from data_contract_registry import (
|
|
85
|
+
ContractRegistry,
|
|
86
|
+
DataContract,
|
|
87
|
+
DataField,
|
|
88
|
+
Owner,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
registry = ContractRegistry()
|
|
92
|
+
|
|
93
|
+
v1 = DataContract(
|
|
94
|
+
dataset_id="users.daily_active",
|
|
95
|
+
version="1.0.0",
|
|
96
|
+
primary_key=["user_id", "active_date"],
|
|
97
|
+
owners=[Owner(team="growth-platform", contact="#growth-platform")],
|
|
98
|
+
fields=[
|
|
99
|
+
DataField(name="user_id", type="string"),
|
|
100
|
+
DataField(name="active_date", type="timestamp"),
|
|
101
|
+
DataField(name="plan", type="string", enum=["free", "pro", "enterprise"]),
|
|
102
|
+
DataField(name="ltv", type="number", required=False),
|
|
103
|
+
],
|
|
104
|
+
status="active",
|
|
105
|
+
)
|
|
106
|
+
registry.register(v1)
|
|
107
|
+
|
|
108
|
+
# Compatible promotion (added an optional field).
|
|
109
|
+
v1_1 = v1.model_copy(update={
|
|
110
|
+
"version": "1.1.0",
|
|
111
|
+
"fields": [*v1.fields, DataField(name="signup_source", type="string", required=False)],
|
|
112
|
+
})
|
|
113
|
+
report = registry.register(v1_1)
|
|
114
|
+
print(report.compatible) # True
|
|
115
|
+
|
|
116
|
+
# Incompatible promotion — removing a field breaks backward compatibility.
|
|
117
|
+
v2 = v1.model_copy(update={"version": "2.0.0", "fields": [f for f in v1.fields if f.name != "ltv"]})
|
|
118
|
+
report = registry.register(v2)
|
|
119
|
+
print(report.compatible) # False
|
|
120
|
+
print(report.errors[0].kind) # "field_removed"
|
|
121
|
+
print(report.errors[0].message) # "field 'ltv' was removed; old data will fail validation"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Compatibility modes
|
|
127
|
+
|
|
128
|
+
| Mode | Meaning |
|
|
129
|
+
| ---------- | --- |
|
|
130
|
+
| `backward` | New schema can read data produced by the previous schema. **Default.** Consumers upgrade first. |
|
|
131
|
+
| `forward` | Previous schema can read data produced by the new schema. Producers upgrade first. |
|
|
132
|
+
| `full` | Both. |
|
|
133
|
+
| `none` | Anything goes. First-time onboarding only. |
|
|
134
|
+
|
|
135
|
+
The checks the engine knows how to flag (each carries a structured `kind` so you can build CI gates around specific failures):
|
|
136
|
+
|
|
137
|
+
| Kind | Severity | Mode |
|
|
138
|
+
| -------------------------- | -------- | --- |
|
|
139
|
+
| `field_removed` | error | backward |
|
|
140
|
+
| `field_type_changed` | error | backward |
|
|
141
|
+
| `field_required_added` | error | backward (optional→required) **or** forward (new required field) |
|
|
142
|
+
| `field_enum_shrunk` | error | backward |
|
|
143
|
+
| `primary_key_changed` | error | always |
|
|
144
|
+
| `version_not_increasing` | error | always |
|
|
145
|
+
| `owner_missing` | error | always |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## FastAPI surface
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install "data-contract-registry[api]"
|
|
153
|
+
uvicorn data_contract_registry.app:app --port 8090
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Method | Path | What it does |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| GET | `/` | Service info. |
|
|
159
|
+
| GET | `/healthz` | Liveness probe. |
|
|
160
|
+
| GET | `/datasets` | List registered dataset IDs. |
|
|
161
|
+
| POST | `/contracts` | Register / promote a contract. 422 with a structured issue list when incompatible. |
|
|
162
|
+
| POST | `/contracts/check` | Dry-run compatibility check — does **not** register. |
|
|
163
|
+
| GET | `/contracts/{ds}/latest` | Latest **active** contract for a dataset. |
|
|
164
|
+
| GET | `/contracts/{ds}/versions` | Full version history. |
|
|
165
|
+
| GET | `/contracts/{ds}/versions/{v}` | One specific version. |
|
|
166
|
+
| POST | `/contracts/{ds}/versions/{v}/deprecate` | Mark deprecated with a migration URI. |
|
|
167
|
+
| POST | `/contracts/{ds}/versions/{v}/archive` | Archive a version (history preserved). |
|
|
168
|
+
| POST | `/contracts/owners/from-decision-card` | **Cross-ecosystem hook** — pull owners out of a Decision Card. |
|
|
169
|
+
|
|
170
|
+
Bundles are held in-memory by default. For restart-safe storage, swap `_BundleStore`'s implementation; the protocol is small.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## The cross-ecosystem hook
|
|
175
|
+
|
|
176
|
+
The third hook in the portfolio (after `procurement-decision-api` → `policy-as-code-engine` and the Suite → Decision Intelligence bridge). When a buyer approves a vendor whose data product the team will consume, the Decision Card's `buyer.name` + `decision_maker` are **the right answer** to "who owns the contract on our side":
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
curl -X POST http://localhost:8090/contracts/owners/from-decision-card \
|
|
180
|
+
-H 'Content-Type: application/json' \
|
|
181
|
+
-d @decision-card.json
|
|
182
|
+
# -> [
|
|
183
|
+
# {"team": "Springfield USD", "contact": "#data-platform"},
|
|
184
|
+
# {"team": "Director of Data (Alex Chen)", "contact": null}
|
|
185
|
+
# ]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Drop that list straight into `DataContract.owners` and the registration carries paging info the team didn't have to re-type.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## YAML authoring
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
# contracts/users-daily-active.yaml
|
|
196
|
+
dataset_id: users.daily_active
|
|
197
|
+
version: "1.0.0"
|
|
198
|
+
owners:
|
|
199
|
+
- team: growth-platform
|
|
200
|
+
contact: "#growth-platform"
|
|
201
|
+
freshness_sla:
|
|
202
|
+
max_lag_seconds: 86400
|
|
203
|
+
fields:
|
|
204
|
+
- {name: user_id, type: string}
|
|
205
|
+
- {name: active_date, type: timestamp}
|
|
206
|
+
- {name: plan, type: string, enum: [free, pro, enterprise]}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Hand-author in YAML, validate in CI, register from Python:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import yaml
|
|
213
|
+
from pathlib import Path
|
|
214
|
+
from data_contract_registry import ContractRegistry, DataContract
|
|
215
|
+
|
|
216
|
+
raw = yaml.safe_load(Path("contracts/users-daily-active.yaml").read_text())
|
|
217
|
+
ContractRegistry().register(DataContract.model_validate(raw))
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Tests
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install -e ".[dev]"
|
|
226
|
+
ruff check src tests && ruff format --check src tests
|
|
227
|
+
mypy src
|
|
228
|
+
pytest -v
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
CI matrix runs Python 3.11 / 3.12 / 3.13.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Related in this ecosystem
|
|
236
|
+
|
|
237
|
+
- **[procurement-decision-api](https://github.com/mizcausevic-dev/procurement-decision-api)** — drafts the Decision Cards this registry pulls owners from.
|
|
238
|
+
- **[policy-as-code-engine](https://github.com/mizcausevic-dev/policy-as-code-engine)** — pair with this registry to enforce contracts at request time.
|
|
239
|
+
- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — wire your freshness SLA into the same monitoring story.
|
|
240
|
+
- More at [kineticgain.com](https://kineticgain.com/).
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# data-contract-registry
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mizcausevic-dev/data-contract-registry/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Schema registry for data contracts.** Semver versioning, compatibility checks (backward / forward / full), declared owners, freshness SLAs. The "you can't promote a new dataset version without an approved contract" pattern, lifted from API governance and aimed at data pipelines.
|
|
8
|
+
|
|
9
|
+
The headline endpoint is `POST /contracts` — register a new version, get back a deterministic compatibility report or a 422 with every breaking change called out by field name and kind.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
The thing that gets data teams paged at 2am isn't a missing test. It's a producer who quietly removed `ltv` because "we never use it anymore" while three downstream dashboards still join on it. Schema registries (Confluent, Buf, etc.) solved this for streaming and gRPC; data pipelines need the same hardness in a shape that fits the things data teams actually argue about:
|
|
16
|
+
|
|
17
|
+
- **owners** — who do I page when this dataset goes stale
|
|
18
|
+
- **freshness SLA** — when does "stale" become "broken"
|
|
19
|
+
- **primary key** — changing it is a `MAJOR`, not a `MINOR`
|
|
20
|
+
- **enum drift** — adding a value is fine; removing one is a backward-compatibility break
|
|
21
|
+
- **deprecation policy** — flag a version with the URI of the migration plan; don't delete it
|
|
22
|
+
|
|
23
|
+
This package is the smallest thing that does all of those.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install data-contract-registry
|
|
31
|
+
# with the FastAPI surface:
|
|
32
|
+
pip install "data-contract-registry[api]"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Python 3.11+. Runtime deps: `pydantic` + `PyYAML`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Library quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from data_contract_registry import (
|
|
43
|
+
ContractRegistry,
|
|
44
|
+
DataContract,
|
|
45
|
+
DataField,
|
|
46
|
+
Owner,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
registry = ContractRegistry()
|
|
50
|
+
|
|
51
|
+
v1 = DataContract(
|
|
52
|
+
dataset_id="users.daily_active",
|
|
53
|
+
version="1.0.0",
|
|
54
|
+
primary_key=["user_id", "active_date"],
|
|
55
|
+
owners=[Owner(team="growth-platform", contact="#growth-platform")],
|
|
56
|
+
fields=[
|
|
57
|
+
DataField(name="user_id", type="string"),
|
|
58
|
+
DataField(name="active_date", type="timestamp"),
|
|
59
|
+
DataField(name="plan", type="string", enum=["free", "pro", "enterprise"]),
|
|
60
|
+
DataField(name="ltv", type="number", required=False),
|
|
61
|
+
],
|
|
62
|
+
status="active",
|
|
63
|
+
)
|
|
64
|
+
registry.register(v1)
|
|
65
|
+
|
|
66
|
+
# Compatible promotion (added an optional field).
|
|
67
|
+
v1_1 = v1.model_copy(update={
|
|
68
|
+
"version": "1.1.0",
|
|
69
|
+
"fields": [*v1.fields, DataField(name="signup_source", type="string", required=False)],
|
|
70
|
+
})
|
|
71
|
+
report = registry.register(v1_1)
|
|
72
|
+
print(report.compatible) # True
|
|
73
|
+
|
|
74
|
+
# Incompatible promotion — removing a field breaks backward compatibility.
|
|
75
|
+
v2 = v1.model_copy(update={"version": "2.0.0", "fields": [f for f in v1.fields if f.name != "ltv"]})
|
|
76
|
+
report = registry.register(v2)
|
|
77
|
+
print(report.compatible) # False
|
|
78
|
+
print(report.errors[0].kind) # "field_removed"
|
|
79
|
+
print(report.errors[0].message) # "field 'ltv' was removed; old data will fail validation"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Compatibility modes
|
|
85
|
+
|
|
86
|
+
| Mode | Meaning |
|
|
87
|
+
| ---------- | --- |
|
|
88
|
+
| `backward` | New schema can read data produced by the previous schema. **Default.** Consumers upgrade first. |
|
|
89
|
+
| `forward` | Previous schema can read data produced by the new schema. Producers upgrade first. |
|
|
90
|
+
| `full` | Both. |
|
|
91
|
+
| `none` | Anything goes. First-time onboarding only. |
|
|
92
|
+
|
|
93
|
+
The checks the engine knows how to flag (each carries a structured `kind` so you can build CI gates around specific failures):
|
|
94
|
+
|
|
95
|
+
| Kind | Severity | Mode |
|
|
96
|
+
| -------------------------- | -------- | --- |
|
|
97
|
+
| `field_removed` | error | backward |
|
|
98
|
+
| `field_type_changed` | error | backward |
|
|
99
|
+
| `field_required_added` | error | backward (optional→required) **or** forward (new required field) |
|
|
100
|
+
| `field_enum_shrunk` | error | backward |
|
|
101
|
+
| `primary_key_changed` | error | always |
|
|
102
|
+
| `version_not_increasing` | error | always |
|
|
103
|
+
| `owner_missing` | error | always |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## FastAPI surface
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install "data-contract-registry[api]"
|
|
111
|
+
uvicorn data_contract_registry.app:app --port 8090
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Method | Path | What it does |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| GET | `/` | Service info. |
|
|
117
|
+
| GET | `/healthz` | Liveness probe. |
|
|
118
|
+
| GET | `/datasets` | List registered dataset IDs. |
|
|
119
|
+
| POST | `/contracts` | Register / promote a contract. 422 with a structured issue list when incompatible. |
|
|
120
|
+
| POST | `/contracts/check` | Dry-run compatibility check — does **not** register. |
|
|
121
|
+
| GET | `/contracts/{ds}/latest` | Latest **active** contract for a dataset. |
|
|
122
|
+
| GET | `/contracts/{ds}/versions` | Full version history. |
|
|
123
|
+
| GET | `/contracts/{ds}/versions/{v}` | One specific version. |
|
|
124
|
+
| POST | `/contracts/{ds}/versions/{v}/deprecate` | Mark deprecated with a migration URI. |
|
|
125
|
+
| POST | `/contracts/{ds}/versions/{v}/archive` | Archive a version (history preserved). |
|
|
126
|
+
| POST | `/contracts/owners/from-decision-card` | **Cross-ecosystem hook** — pull owners out of a Decision Card. |
|
|
127
|
+
|
|
128
|
+
Bundles are held in-memory by default. For restart-safe storage, swap `_BundleStore`'s implementation; the protocol is small.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## The cross-ecosystem hook
|
|
133
|
+
|
|
134
|
+
The third hook in the portfolio (after `procurement-decision-api` → `policy-as-code-engine` and the Suite → Decision Intelligence bridge). When a buyer approves a vendor whose data product the team will consume, the Decision Card's `buyer.name` + `decision_maker` are **the right answer** to "who owns the contract on our side":
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
curl -X POST http://localhost:8090/contracts/owners/from-decision-card \
|
|
138
|
+
-H 'Content-Type: application/json' \
|
|
139
|
+
-d @decision-card.json
|
|
140
|
+
# -> [
|
|
141
|
+
# {"team": "Springfield USD", "contact": "#data-platform"},
|
|
142
|
+
# {"team": "Director of Data (Alex Chen)", "contact": null}
|
|
143
|
+
# ]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Drop that list straight into `DataContract.owners` and the registration carries paging info the team didn't have to re-type.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## YAML authoring
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
# contracts/users-daily-active.yaml
|
|
154
|
+
dataset_id: users.daily_active
|
|
155
|
+
version: "1.0.0"
|
|
156
|
+
owners:
|
|
157
|
+
- team: growth-platform
|
|
158
|
+
contact: "#growth-platform"
|
|
159
|
+
freshness_sla:
|
|
160
|
+
max_lag_seconds: 86400
|
|
161
|
+
fields:
|
|
162
|
+
- {name: user_id, type: string}
|
|
163
|
+
- {name: active_date, type: timestamp}
|
|
164
|
+
- {name: plan, type: string, enum: [free, pro, enterprise]}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Hand-author in YAML, validate in CI, register from Python:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import yaml
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
from data_contract_registry import ContractRegistry, DataContract
|
|
173
|
+
|
|
174
|
+
raw = yaml.safe_load(Path("contracts/users-daily-active.yaml").read_text())
|
|
175
|
+
ContractRegistry().register(DataContract.model_validate(raw))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Tests
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install -e ".[dev]"
|
|
184
|
+
ruff check src tests && ruff format --check src tests
|
|
185
|
+
mypy src
|
|
186
|
+
pytest -v
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
CI matrix runs Python 3.11 / 3.12 / 3.13.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Related in this ecosystem
|
|
194
|
+
|
|
195
|
+
- **[procurement-decision-api](https://github.com/mizcausevic-dev/procurement-decision-api)** — drafts the Decision Cards this registry pulls owners from.
|
|
196
|
+
- **[policy-as-code-engine](https://github.com/mizcausevic-dev/policy-as-code-engine)** — pair with this registry to enforce contracts at request time.
|
|
197
|
+
- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — wire your freshness SLA into the same monitoring story.
|
|
198
|
+
- More at [kineticgain.com](https://kineticgain.com/).
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
dataset_id: users.daily_active
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: "One row per (user, day) for active users on the consumer plan."
|
|
4
|
+
status: active
|
|
5
|
+
primary_key:
|
|
6
|
+
- user_id
|
|
7
|
+
- active_date
|
|
8
|
+
owners:
|
|
9
|
+
- team: growth-platform
|
|
10
|
+
contact: "#growth-platform"
|
|
11
|
+
freshness_sla:
|
|
12
|
+
max_lag_seconds: 86400 # one day
|
|
13
|
+
measurement: event_time
|
|
14
|
+
fields:
|
|
15
|
+
- name: user_id
|
|
16
|
+
type: string
|
|
17
|
+
description: "Subject identifier — stable across signups."
|
|
18
|
+
- name: active_date
|
|
19
|
+
type: timestamp
|
|
20
|
+
description: "UTC midnight of the day the user was active."
|
|
21
|
+
- name: plan
|
|
22
|
+
type: string
|
|
23
|
+
enum: [free, pro, enterprise]
|
|
24
|
+
- name: country
|
|
25
|
+
type: string
|
|
26
|
+
- name: ltv
|
|
27
|
+
type: number
|
|
28
|
+
required: false
|
|
29
|
+
- name: session_count
|
|
30
|
+
type: integer
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "data-contract-registry"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Schema registry for data contracts: semver versioning, compatibility checks (backward/forward/full), ownership, freshness SLAs. The 'you can't promote it without an approved contract' pattern for data pipelines. Optional audit-stream-py integration via AUDIT_STREAM_URL."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Miz Causevic", email = "miz@kineticgain.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["data-contract", "schema-registry", "data-quality", "fastapi", "kinetic-gain"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Framework :: FastAPI",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Database",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"httpx>=0.27",
|
|
32
|
+
"pydantic>=2.7",
|
|
33
|
+
"PyYAML>=6.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
api = [
|
|
38
|
+
"fastapi>=0.115",
|
|
39
|
+
"uvicorn[standard]>=0.30",
|
|
40
|
+
]
|
|
41
|
+
dev = [
|
|
42
|
+
"fastapi>=0.115",
|
|
43
|
+
"uvicorn[standard]>=0.30",
|
|
44
|
+
"httpx>=0.27",
|
|
45
|
+
"pytest>=8.2",
|
|
46
|
+
"pytest-asyncio>=0.23",
|
|
47
|
+
"ruff>=0.6",
|
|
48
|
+
"mypy>=1.11",
|
|
49
|
+
"types-PyYAML>=6.0",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/mizcausevic-dev/data-contract-registry"
|
|
54
|
+
Repository = "https://github.com/mizcausevic-dev/data-contract-registry"
|
|
55
|
+
Issues = "https://github.com/mizcausevic-dev/data-contract-registry/issues"
|
|
56
|
+
"Author Site" = "https://kineticgain.com/"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.wheel]
|
|
59
|
+
packages = ["src/data_contract_registry"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
asyncio_mode = "auto"
|
|
64
|
+
filterwarnings = [
|
|
65
|
+
"ignore::DeprecationWarning:starlette.*",
|
|
66
|
+
"ignore::DeprecationWarning:fastapi.*",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
line-length = 110
|
|
71
|
+
target-version = "py311"
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint]
|
|
74
|
+
select = ["E", "F", "I", "B", "UP", "RUF"]
|
|
75
|
+
ignore = ["E501"]
|
|
76
|
+
|
|
77
|
+
[tool.mypy]
|
|
78
|
+
python_version = "3.11"
|
|
79
|
+
strict = true
|
|
80
|
+
disallow_untyped_defs = true
|
|
81
|
+
warn_unused_ignores = true
|
|
82
|
+
warn_redundant_casts = true
|
|
83
|
+
no_implicit_optional = true
|