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 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
+ [![CI Pipeline](https://github.com/valdal14/rdce/actions/workflows/ci.yml/badge.svg)](https://github.com/valdal14/rdce/actions/workflows/ci.yml)
23
+ ![Python](https://img.shields.io/badge/Python-3.10%2B-blue?style=flat&logo=python)
24
+ ![License](https://img.shields.io/badge/License-MIT-green)
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
+ [![CI Pipeline](https://github.com/valdal14/rdce/actions/workflows/ci.yml/badge.svg)](https://github.com/valdal14/rdce/actions/workflows/ci.yml)
4
+ ![Python](https://img.shields.io/badge/Python-3.10%2B-blue?style=flat&logo=python)
5
+ ![License](https://img.shields.io/badge/License-MIT-green)
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,3 @@
1
+ from .enforcer import enforce_contract
2
+
3
+ __all__ = ["enforce_contract"]
@@ -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