rdce 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.
- rdce-0.1.0/LICENSE +21 -0
- rdce-0.1.0/PKG-INFO +116 -0
- rdce-0.1.0/README.md +96 -0
- rdce-0.1.0/pyproject.toml +38 -0
- rdce-0.1.0/rdce/__init__.py +3 -0
- rdce-0.1.0/rdce/enforcer.py +11 -0
- rdce-0.1.0/rdce/engine.py +61 -0
- rdce-0.1.0/rdce/extractor.py +27 -0
rdce-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valerio
|
|
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.
|
rdce-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rdce
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Runtime Data Contract Enforcer
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Valerio DAlessio
|
|
8
|
+
Author-email: valdal14@gmail.com
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# rdce
|
|
21
|
+
|
|
22
|
+
[](https://github.com/valdal14/rdce/actions/workflows/ci.yml)
|
|
23
|
+

|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
**Runtime Data Contract Enforcer**: A lightweight Python 3 library for recursively validating and diffing nested JSON payloads against explicit Pydantic schemas.
|
|
27
|
+
|
|
28
|
+
## Current Status
|
|
29
|
+
🚀 **v0.1.0 Complete** 🚀
|
|
30
|
+
The core recursive validation engine, Pydantic extractor, and public API are complete and fully tested with 100% coverage.
|
|
31
|
+
|
|
32
|
+
## 🌟 Features
|
|
33
|
+
* **Pydantic Native:** Define your data contracts using standard Pydantic `BaseModel` classes.
|
|
34
|
+
* **Recursive Type Validation:** Deeply inspects nested dictionaries and payloads without flattening them.
|
|
35
|
+
* **Path Tracking:** Returns exact dot-notation breadcrumbs for schema drift (e.g., `user.address.zip_code`).
|
|
36
|
+
* **Zero Bloat:** Built to do one thing perfectly—diffing data schemas.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📦 Installation
|
|
41
|
+
|
|
42
|
+
*(Note: Pending PyPI release)*
|
|
43
|
+
```bash
|
|
44
|
+
pip install rdce
|
|
45
|
+
# Or using poetry
|
|
46
|
+
poetry add rdce
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 🚀 Quick Start
|
|
52
|
+
`rdce` is designed to be a transparent bridge between your Pydantic models and incoming, untrusted dictionary payloads.
|
|
53
|
+
|
|
54
|
+
### 1. Define your Contract
|
|
55
|
+
Use standard Pydantic models. Nested models are fully supported.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from pydantic import BaseModel
|
|
59
|
+
|
|
60
|
+
class Address(BaseModel):
|
|
61
|
+
city: str
|
|
62
|
+
zip_code: int
|
|
63
|
+
|
|
64
|
+
class UserContract(BaseModel):
|
|
65
|
+
username: str
|
|
66
|
+
is_active: bool
|
|
67
|
+
address: Address
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Enforce the Payload
|
|
71
|
+
Pass the model class and your raw dictionary payload into the `enforce_contract` engine.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from rdce import enforce_contract
|
|
75
|
+
|
|
76
|
+
# A payload with schema drift (wrong type for zip_code, missing is_active)
|
|
77
|
+
incoming_payload = {
|
|
78
|
+
"username": "alice_data",
|
|
79
|
+
"address": {
|
|
80
|
+
"city": "London",
|
|
81
|
+
"zip_code": "E1 6AN" # Expected int, got string
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
errors = enforce_contract(UserContract, incoming_payload)
|
|
86
|
+
|
|
87
|
+
for error in errors:
|
|
88
|
+
print(error)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Output:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
[
|
|
95
|
+
{"path": "is_active", "expected": "bool", "actual": "MISSING"},
|
|
96
|
+
{"path": "address.zip_code", "expected": "int", "actual": "str"}
|
|
97
|
+
]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 🤝 Contributing
|
|
101
|
+
We welcome contributions! To set up the project locally:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
1 Clone the repository.
|
|
105
|
+
2 Initialize the environment: poetry install
|
|
106
|
+
3 We strictly enforce formatting and linting via Ruff:
|
|
107
|
+
4 Linter:
|
|
108
|
+
poetry run python3 -m ruff check .
|
|
109
|
+
5 Formatter:
|
|
110
|
+
poetry run python3 -m ruff format .
|
|
111
|
+
6 Run the test suite:
|
|
112
|
+
poetry run pytest
|
|
113
|
+
7 Ensure 100% test coverage before submitting a Pull Request.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
|
rdce-0.1.0/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# rdce
|
|
2
|
+
|
|
3
|
+
[](https://github.com/valdal14/rdce/actions/workflows/ci.yml)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Runtime Data Contract Enforcer**: A lightweight Python 3 library for recursively validating and diffing nested JSON payloads against explicit Pydantic schemas.
|
|
8
|
+
|
|
9
|
+
## Current Status
|
|
10
|
+
🚀 **v0.1.0 Complete** 🚀
|
|
11
|
+
The core recursive validation engine, Pydantic extractor, and public API are complete and fully tested with 100% coverage.
|
|
12
|
+
|
|
13
|
+
## 🌟 Features
|
|
14
|
+
* **Pydantic Native:** Define your data contracts using standard Pydantic `BaseModel` classes.
|
|
15
|
+
* **Recursive Type Validation:** Deeply inspects nested dictionaries and payloads without flattening them.
|
|
16
|
+
* **Path Tracking:** Returns exact dot-notation breadcrumbs for schema drift (e.g., `user.address.zip_code`).
|
|
17
|
+
* **Zero Bloat:** Built to do one thing perfectly—diffing data schemas.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
*(Note: Pending PyPI release)*
|
|
24
|
+
```bash
|
|
25
|
+
pip install rdce
|
|
26
|
+
# Or using poetry
|
|
27
|
+
poetry add rdce
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🚀 Quick Start
|
|
33
|
+
`rdce` is designed to be a transparent bridge between your Pydantic models and incoming, untrusted dictionary payloads.
|
|
34
|
+
|
|
35
|
+
### 1. Define your Contract
|
|
36
|
+
Use standard Pydantic models. Nested models are fully supported.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pydantic import BaseModel
|
|
40
|
+
|
|
41
|
+
class Address(BaseModel):
|
|
42
|
+
city: str
|
|
43
|
+
zip_code: int
|
|
44
|
+
|
|
45
|
+
class UserContract(BaseModel):
|
|
46
|
+
username: str
|
|
47
|
+
is_active: bool
|
|
48
|
+
address: Address
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Enforce the Payload
|
|
52
|
+
Pass the model class and your raw dictionary payload into the `enforce_contract` engine.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from rdce import enforce_contract
|
|
56
|
+
|
|
57
|
+
# A payload with schema drift (wrong type for zip_code, missing is_active)
|
|
58
|
+
incoming_payload = {
|
|
59
|
+
"username": "alice_data",
|
|
60
|
+
"address": {
|
|
61
|
+
"city": "London",
|
|
62
|
+
"zip_code": "E1 6AN" # Expected int, got string
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
errors = enforce_contract(UserContract, incoming_payload)
|
|
67
|
+
|
|
68
|
+
for error in errors:
|
|
69
|
+
print(error)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
[
|
|
76
|
+
{"path": "is_active", "expected": "bool", "actual": "MISSING"},
|
|
77
|
+
{"path": "address.zip_code", "expected": "int", "actual": "str"}
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 🤝 Contributing
|
|
82
|
+
We welcome contributions! To set up the project locally:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
1 Clone the repository.
|
|
86
|
+
2 Initialize the environment: poetry install
|
|
87
|
+
3 We strictly enforce formatting and linting via Ruff:
|
|
88
|
+
4 Linter:
|
|
89
|
+
poetry run python3 -m ruff check .
|
|
90
|
+
5 Formatter:
|
|
91
|
+
poetry run python3 -m ruff format .
|
|
92
|
+
6 Run the test suite:
|
|
93
|
+
poetry run pytest
|
|
94
|
+
7 Ensure 100% test coverage before submitting a Pull Request.
|
|
95
|
+
```
|
|
96
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rdce"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Runtime Data Contract Enforcer"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Valerio DAlessio",email = "valdal14@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pydantic (>=2.12.5,<3.0.0)"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"ruff (>=0.15.7,<0.16.0)",
|
|
23
|
+
"pytest (>=9.0.2,<10.0.0)",
|
|
24
|
+
"pytest-asyncio (>=1.3.0,<2.0.0)",
|
|
25
|
+
"pytest-cov (>=7.1.0,<8.0.0)"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 100
|
|
30
|
+
target-version = "py310"
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["E", "F", "I"]
|
|
34
|
+
ignore = ["E501"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff.format]
|
|
37
|
+
quote-style = "double"
|
|
38
|
+
indent-style = "space"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .engine import compare_payload
|
|
6
|
+
from .extractor import extract_schema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def enforce_contract(contract: type[BaseModel], payload: dict[str, Any]) -> list[dict[str, str]]:
|
|
10
|
+
schema = extract_schema(contract)
|
|
11
|
+
return compare_payload(schema, payload)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compare_payload(
|
|
5
|
+
schema: dict[str, Any], payload: dict[str, Any], current_path: str = ""
|
|
6
|
+
) -> list[dict[str, str]]:
|
|
7
|
+
"""
|
|
8
|
+
Recursively compares a payload dictionary against an expected schema dictionary.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
schema (dict[str, Any]): The expected data contract defining fields and types.
|
|
12
|
+
payload (dict[str, Any]): The actual incoming data payload.
|
|
13
|
+
current_path (str, optional): The current path traversal state. Defaults to "".
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
list[dict[str, str]]: A list of validation errors. Returns an empty list if perfectly matched.
|
|
17
|
+
"""
|
|
18
|
+
errors = []
|
|
19
|
+
|
|
20
|
+
for key, expected_type in schema.items():
|
|
21
|
+
if current_path == "":
|
|
22
|
+
path = key
|
|
23
|
+
else:
|
|
24
|
+
path = f"{current_path}.{key}"
|
|
25
|
+
|
|
26
|
+
if key not in payload:
|
|
27
|
+
# Log the missing key error
|
|
28
|
+
errors.append(_build_error(path, str(expected_type), "MISSING"))
|
|
29
|
+
# skip to the next key in the schema
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
actual_value = payload[key]
|
|
33
|
+
|
|
34
|
+
# Check if this is a branch (The schema expects a dictionary)
|
|
35
|
+
if isinstance(expected_type, dict):
|
|
36
|
+
res = compare_payload(expected_type, actual_value, path)
|
|
37
|
+
errors.extend(res)
|
|
38
|
+
else:
|
|
39
|
+
# Get the actual type of the payload's value as a string
|
|
40
|
+
actual_type_string = type(actual_value).__name__
|
|
41
|
+
|
|
42
|
+
# Compare it to what the schema asked for
|
|
43
|
+
if actual_type_string != expected_type:
|
|
44
|
+
errors.append(_build_error(path, expected_type, actual_type_string))
|
|
45
|
+
|
|
46
|
+
return errors
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_error(path: str, expected_type: str, actual: str) -> dict[str, str]:
|
|
50
|
+
"""
|
|
51
|
+
Constructs a standardized validation error dictionary.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
path (str): The dot-notation breadcrumb path of the failing field.
|
|
55
|
+
expected_type (str): The Python type expected by the schema.
|
|
56
|
+
actual (str): The actual type received, or "MISSING" if not found.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
dict[str, str]: The formatted error payload.
|
|
60
|
+
"""
|
|
61
|
+
return {"path": path, "expected": expected_type, "actual": actual}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_schema(model: type[BaseModel]) -> dict[str, Any]:
|
|
7
|
+
"""
|
|
8
|
+
Translates a Pydantic model into a raw schema dictionary.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
model (type[BaseModel]): The Pydantic class to inspect.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
dict[str, Any]: A nested dictionary representing the expected types.
|
|
15
|
+
"""
|
|
16
|
+
schema = {}
|
|
17
|
+
|
|
18
|
+
for field_name, field_info in model.model_fields.items():
|
|
19
|
+
field_type = field_info.annotation
|
|
20
|
+
|
|
21
|
+
# Check if it is another Pydantic model
|
|
22
|
+
if isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
|
23
|
+
schema[field_name] = extract_schema(field_type)
|
|
24
|
+
else:
|
|
25
|
+
schema[field_name] = field_type.__name__
|
|
26
|
+
|
|
27
|
+
return schema
|