pycharter 0.0.25__py3-none-any.whl → 0.0.26__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.
- pycharter/__init__.py +6 -0
- pycharter/api/README.md +1 -1
- pycharter/api/dependencies/auth.py +158 -0
- pycharter/api/main.py +30 -2
- pycharter/api/models/etl.py +66 -0
- pycharter/api/routes/v1/__init__.py +4 -0
- pycharter/api/routes/v1/auth.py +97 -0
- pycharter/api/routes/v1/contracts.py +10 -8
- pycharter/api/routes/v1/etl.py +131 -0
- pycharter/cli.py +1 -1
- pycharter/config.py +69 -0
- pycharter/contract_builder/builder.py +32 -37
- pycharter/data/seed/compliance_frameworks.yaml +22 -0
- pycharter/data/seed/contracts.yaml +130 -0
- pycharter/data/seed/data_feeds.yaml +22 -0
- pycharter/data/seed/domains.yaml +13 -0
- pycharter/data/seed/environments.yaml +19 -0
- pycharter/data/seed/owners.yaml +21 -0
- pycharter/data/seed/systems.yaml +13 -0
- pycharter/data/seed/tags.yaml +25 -0
- pycharter/data/templates/contract/README.md +31 -14
- pycharter/data/templates/contract/template_contract.yaml +37 -0
- pycharter/data/templates/etl/README.md +1 -1
- pycharter/data/templates/etl/extract_with_validation.yaml +86 -0
- pycharter/data/templates/etl/load_with_validation.yaml +111 -0
- pycharter/data/templates/etl/settings.yaml +55 -0
- pycharter/db/cli.py +126 -4
- pycharter/db/migrations/versions/20260122000000_change_artifact_unique_constraints_to_title_version.py +2 -2
- pycharter/etl_generator/INTERFACES.md +6 -7
- pycharter/etl_generator/__init__.py +47 -11
- pycharter/etl_generator/config_models.py +673 -0
- pycharter/etl_generator/config_validator.py +133 -157
- pycharter/etl_generator/context.py +3 -0
- pycharter/etl_generator/database.py +5 -1
- pycharter/etl_generator/extractors/__init__.py +4 -2
- pycharter/etl_generator/extractors/cloud_storage.py +9 -9
- pycharter/etl_generator/extractors/database.py +2 -2
- pycharter/etl_generator/extractors/factory.py +15 -33
- pycharter/etl_generator/extractors/file.py +2 -2
- pycharter/etl_generator/extractors/http.py +2 -2
- pycharter/etl_generator/extractors/mongodb.py +393 -0
- pycharter/etl_generator/extractors/streaming.py +2 -2
- pycharter/etl_generator/loaders/__init__.py +15 -9
- pycharter/etl_generator/loaders/{cloud_storage_loader.py → cloud_storage.py} +95 -2
- pycharter/etl_generator/loaders/factory.py +16 -29
- pycharter/etl_generator/loaders/file.py +135 -1
- pycharter/etl_generator/loaders/mongodb.py +416 -0
- pycharter/etl_generator/pipeline.py +283 -164
- pycharter/etl_generator/result.py +16 -0
- pycharter/etl_generator/schemas/__init__.py +71 -42
- pycharter/etl_generator/transformers/config.py +3 -2
- pycharter/etl_generator/transformers/simple_operations.py +57 -4
- pycharter/etl_generator/validation.py +551 -0
- pycharter/runtime_validator/__init__.py +7 -0
- pycharter/runtime_validator/utils.py +33 -0
- pycharter/runtime_validator/validator.py +13 -10
- pycharter/ui/package-lock.json +50 -41
- pycharter/ui/package.json +2 -1
- pycharter/ui/static/404/index.html +1 -1
- pycharter/ui/static/404.html +1 -1
- pycharter/ui/static/__next.__PAGE__.txt +2 -2
- pycharter/ui/static/__next._full.txt +7 -7
- pycharter/ui/static/__next._head.txt +1 -1
- pycharter/ui/static/__next._index.txt +6 -6
- pycharter/ui/static/__next._tree.txt +2 -2
- pycharter/ui/static/_next/static/chunks/0fc1f70b787b8845.js +1 -0
- pycharter/ui/static/_next/static/chunks/17bb8075d7b75663.css +1 -0
- pycharter/ui/static/_next/static/chunks/381932864dcbfdb8.js +1 -0
- pycharter/ui/static/_next/static/chunks/4c951b8e4507e2b3.js +1 -0
- pycharter/ui/static/_next/static/chunks/68b87a6f65abd3ed.js +1 -0
- pycharter/ui/static/_next/static/chunks/78572617b8fae189.js +1 -0
- pycharter/ui/static/_next/static/chunks/8b7be2803e3fe184.js +1 -0
- pycharter/ui/static/_next/static/chunks/a8e529fd1e67f121.js +1 -0
- pycharter/ui/static/_next/static/chunks/c35d998f80be3ff5.js +1 -0
- pycharter/ui/static/_next/static/chunks/e453aa5d01c32c17.js +1 -0
- pycharter/ui/static/_next/static/chunks/f2d240eb057f898a.js +970 -0
- pycharter/ui/static/_next/static/chunks/f7722448f6040846.js +1 -0
- pycharter/ui/static/_not-found/__next._full.txt +12 -12
- pycharter/ui/static/_not-found/__next._head.txt +3 -3
- pycharter/ui/static/_not-found/__next._index.txt +8 -8
- pycharter/ui/static/_not-found/__next._not-found.__PAGE__.txt +2 -2
- pycharter/ui/static/_not-found/__next._not-found.txt +3 -3
- pycharter/ui/static/_not-found/__next._tree.txt +2 -2
- pycharter/ui/static/_not-found/index.html +1 -1
- pycharter/ui/static/_not-found/index.txt +12 -12
- pycharter/ui/static/contracts/__next._full.txt +7 -7
- pycharter/ui/static/contracts/__next._head.txt +1 -1
- pycharter/ui/static/contracts/__next._index.txt +6 -6
- pycharter/ui/static/contracts/__next._tree.txt +2 -2
- pycharter/ui/static/contracts/__next.contracts.__PAGE__.txt +2 -2
- pycharter/ui/static/contracts/__next.contracts.txt +1 -1
- pycharter/ui/static/contracts/index.html +1 -1
- pycharter/ui/static/contracts/index.txt +7 -7
- pycharter/ui/static/documentation/__next._full.txt +7 -7
- pycharter/ui/static/documentation/__next._head.txt +1 -1
- pycharter/ui/static/documentation/__next._index.txt +6 -6
- pycharter/ui/static/documentation/__next._tree.txt +2 -2
- pycharter/ui/static/documentation/__next.documentation.__PAGE__.txt +2 -2
- pycharter/ui/static/documentation/__next.documentation.txt +1 -1
- pycharter/ui/static/documentation/index.html +3 -3
- pycharter/ui/static/documentation/index.txt +7 -7
- pycharter/ui/static/etl/__next._full.txt +21 -0
- pycharter/ui/static/etl/__next._head.txt +7 -0
- pycharter/ui/static/etl/__next._index.txt +9 -0
- pycharter/ui/static/etl/__next._tree.txt +2 -0
- pycharter/ui/static/etl/__next.etl.__PAGE__.txt +9 -0
- pycharter/ui/static/etl/__next.etl.txt +4 -0
- pycharter/ui/static/etl/index.html +2 -0
- pycharter/ui/static/etl/index.txt +21 -0
- pycharter/ui/static/index.html +1 -1
- pycharter/ui/static/index.txt +7 -7
- pycharter/ui/static/metadata/__next._full.txt +7 -7
- pycharter/ui/static/metadata/__next._head.txt +1 -1
- pycharter/ui/static/metadata/__next._index.txt +6 -6
- pycharter/ui/static/metadata/__next._tree.txt +2 -2
- pycharter/ui/static/metadata/__next.metadata.__PAGE__.txt +2 -2
- pycharter/ui/static/metadata/__next.metadata.txt +1 -1
- pycharter/ui/static/metadata/index.html +1 -1
- pycharter/ui/static/metadata/index.txt +7 -7
- pycharter/ui/static/quality/__next._full.txt +7 -7
- pycharter/ui/static/quality/__next._head.txt +1 -1
- pycharter/ui/static/quality/__next._index.txt +6 -6
- pycharter/ui/static/quality/__next._tree.txt +2 -2
- pycharter/ui/static/quality/__next.quality.__PAGE__.txt +2 -2
- pycharter/ui/static/quality/__next.quality.txt +1 -1
- pycharter/ui/static/quality/index.html +2 -2
- pycharter/ui/static/quality/index.txt +7 -7
- pycharter/ui/static/rules/__next._full.txt +7 -7
- pycharter/ui/static/rules/__next._head.txt +1 -1
- pycharter/ui/static/rules/__next._index.txt +6 -6
- pycharter/ui/static/rules/__next._tree.txt +2 -2
- pycharter/ui/static/rules/__next.rules.__PAGE__.txt +2 -2
- pycharter/ui/static/rules/__next.rules.txt +1 -1
- pycharter/ui/static/rules/index.html +1 -1
- pycharter/ui/static/rules/index.txt +7 -7
- pycharter/ui/static/schemas/__next._full.txt +7 -7
- pycharter/ui/static/schemas/__next._head.txt +1 -1
- pycharter/ui/static/schemas/__next._index.txt +6 -6
- pycharter/ui/static/schemas/__next._tree.txt +2 -2
- pycharter/ui/static/schemas/__next.schemas.__PAGE__.txt +2 -2
- pycharter/ui/static/schemas/__next.schemas.txt +1 -1
- pycharter/ui/static/schemas/index.html +1 -1
- pycharter/ui/static/schemas/index.txt +7 -7
- pycharter/ui/static/settings/__next._full.txt +7 -7
- pycharter/ui/static/settings/__next._head.txt +1 -1
- pycharter/ui/static/settings/__next._index.txt +6 -6
- pycharter/ui/static/settings/__next._tree.txt +2 -2
- pycharter/ui/static/settings/__next.settings.__PAGE__.txt +2 -2
- pycharter/ui/static/settings/__next.settings.txt +1 -1
- pycharter/ui/static/settings/index.html +1 -1
- pycharter/ui/static/settings/index.txt +7 -7
- pycharter/ui/static/static/404/index.html +1 -1
- pycharter/ui/static/static/404.html +1 -1
- pycharter/ui/static/static/__next.__PAGE__.txt +1 -1
- pycharter/ui/static/static/__next._full.txt +1 -1
- pycharter/ui/static/static/__next._head.txt +1 -1
- pycharter/ui/static/static/__next._index.txt +1 -1
- pycharter/ui/static/static/__next._tree.txt +1 -1
- pycharter/ui/static/static/_not-found/__next._full.txt +1 -1
- pycharter/ui/static/static/_not-found/__next._head.txt +1 -1
- pycharter/ui/static/static/_not-found/__next._index.txt +1 -1
- pycharter/ui/static/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
- pycharter/ui/static/static/_not-found/__next._not-found.txt +1 -1
- pycharter/ui/static/static/_not-found/__next._tree.txt +1 -1
- pycharter/ui/static/static/_not-found/index.html +1 -1
- pycharter/ui/static/static/_not-found/index.txt +1 -1
- pycharter/ui/static/static/contracts/__next._full.txt +2 -2
- pycharter/ui/static/static/contracts/__next._head.txt +1 -1
- pycharter/ui/static/static/contracts/__next._index.txt +1 -1
- pycharter/ui/static/static/contracts/__next._tree.txt +1 -1
- pycharter/ui/static/static/contracts/__next.contracts.__PAGE__.txt +2 -2
- pycharter/ui/static/static/contracts/__next.contracts.txt +1 -1
- pycharter/ui/static/static/contracts/index.html +1 -1
- pycharter/ui/static/static/contracts/index.txt +2 -2
- pycharter/ui/static/static/documentation/__next._full.txt +1 -1
- pycharter/ui/static/static/documentation/__next._head.txt +1 -1
- pycharter/ui/static/static/documentation/__next._index.txt +1 -1
- pycharter/ui/static/static/documentation/__next._tree.txt +1 -1
- pycharter/ui/static/static/documentation/__next.documentation.__PAGE__.txt +1 -1
- pycharter/ui/static/static/documentation/__next.documentation.txt +1 -1
- pycharter/ui/static/static/documentation/index.html +2 -2
- pycharter/ui/static/static/documentation/index.txt +1 -1
- pycharter/ui/static/static/index.html +1 -1
- pycharter/ui/static/static/index.txt +1 -1
- pycharter/ui/static/static/metadata/__next._full.txt +1 -1
- pycharter/ui/static/static/metadata/__next._head.txt +1 -1
- pycharter/ui/static/static/metadata/__next._index.txt +1 -1
- pycharter/ui/static/static/metadata/__next._tree.txt +1 -1
- pycharter/ui/static/static/metadata/__next.metadata.__PAGE__.txt +1 -1
- pycharter/ui/static/static/metadata/__next.metadata.txt +1 -1
- pycharter/ui/static/static/metadata/index.html +1 -1
- pycharter/ui/static/static/metadata/index.txt +1 -1
- pycharter/ui/static/static/quality/__next._full.txt +2 -2
- pycharter/ui/static/static/quality/__next._head.txt +1 -1
- pycharter/ui/static/static/quality/__next._index.txt +1 -1
- pycharter/ui/static/static/quality/__next._tree.txt +1 -1
- pycharter/ui/static/static/quality/__next.quality.__PAGE__.txt +2 -2
- pycharter/ui/static/static/quality/__next.quality.txt +1 -1
- pycharter/ui/static/static/quality/index.html +2 -2
- pycharter/ui/static/static/quality/index.txt +2 -2
- pycharter/ui/static/static/rules/__next._full.txt +1 -1
- pycharter/ui/static/static/rules/__next._head.txt +1 -1
- pycharter/ui/static/static/rules/__next._index.txt +1 -1
- pycharter/ui/static/static/rules/__next._tree.txt +1 -1
- pycharter/ui/static/static/rules/__next.rules.__PAGE__.txt +1 -1
- pycharter/ui/static/static/rules/__next.rules.txt +1 -1
- pycharter/ui/static/static/rules/index.html +1 -1
- pycharter/ui/static/static/rules/index.txt +1 -1
- pycharter/ui/static/static/schemas/__next._full.txt +1 -1
- pycharter/ui/static/static/schemas/__next._head.txt +1 -1
- pycharter/ui/static/static/schemas/__next._index.txt +1 -1
- pycharter/ui/static/static/schemas/__next._tree.txt +1 -1
- pycharter/ui/static/static/schemas/__next.schemas.__PAGE__.txt +1 -1
- pycharter/ui/static/static/schemas/__next.schemas.txt +1 -1
- pycharter/ui/static/static/schemas/index.html +1 -1
- pycharter/ui/static/static/schemas/index.txt +1 -1
- pycharter/ui/static/static/settings/__next._full.txt +1 -1
- pycharter/ui/static/static/settings/__next._head.txt +1 -1
- pycharter/ui/static/static/settings/__next._index.txt +1 -1
- pycharter/ui/static/static/settings/__next._tree.txt +1 -1
- pycharter/ui/static/static/settings/__next.settings.__PAGE__.txt +1 -1
- pycharter/ui/static/static/settings/__next.settings.txt +1 -1
- pycharter/ui/static/static/settings/index.html +1 -1
- pycharter/ui/static/static/settings/index.txt +1 -1
- pycharter/ui/static/static/static/404/index.html +1 -1
- pycharter/ui/static/static/static/404.html +1 -1
- pycharter/ui/static/static/static/__next.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/__next._full.txt +2 -2
- pycharter/ui/static/static/static/__next._head.txt +1 -1
- pycharter/ui/static/static/static/__next._index.txt +2 -2
- pycharter/ui/static/static/static/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/_next/static/chunks/f7d1a90dd75d2572.js +1 -0
- pycharter/ui/static/static/static/_not-found/__next._full.txt +2 -2
- pycharter/ui/static/static/static/_not-found/__next._head.txt +1 -1
- pycharter/ui/static/static/static/_not-found/__next._index.txt +2 -2
- pycharter/ui/static/static/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/_not-found/__next._not-found.txt +1 -1
- pycharter/ui/static/static/static/_not-found/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/_not-found/index.html +1 -1
- pycharter/ui/static/static/static/_not-found/index.txt +2 -2
- pycharter/ui/static/static/static/contracts/__next._full.txt +3 -3
- pycharter/ui/static/static/static/contracts/__next._head.txt +1 -1
- pycharter/ui/static/static/static/contracts/__next._index.txt +2 -2
- pycharter/ui/static/static/static/contracts/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/contracts/__next.contracts.__PAGE__.txt +2 -2
- pycharter/ui/static/static/static/contracts/__next.contracts.txt +1 -1
- pycharter/ui/static/static/static/contracts/index.html +1 -1
- pycharter/ui/static/static/static/contracts/index.txt +3 -3
- pycharter/ui/static/static/static/documentation/__next._full.txt +3 -3
- pycharter/ui/static/static/static/documentation/__next._head.txt +1 -1
- pycharter/ui/static/static/static/documentation/__next._index.txt +2 -2
- pycharter/ui/static/static/static/documentation/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/documentation/__next.documentation.__PAGE__.txt +2 -2
- pycharter/ui/static/static/static/documentation/__next.documentation.txt +1 -1
- pycharter/ui/static/static/static/documentation/index.html +2 -2
- pycharter/ui/static/static/static/documentation/index.txt +3 -3
- pycharter/ui/static/static/static/index.html +1 -1
- pycharter/ui/static/static/static/index.txt +2 -2
- pycharter/ui/static/static/static/metadata/__next._full.txt +2 -2
- pycharter/ui/static/static/static/metadata/__next._head.txt +1 -1
- pycharter/ui/static/static/static/metadata/__next._index.txt +2 -2
- pycharter/ui/static/static/static/metadata/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/metadata/__next.metadata.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/metadata/__next.metadata.txt +1 -1
- pycharter/ui/static/static/static/metadata/index.html +1 -1
- pycharter/ui/static/static/static/metadata/index.txt +2 -2
- pycharter/ui/static/static/static/quality/__next._full.txt +2 -2
- pycharter/ui/static/static/static/quality/__next._head.txt +1 -1
- pycharter/ui/static/static/static/quality/__next._index.txt +2 -2
- pycharter/ui/static/static/static/quality/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/quality/__next.quality.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/quality/__next.quality.txt +1 -1
- pycharter/ui/static/static/static/quality/index.html +2 -2
- pycharter/ui/static/static/static/quality/index.txt +2 -2
- pycharter/ui/static/static/static/rules/__next._full.txt +2 -2
- pycharter/ui/static/static/static/rules/__next._head.txt +1 -1
- pycharter/ui/static/static/static/rules/__next._index.txt +2 -2
- pycharter/ui/static/static/static/rules/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/rules/__next.rules.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/rules/__next.rules.txt +1 -1
- pycharter/ui/static/static/static/rules/index.html +1 -1
- pycharter/ui/static/static/static/rules/index.txt +2 -2
- pycharter/ui/static/static/static/schemas/__next._full.txt +2 -2
- pycharter/ui/static/static/static/schemas/__next._head.txt +1 -1
- pycharter/ui/static/static/static/schemas/__next._index.txt +2 -2
- pycharter/ui/static/static/static/schemas/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/schemas/__next.schemas.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/schemas/__next.schemas.txt +1 -1
- pycharter/ui/static/static/static/schemas/index.html +1 -1
- pycharter/ui/static/static/static/schemas/index.txt +2 -2
- pycharter/ui/static/static/static/settings/__next._full.txt +2 -2
- pycharter/ui/static/static/static/settings/__next._head.txt +1 -1
- pycharter/ui/static/static/static/settings/__next._index.txt +2 -2
- pycharter/ui/static/static/static/settings/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/settings/__next.settings.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/settings/__next.settings.txt +1 -1
- pycharter/ui/static/static/static/settings/index.html +1 -1
- pycharter/ui/static/static/static/settings/index.txt +2 -2
- pycharter/ui/static/static/static/static/.gitkeep +0 -0
- pycharter/ui/static/static/static/static/404/index.html +1 -0
- pycharter/ui/static/static/static/static/404.html +1 -0
- pycharter/ui/static/static/static/static/__next.__PAGE__.txt +10 -0
- pycharter/ui/static/static/static/static/__next._full.txt +30 -0
- pycharter/ui/static/static/static/static/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/222442f6da32302a.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/247eb132b7f7b574.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/297d55555b71baba.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/414e77373f8ff61c.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/652ad0aa26265c47.js +2 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/9c23f44fff36548a.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/b32a0963684b9933.js +4 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/db913959c675cea6.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/f2e7afeab1178138.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/ff1a16fafef87110.js +1 -0
- pycharter/ui/static/static/static/static/_next/static/chunks/turbopack-ffcb7ab6794027ef.js +3 -0
- pycharter/ui/static/static/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_buildManifest.js +11 -0
- pycharter/ui/static/static/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_clientMiddlewareManifest.json +1 -0
- pycharter/ui/static/static/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_ssgManifest.js +1 -0
- pycharter/ui/static/static/static/static/_not-found/__next._full.txt +17 -0
- pycharter/ui/static/static/static/static/_not-found/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/_not-found/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
- pycharter/ui/static/static/static/static/_not-found/__next._not-found.txt +4 -0
- pycharter/ui/static/static/static/static/_not-found/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/_not-found/index.html +1 -0
- pycharter/ui/static/static/static/static/_not-found/index.txt +17 -0
- pycharter/ui/static/static/static/static/contracts/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/contracts/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/contracts/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/contracts/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/contracts/__next.contracts.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/contracts/__next.contracts.txt +4 -0
- pycharter/ui/static/static/static/static/contracts/index.html +1 -0
- pycharter/ui/static/static/static/static/contracts/index.txt +21 -0
- pycharter/ui/static/static/static/static/documentation/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/documentation/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/documentation/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/documentation/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/documentation/__next.documentation.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/documentation/__next.documentation.txt +4 -0
- pycharter/ui/static/static/static/static/documentation/index.html +93 -0
- pycharter/ui/static/static/static/static/documentation/index.txt +21 -0
- pycharter/ui/static/static/static/static/index.html +1 -0
- pycharter/ui/static/static/static/static/index.txt +30 -0
- pycharter/ui/static/static/static/static/metadata/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/metadata/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/metadata/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/metadata/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/metadata/__next.metadata.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/metadata/__next.metadata.txt +4 -0
- pycharter/ui/static/static/static/static/metadata/index.html +1 -0
- pycharter/ui/static/static/static/static/metadata/index.txt +21 -0
- pycharter/ui/static/static/static/static/quality/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/quality/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/quality/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/quality/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/quality/__next.quality.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/quality/__next.quality.txt +4 -0
- pycharter/ui/static/static/static/static/quality/index.html +2 -0
- pycharter/ui/static/static/static/static/quality/index.txt +21 -0
- pycharter/ui/static/static/static/static/rules/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/rules/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/rules/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/rules/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/rules/__next.rules.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/rules/__next.rules.txt +4 -0
- pycharter/ui/static/static/static/static/rules/index.html +1 -0
- pycharter/ui/static/static/static/static/rules/index.txt +21 -0
- pycharter/ui/static/static/static/static/schemas/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/schemas/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/schemas/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/schemas/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/schemas/__next.schemas.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/schemas/__next.schemas.txt +4 -0
- pycharter/ui/static/static/static/static/schemas/index.html +1 -0
- pycharter/ui/static/static/static/static/schemas/index.txt +21 -0
- pycharter/ui/static/static/static/static/settings/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/settings/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/settings/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/settings/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/settings/__next.settings.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/settings/__next.settings.txt +4 -0
- pycharter/ui/static/static/static/static/settings/index.html +1 -0
- pycharter/ui/static/static/static/static/settings/index.txt +21 -0
- pycharter/ui/static/static/static/static/validation/__next._full.txt +21 -0
- pycharter/ui/static/static/static/static/validation/__next._head.txt +7 -0
- pycharter/ui/static/static/static/static/validation/__next._index.txt +9 -0
- pycharter/ui/static/static/static/static/validation/__next._tree.txt +2 -0
- pycharter/ui/static/static/static/static/validation/__next.validation.__PAGE__.txt +9 -0
- pycharter/ui/static/static/static/static/validation/__next.validation.txt +4 -0
- pycharter/ui/static/static/static/static/validation/index.html +1 -0
- pycharter/ui/static/static/static/static/validation/index.txt +21 -0
- pycharter/ui/static/static/static/validation/__next._full.txt +2 -2
- pycharter/ui/static/static/static/validation/__next._head.txt +1 -1
- pycharter/ui/static/static/static/validation/__next._index.txt +2 -2
- pycharter/ui/static/static/static/validation/__next._tree.txt +2 -2
- pycharter/ui/static/static/static/validation/__next.validation.__PAGE__.txt +1 -1
- pycharter/ui/static/static/static/validation/__next.validation.txt +1 -1
- pycharter/ui/static/static/static/validation/index.html +1 -1
- pycharter/ui/static/static/static/validation/index.txt +2 -2
- pycharter/ui/static/static/validation/__next._full.txt +2 -2
- pycharter/ui/static/static/validation/__next._head.txt +1 -1
- pycharter/ui/static/static/validation/__next._index.txt +1 -1
- pycharter/ui/static/static/validation/__next._tree.txt +1 -1
- pycharter/ui/static/static/validation/__next.validation.__PAGE__.txt +2 -2
- pycharter/ui/static/static/validation/__next.validation.txt +1 -1
- pycharter/ui/static/static/validation/index.html +1 -1
- pycharter/ui/static/static/validation/index.txt +2 -2
- pycharter/ui/static/validation/__next._full.txt +7 -7
- pycharter/ui/static/validation/__next._head.txt +1 -1
- pycharter/ui/static/validation/__next._index.txt +6 -6
- pycharter/ui/static/validation/__next._tree.txt +2 -2
- pycharter/ui/static/validation/__next.validation.__PAGE__.txt +2 -2
- pycharter/ui/static/validation/__next.validation.txt +1 -1
- pycharter/ui/static/validation/index.html +1 -1
- pycharter/ui/static/validation/index.txt +7 -7
- {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/METADATA +57 -26
- pycharter-0.0.26.dist-info/RECORD +702 -0
- pycharter/etl_generator/config_loader.py +0 -394
- pycharter/etl_generator/loaders/cloud.py +0 -87
- pycharter/etl_generator/loaders/file_loader.py +0 -130
- pycharter/etl_generator/schemas/extract.json +0 -234
- pycharter/etl_generator/schemas/load.json +0 -202
- pycharter/etl_generator/schemas/pipeline.json +0 -94
- pycharter/etl_generator/schemas/transform.json +0 -171
- pycharter-0.0.25.dist-info/RECORD +0 -572
- /pycharter/ui/static/_next/static/{2gKjNv6YvE6BcIdFthBLs → YCnlK66gA7FV5vvcixspB}/_buildManifest.js +0 -0
- /pycharter/ui/static/_next/static/{2gKjNv6YvE6BcIdFthBLs → YCnlK66gA7FV5vvcixspB}/_clientMiddlewareManifest.json +0 -0
- /pycharter/ui/static/_next/static/{2gKjNv6YvE6BcIdFthBLs → YCnlK66gA7FV5vvcixspB}/_ssgManifest.js +0 -0
- /pycharter/ui/static/static/_next/static/{0rYA78L88aUyD2Uh38hhX → 2gKjNv6YvE6BcIdFthBLs}/_buildManifest.js +0 -0
- /pycharter/ui/static/static/_next/static/{0rYA78L88aUyD2Uh38hhX → 2gKjNv6YvE6BcIdFthBLs}/_clientMiddlewareManifest.json +0 -0
- /pycharter/ui/static/static/_next/static/{0rYA78L88aUyD2Uh38hhX → 2gKjNv6YvE6BcIdFthBLs}/_ssgManifest.js +0 -0
- /pycharter/ui/static/{_next → static/_next}/static/chunks/26dfc590f7714c03.js +0 -0
- /pycharter/ui/static/{_next → static/_next}/static/chunks/34d289e6db2ef551.js +0 -0
- /pycharter/ui/static/{_next → static/_next}/static/chunks/99508d9d5869cc27.js +0 -0
- /pycharter/ui/static/{_next → static/_next}/static/chunks/b313c35a6ba76574.js +0 -0
- /pycharter/ui/static/static/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_buildManifest.js +0 -0
- /pycharter/ui/static/static/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_clientMiddlewareManifest.json +0 -0
- /pycharter/ui/static/static/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_ssgManifest.js +0 -0
- /pycharter/ui/static/{_next → static/static/_next}/static/chunks/13d4a0fbd74c1ee4.js +0 -0
- /pycharter/ui/static/{_next → static/static/_next}/static/chunks/2edb43b48432ac04.js +0 -0
- /pycharter/ui/static/static/{_next → static/_next}/static/chunks/c4fa4f4114b7c352.js +0 -0
- /pycharter/ui/static/{_next → static/static/_next}/static/chunks/d2363397e1b2bcab.css +0 -0
- /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/2ab439ce003cd691.js +0 -0
- /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/49ca65abd26ae49e.js +0 -0
- /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/4e310fe5005770a3.css +0 -0
- /pycharter/ui/static/static/{_next → static/static/_next}/static/chunks/5e04d10c4a7b58a3.js +0 -0
- /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/5fc14c00a2779dc5.js +0 -0
- /pycharter/ui/static/static/{_next → static/static/_next}/static/chunks/75d88a058d8ffaa6.js +0 -0
- /pycharter/ui/static/static/{_next → static/static/_next}/static/chunks/8c89634cf6bad76f.js +0 -0
- /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/9667e7a3d359eb39.js +0 -0
- /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/b584574fdc8ab13e.js +0 -0
- /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/c69f6cba366bd988.js +0 -0
- /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/d5989c94d3614b3a.js +0 -0
- /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/f061a4be97bfc3b3.js +0 -0
- {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/WHEEL +0 -0
- {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/entry_points.txt +0 -0
- {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/licenses/LICENSE +0 -0
- {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ETL Validation Module.
|
|
3
|
+
|
|
4
|
+
Provides validation for ETL pipelines:
|
|
5
|
+
- ETLValidator: Validates data against schemas/contracts with DLQ support
|
|
6
|
+
- resolve_contract: Resolves contract references (file or database)
|
|
7
|
+
- resolve_schema: Resolves schema file paths
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from pycharter.etl_generator.config_models import (
|
|
17
|
+
ContractRef,
|
|
18
|
+
DLQConfig,
|
|
19
|
+
DLQBackend,
|
|
20
|
+
ExtractValidationConfig,
|
|
21
|
+
LoadValidationConfig,
|
|
22
|
+
OnErrorAction,
|
|
23
|
+
)
|
|
24
|
+
from pycharter.etl_generator.dlq import DeadLetterQueue, DLQReason
|
|
25
|
+
from pycharter.runtime_validator import Validator
|
|
26
|
+
from pycharter.metadata_store import MetadataStoreClient
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ETLValidationError(Exception):
|
|
32
|
+
"""Raised when validation fails with on_error=fail."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
message: str,
|
|
37
|
+
invalid_count: int = 0,
|
|
38
|
+
errors: Optional[List[str]] = None,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.invalid_count = invalid_count
|
|
42
|
+
self.errors = errors or []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ETLValidator:
|
|
46
|
+
"""
|
|
47
|
+
Validates data in ETL pipelines with configurable error handling.
|
|
48
|
+
|
|
49
|
+
Supports:
|
|
50
|
+
- Source validation (after extract) against a schema file
|
|
51
|
+
- Target validation (before load) against a data contract
|
|
52
|
+
- Error handling modes: fail, warn, skip, quarantine
|
|
53
|
+
- DLQ integration for quarantining invalid records
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> validator = ETLValidator(
|
|
57
|
+
... schema_or_contract={"type": "object", "properties": {...}},
|
|
58
|
+
... on_error=OnErrorAction.QUARANTINE,
|
|
59
|
+
... dlq=dlq_instance,
|
|
60
|
+
... pipeline_name="orders_pipeline",
|
|
61
|
+
... stage="extract",
|
|
62
|
+
... )
|
|
63
|
+
>>> valid, invalid, error_count = await validator.validate(data)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
schema_or_contract: Dict[str, Any],
|
|
69
|
+
on_error: OnErrorAction = OnErrorAction.FAIL,
|
|
70
|
+
dlq: Optional[DeadLetterQueue] = None,
|
|
71
|
+
pipeline_name: str = "",
|
|
72
|
+
stage: str = "",
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Initialize the ETL validator.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
schema_or_contract: JSON Schema dict or complete contract dict
|
|
79
|
+
on_error: Action on validation error (fail, warn, skip, quarantine)
|
|
80
|
+
dlq: Optional DeadLetterQueue instance for quarantine mode
|
|
81
|
+
pipeline_name: Name of the pipeline (for DLQ metadata)
|
|
82
|
+
stage: ETL stage (extract or load) for DLQ metadata
|
|
83
|
+
"""
|
|
84
|
+
self.on_error = on_error
|
|
85
|
+
self.dlq = dlq
|
|
86
|
+
self.pipeline_name = pipeline_name
|
|
87
|
+
self.stage = stage
|
|
88
|
+
|
|
89
|
+
# Initialize the underlying Validator
|
|
90
|
+
# If it's a full contract (has 'schema' key), load it as contract_dict
|
|
91
|
+
# Otherwise treat it as a schema directly
|
|
92
|
+
if "schema" in schema_or_contract and isinstance(
|
|
93
|
+
schema_or_contract["schema"], dict
|
|
94
|
+
):
|
|
95
|
+
schema = schema_or_contract["schema"]
|
|
96
|
+
# Ensure schema has version (required by Validator)
|
|
97
|
+
if "version" not in schema:
|
|
98
|
+
schema["version"] = "1.0.0"
|
|
99
|
+
self._validator = Validator(contract_dict=schema_or_contract)
|
|
100
|
+
else:
|
|
101
|
+
# It's a schema, wrap it in contract format
|
|
102
|
+
# Ensure schema has version (required by Validator)
|
|
103
|
+
if "version" not in schema_or_contract:
|
|
104
|
+
schema_or_contract = {**schema_or_contract, "version": "1.0.0"}
|
|
105
|
+
self._validator = Validator(contract_dict={"schema": schema_or_contract})
|
|
106
|
+
|
|
107
|
+
async def validate(
|
|
108
|
+
self,
|
|
109
|
+
data: List[Dict[str, Any]],
|
|
110
|
+
run_id: str = "",
|
|
111
|
+
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], int]:
|
|
112
|
+
"""
|
|
113
|
+
Validate a batch of data records.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
data: List of data records to validate
|
|
117
|
+
run_id: Optional run identifier for DLQ metadata
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Tuple of (valid_records, quarantined_records, error_count)
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ETLValidationError: If on_error=fail and validation fails
|
|
124
|
+
"""
|
|
125
|
+
if not data:
|
|
126
|
+
return [], [], 0
|
|
127
|
+
|
|
128
|
+
valid_records: List[Dict[str, Any]] = []
|
|
129
|
+
invalid_records: List[Dict[str, Any]] = []
|
|
130
|
+
all_errors: List[str] = []
|
|
131
|
+
|
|
132
|
+
# Validate each record
|
|
133
|
+
results = self._validator.validate_batch(data, strict=False)
|
|
134
|
+
|
|
135
|
+
for record, result in zip(data, results):
|
|
136
|
+
if result.is_valid:
|
|
137
|
+
# Return the original dict, not the Pydantic model
|
|
138
|
+
valid_records.append(record)
|
|
139
|
+
else:
|
|
140
|
+
invalid_records.append({
|
|
141
|
+
"record": record,
|
|
142
|
+
"errors": result.errors,
|
|
143
|
+
})
|
|
144
|
+
all_errors.extend(result.errors)
|
|
145
|
+
|
|
146
|
+
error_count = len(invalid_records)
|
|
147
|
+
|
|
148
|
+
# Handle based on on_error setting
|
|
149
|
+
if invalid_records:
|
|
150
|
+
if self.on_error == OnErrorAction.FAIL:
|
|
151
|
+
raise ETLValidationError(
|
|
152
|
+
f"Validation failed: {error_count} record(s) invalid",
|
|
153
|
+
invalid_count=error_count,
|
|
154
|
+
errors=all_errors[:10], # Limit errors in exception
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
elif self.on_error == OnErrorAction.WARN:
|
|
158
|
+
logger.warning(
|
|
159
|
+
f"[{self.pipeline_name}] Validation warning: "
|
|
160
|
+
f"{error_count} record(s) invalid in {self.stage} stage"
|
|
161
|
+
)
|
|
162
|
+
# Return all records (including invalid) for warn mode
|
|
163
|
+
return data, [], error_count
|
|
164
|
+
|
|
165
|
+
elif self.on_error == OnErrorAction.SKIP:
|
|
166
|
+
logger.info(
|
|
167
|
+
f"[{self.pipeline_name}] Skipping {error_count} invalid record(s) "
|
|
168
|
+
f"in {self.stage} stage"
|
|
169
|
+
)
|
|
170
|
+
# Return only valid records, no quarantine
|
|
171
|
+
return valid_records, [], error_count
|
|
172
|
+
|
|
173
|
+
elif self.on_error == OnErrorAction.QUARANTINE:
|
|
174
|
+
logger.info(
|
|
175
|
+
f"[{self.pipeline_name}] Quarantining {error_count} invalid record(s) "
|
|
176
|
+
f"in {self.stage} stage"
|
|
177
|
+
)
|
|
178
|
+
# Send to DLQ if configured
|
|
179
|
+
if self.dlq:
|
|
180
|
+
await self._send_to_dlq(invalid_records, run_id)
|
|
181
|
+
# Return valid records, quarantined records
|
|
182
|
+
return (
|
|
183
|
+
valid_records,
|
|
184
|
+
[item["record"] for item in invalid_records],
|
|
185
|
+
error_count,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return valid_records, [], 0
|
|
189
|
+
|
|
190
|
+
async def _send_to_dlq(
|
|
191
|
+
self,
|
|
192
|
+
invalid_records: List[Dict[str, Any]],
|
|
193
|
+
run_id: str = "",
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Send invalid records to the Dead Letter Queue."""
|
|
196
|
+
reason = (
|
|
197
|
+
DLQReason.SCHEMA_MISMATCH
|
|
198
|
+
if self.stage == "extract"
|
|
199
|
+
else DLQReason.VALIDATION_ERROR
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
for item in invalid_records:
|
|
203
|
+
await self.dlq.add_record(
|
|
204
|
+
pipeline_name=self.pipeline_name,
|
|
205
|
+
record_data=item["record"],
|
|
206
|
+
reason=reason,
|
|
207
|
+
error_message="; ".join(item["errors"][:5]), # Limit error message length
|
|
208
|
+
error_type="ValidationError",
|
|
209
|
+
stage=self.stage,
|
|
210
|
+
metadata={
|
|
211
|
+
"run_id": run_id,
|
|
212
|
+
"error_count": len(item["errors"]),
|
|
213
|
+
"validation_errors": item["errors"][:10], # Include detailed errors
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def resolve_schema(
|
|
219
|
+
schema_ref: str,
|
|
220
|
+
base_dir: Optional[Path] = None,
|
|
221
|
+
) -> Dict[str, Any]:
|
|
222
|
+
"""
|
|
223
|
+
Resolve a schema file path to a schema dict.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
schema_ref: Path to schema file (absolute or relative)
|
|
227
|
+
base_dir: Base directory for relative paths
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Schema dict
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
FileNotFoundError: If schema file not found
|
|
234
|
+
ValueError: If schema file is invalid
|
|
235
|
+
"""
|
|
236
|
+
schema_path = Path(schema_ref)
|
|
237
|
+
|
|
238
|
+
# Resolve relative paths
|
|
239
|
+
if not schema_path.is_absolute() and base_dir:
|
|
240
|
+
schema_path = base_dir / schema_path
|
|
241
|
+
|
|
242
|
+
if not schema_path.exists():
|
|
243
|
+
raise FileNotFoundError(f"Schema file not found: {schema_path}")
|
|
244
|
+
|
|
245
|
+
with open(schema_path) as f:
|
|
246
|
+
schema = yaml.safe_load(f)
|
|
247
|
+
|
|
248
|
+
if not isinstance(schema, dict):
|
|
249
|
+
raise ValueError(f"Invalid schema file: {schema_path}")
|
|
250
|
+
|
|
251
|
+
return schema
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def resolve_contract(
|
|
255
|
+
contract_ref: Union[str, ContractRef, Dict[str, Any]],
|
|
256
|
+
metadata_store: Optional[MetadataStoreClient] = None,
|
|
257
|
+
base_dir: Optional[Path] = None,
|
|
258
|
+
) -> Dict[str, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Resolve a contract reference to a complete contract dict.
|
|
261
|
+
|
|
262
|
+
Supports:
|
|
263
|
+
- String path: "contracts/orders/schema.yaml" (file path)
|
|
264
|
+
- ContractRef object: {"type": "database", "name": "orders", "version": "2.0"}
|
|
265
|
+
- Dict with 'name' key: {"name": "orders"} (uses metadata_store)
|
|
266
|
+
- Dict with 'type': file' and 'path' key
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
contract_ref: Contract reference (path, ContractRef, or dict)
|
|
270
|
+
metadata_store: Optional MetadataStoreClient for database contracts
|
|
271
|
+
base_dir: Base directory for relative file paths
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Complete contract dict with schema (and optionally coercion/validation rules)
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
FileNotFoundError: If file-based contract not found
|
|
278
|
+
ValueError: If contract reference is invalid or store not provided
|
|
279
|
+
"""
|
|
280
|
+
# Case 1: String path (file-based)
|
|
281
|
+
if isinstance(contract_ref, str):
|
|
282
|
+
return _load_contract_from_file(contract_ref, base_dir)
|
|
283
|
+
|
|
284
|
+
# Case 2: ContractRef Pydantic model
|
|
285
|
+
if isinstance(contract_ref, ContractRef):
|
|
286
|
+
if not metadata_store:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
"metadata_store required for database contract reference"
|
|
289
|
+
)
|
|
290
|
+
return _load_contract_from_store(
|
|
291
|
+
metadata_store, contract_ref.name, contract_ref.version
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Case 3: Dict
|
|
295
|
+
if isinstance(contract_ref, dict):
|
|
296
|
+
ref_type = contract_ref.get("type", "file")
|
|
297
|
+
|
|
298
|
+
if ref_type == "file":
|
|
299
|
+
path = contract_ref.get("path")
|
|
300
|
+
if not path:
|
|
301
|
+
raise ValueError("Contract reference missing 'path' field")
|
|
302
|
+
return _load_contract_from_file(path, base_dir)
|
|
303
|
+
|
|
304
|
+
elif ref_type == "database":
|
|
305
|
+
name = contract_ref.get("name")
|
|
306
|
+
if not name:
|
|
307
|
+
raise ValueError("Contract reference missing 'name' field")
|
|
308
|
+
if not metadata_store:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"metadata_store required for database contract reference"
|
|
311
|
+
)
|
|
312
|
+
return _load_contract_from_store(
|
|
313
|
+
metadata_store, name, contract_ref.get("version")
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Case 4: Dict with just 'name' (shorthand for database)
|
|
317
|
+
elif "name" in contract_ref:
|
|
318
|
+
if not metadata_store:
|
|
319
|
+
raise ValueError(
|
|
320
|
+
"metadata_store required for contract name reference"
|
|
321
|
+
)
|
|
322
|
+
return _load_contract_from_store(
|
|
323
|
+
metadata_store,
|
|
324
|
+
contract_ref["name"],
|
|
325
|
+
contract_ref.get("version"),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
else:
|
|
329
|
+
raise ValueError(f"Unknown contract reference type: {ref_type}")
|
|
330
|
+
|
|
331
|
+
raise ValueError(f"Invalid contract reference: {contract_ref}")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _load_contract_from_file(
|
|
335
|
+
path: str,
|
|
336
|
+
base_dir: Optional[Path] = None,
|
|
337
|
+
) -> Dict[str, Any]:
|
|
338
|
+
"""Load contract from file path."""
|
|
339
|
+
contract_path = Path(path)
|
|
340
|
+
|
|
341
|
+
# Resolve relative paths
|
|
342
|
+
if not contract_path.is_absolute() and base_dir:
|
|
343
|
+
contract_path = base_dir / contract_path
|
|
344
|
+
|
|
345
|
+
# Check if it's a directory (contract directory) or file
|
|
346
|
+
if contract_path.is_dir():
|
|
347
|
+
return _load_contract_from_directory(contract_path)
|
|
348
|
+
elif contract_path.exists():
|
|
349
|
+
return _load_contract_from_single_file(contract_path)
|
|
350
|
+
else:
|
|
351
|
+
# Try adding common extensions
|
|
352
|
+
for ext in (".yaml", ".yml", ".json"):
|
|
353
|
+
if contract_path.with_suffix(ext).exists():
|
|
354
|
+
return _load_contract_from_single_file(contract_path.with_suffix(ext))
|
|
355
|
+
|
|
356
|
+
raise FileNotFoundError(f"Contract not found: {contract_path}")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _load_contract_from_directory(directory: Path) -> Dict[str, Any]:
|
|
360
|
+
"""Load contract from directory with separate files."""
|
|
361
|
+
contract: Dict[str, Any] = {}
|
|
362
|
+
|
|
363
|
+
# Load schema (required)
|
|
364
|
+
schema_path = directory / "schema.yaml"
|
|
365
|
+
if not schema_path.exists():
|
|
366
|
+
schema_path = directory / "schema.yml"
|
|
367
|
+
if not schema_path.exists():
|
|
368
|
+
raise FileNotFoundError(f"Schema not found in contract directory: {directory}")
|
|
369
|
+
|
|
370
|
+
with open(schema_path) as f:
|
|
371
|
+
contract["schema"] = yaml.safe_load(f)
|
|
372
|
+
|
|
373
|
+
# Load coercion rules (optional)
|
|
374
|
+
for name in ("coercion_rules.yaml", "coercion_rules.yml", "coercion.yaml"):
|
|
375
|
+
coercion_path = directory / name
|
|
376
|
+
if coercion_path.exists():
|
|
377
|
+
with open(coercion_path) as f:
|
|
378
|
+
contract["coercion_rules"] = yaml.safe_load(f)
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
# Load validation rules (optional)
|
|
382
|
+
for name in ("validation_rules.yaml", "validation_rules.yml", "validation.yaml"):
|
|
383
|
+
validation_path = directory / name
|
|
384
|
+
if validation_path.exists():
|
|
385
|
+
with open(validation_path) as f:
|
|
386
|
+
contract["validation_rules"] = yaml.safe_load(f)
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
return contract
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _load_contract_from_single_file(path: Path) -> Dict[str, Any]:
|
|
393
|
+
"""Load contract from single file."""
|
|
394
|
+
with open(path) as f:
|
|
395
|
+
content = yaml.safe_load(f)
|
|
396
|
+
|
|
397
|
+
if not isinstance(content, dict):
|
|
398
|
+
raise ValueError(f"Invalid contract file: {path}")
|
|
399
|
+
|
|
400
|
+
# If it has a 'schema' key, it's a complete contract
|
|
401
|
+
if "schema" in content:
|
|
402
|
+
return content
|
|
403
|
+
|
|
404
|
+
# Otherwise treat the whole file as a schema
|
|
405
|
+
return {"schema": content}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _load_contract_from_store(
|
|
409
|
+
store: MetadataStoreClient,
|
|
410
|
+
name: str,
|
|
411
|
+
version: Optional[str] = None,
|
|
412
|
+
) -> Dict[str, Any]:
|
|
413
|
+
"""Load contract from metadata store.
|
|
414
|
+
|
|
415
|
+
Returns raw schema + separate rules. The Validator handles merging internally.
|
|
416
|
+
"""
|
|
417
|
+
# Get raw schema (not merged)
|
|
418
|
+
schema = store.get_schema(name, version)
|
|
419
|
+
|
|
420
|
+
if not schema:
|
|
421
|
+
raise ValueError(f"Contract not found in store: {name} (version: {version})")
|
|
422
|
+
|
|
423
|
+
# Get coercion and validation rules separately
|
|
424
|
+
coercion_rules = store.get_coercion_rules(name, version)
|
|
425
|
+
validation_rules = store.get_validation_rules(name, version)
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
"schema": schema,
|
|
429
|
+
"coercion_rules": coercion_rules or {},
|
|
430
|
+
"validation_rules": validation_rules or {},
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def create_dlq(
|
|
435
|
+
dlq_config: Optional[Union[bool, DLQConfig, Dict[str, Any]]],
|
|
436
|
+
default_config: Optional[DLQConfig] = None,
|
|
437
|
+
db_session: Optional[Any] = None,
|
|
438
|
+
) -> Optional[DeadLetterQueue]:
|
|
439
|
+
"""
|
|
440
|
+
Create a DeadLetterQueue from configuration.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
dlq_config: DLQ configuration (bool, DLQConfig, or dict)
|
|
444
|
+
default_config: Default DLQ config from settings
|
|
445
|
+
db_session: Database session for database backend
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
DeadLetterQueue instance or None if disabled
|
|
449
|
+
"""
|
|
450
|
+
# Handle bool shorthand
|
|
451
|
+
if dlq_config is True:
|
|
452
|
+
# Use default config if available
|
|
453
|
+
if default_config:
|
|
454
|
+
dlq_config = default_config
|
|
455
|
+
else:
|
|
456
|
+
# Create minimal file-based DLQ
|
|
457
|
+
dlq_config = DLQConfig(enabled=True, backend=DLQBackend.FILE, path="./dlq")
|
|
458
|
+
elif dlq_config is False or dlq_config is None:
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
# Convert dict to DLQConfig if needed
|
|
462
|
+
if isinstance(dlq_config, dict):
|
|
463
|
+
dlq_config = DLQConfig(**dlq_config)
|
|
464
|
+
|
|
465
|
+
if not dlq_config.enabled:
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
# Create DLQ based on backend
|
|
469
|
+
return DeadLetterQueue(
|
|
470
|
+
db_session=db_session if dlq_config.backend == DLQBackend.DATABASE else None,
|
|
471
|
+
storage_backend=dlq_config.backend.value,
|
|
472
|
+
storage_path=dlq_config.path,
|
|
473
|
+
enabled=dlq_config.enabled,
|
|
474
|
+
schema_name=dlq_config.schema_name,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def create_etl_validator(
|
|
479
|
+
validation_config: Union[ExtractValidationConfig, LoadValidationConfig, Dict[str, Any]],
|
|
480
|
+
pipeline_name: str,
|
|
481
|
+
stage: str,
|
|
482
|
+
metadata_store: Optional[MetadataStoreClient] = None,
|
|
483
|
+
default_contract: Optional[str] = None,
|
|
484
|
+
default_dlq_config: Optional[DLQConfig] = None,
|
|
485
|
+
base_dir: Optional[Path] = None,
|
|
486
|
+
db_session: Optional[Any] = None,
|
|
487
|
+
) -> Optional[ETLValidator]:
|
|
488
|
+
"""
|
|
489
|
+
Create an ETLValidator from validation configuration.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
validation_config: Validation config (ExtractValidationConfig, LoadValidationConfig, or dict)
|
|
493
|
+
pipeline_name: Pipeline name for DLQ metadata
|
|
494
|
+
stage: ETL stage ('extract' or 'load')
|
|
495
|
+
metadata_store: Optional metadata store for database contracts
|
|
496
|
+
default_contract: Default contract name from settings
|
|
497
|
+
default_dlq_config: Default DLQ config from settings
|
|
498
|
+
base_dir: Base directory for relative file paths
|
|
499
|
+
db_session: Database session for DLQ database backend
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
ETLValidator instance or None if validation not configured
|
|
503
|
+
"""
|
|
504
|
+
if validation_config is None:
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
# Convert dict to appropriate config type
|
|
508
|
+
if isinstance(validation_config, dict):
|
|
509
|
+
if stage == "extract":
|
|
510
|
+
validation_config = ExtractValidationConfig(**validation_config)
|
|
511
|
+
else:
|
|
512
|
+
validation_config = LoadValidationConfig(**validation_config)
|
|
513
|
+
|
|
514
|
+
# Resolve schema/contract
|
|
515
|
+
schema_or_contract: Optional[Dict[str, Any]] = None
|
|
516
|
+
|
|
517
|
+
if isinstance(validation_config, ExtractValidationConfig):
|
|
518
|
+
# Extract validation uses schema file
|
|
519
|
+
if validation_config.schema_path:
|
|
520
|
+
schema_or_contract = resolve_schema(validation_config.schema_path, base_dir)
|
|
521
|
+
else:
|
|
522
|
+
logger.warning(f"Extract validation config missing 'schema' field")
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
elif isinstance(validation_config, LoadValidationConfig):
|
|
526
|
+
# Load validation uses contract
|
|
527
|
+
if validation_config.contract:
|
|
528
|
+
schema_or_contract = resolve_contract(
|
|
529
|
+
validation_config.contract, metadata_store, base_dir
|
|
530
|
+
)
|
|
531
|
+
elif validation_config.use_contract and default_contract:
|
|
532
|
+
schema_or_contract = resolve_contract(
|
|
533
|
+
{"name": default_contract}, metadata_store, base_dir
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
logger.warning(f"Load validation config missing 'contract' field")
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
if schema_or_contract is None:
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
# Create DLQ if configured
|
|
543
|
+
dlq = create_dlq(validation_config.dlq, default_dlq_config, db_session)
|
|
544
|
+
|
|
545
|
+
return ETLValidator(
|
|
546
|
+
schema_or_contract=schema_or_contract,
|
|
547
|
+
on_error=validation_config.on_error,
|
|
548
|
+
dlq=dlq,
|
|
549
|
+
pipeline_name=pipeline_name,
|
|
550
|
+
stage=stage,
|
|
551
|
+
)
|
|
@@ -62,6 +62,10 @@ from pycharter.runtime_validator.validator import (
|
|
|
62
62
|
Validator,
|
|
63
63
|
create_validator,
|
|
64
64
|
)
|
|
65
|
+
from pycharter.runtime_validator.utils import (
|
|
66
|
+
merge_rules_into_schema,
|
|
67
|
+
get_merged_schema_from_contract,
|
|
68
|
+
)
|
|
65
69
|
|
|
66
70
|
__all__ = [
|
|
67
71
|
# PRIMARY INTERFACE: Validator and Builder
|
|
@@ -85,4 +89,7 @@ __all__ = [
|
|
|
85
89
|
"validate_input",
|
|
86
90
|
"validate_output",
|
|
87
91
|
"validate_with_contract_decorator",
|
|
92
|
+
# Schema utilities
|
|
93
|
+
"merge_rules_into_schema",
|
|
94
|
+
"get_merged_schema_from_contract",
|
|
88
95
|
]
|
|
@@ -69,3 +69,36 @@ def merge_rules_into_schema(
|
|
|
69
69
|
|
|
70
70
|
return complete_schema
|
|
71
71
|
|
|
72
|
+
|
|
73
|
+
def get_merged_schema_from_contract(contract: Dict[str, Any]) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Get merged schema from a contract dictionary.
|
|
76
|
+
|
|
77
|
+
This is a convenience function for cases where you need the merged schema
|
|
78
|
+
(e.g., for display, documentation generation, or legacy code).
|
|
79
|
+
|
|
80
|
+
For validation, prefer using the Validator class which handles merging
|
|
81
|
+
internally.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
contract: Contract dictionary with 'schema', 'coercion_rules', 'validation_rules' keys
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Schema dictionary with coercion and validation rules merged into properties
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> contract = build_contract(artifacts)
|
|
91
|
+
>>> merged = get_merged_schema_from_contract(contract)
|
|
92
|
+
>>> merged["properties"]["age"]["coercion"] # Rules are now in schema
|
|
93
|
+
'coerce_to_integer'
|
|
94
|
+
|
|
95
|
+
Note:
|
|
96
|
+
The returned schema is a deep copy - modifications won't affect the
|
|
97
|
+
original contract.
|
|
98
|
+
"""
|
|
99
|
+
schema = contract.get("schema", {})
|
|
100
|
+
coercion_rules = contract.get("coercion_rules", {})
|
|
101
|
+
validation_rules = contract.get("validation_rules", {})
|
|
102
|
+
|
|
103
|
+
return merge_rules_into_schema(schema, coercion_rules, validation_rules)
|
|
104
|
+
|
|
@@ -279,8 +279,7 @@ class Validator:
|
|
|
279
279
|
"""
|
|
280
280
|
Load contract from metadata store.
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
so we skip the merging step.
|
|
282
|
+
Fetches raw schema and rules separately, then merges internally.
|
|
284
283
|
|
|
285
284
|
Args:
|
|
286
285
|
store: MetadataStoreClient instance
|
|
@@ -290,17 +289,21 @@ class Validator:
|
|
|
290
289
|
Raises:
|
|
291
290
|
ValueError: If schema not found in store
|
|
292
291
|
"""
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
# Fetch raw schema (not merged)
|
|
293
|
+
raw_schema = store.get_schema(schema_id, version)
|
|
294
|
+
if not raw_schema:
|
|
295
295
|
raise ValueError(f"Schema '{schema_id}' not found in store" +
|
|
296
296
|
(f" (version: {version})" if version else ""))
|
|
297
297
|
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
self.
|
|
298
|
+
# Fetch rules separately
|
|
299
|
+
coercion_rules = store.get_coercion_rules(schema_id, version)
|
|
300
|
+
validation_rules = store.get_validation_rules(schema_id, version)
|
|
301
|
+
|
|
302
|
+
# Store raw components - merge will happen in _merge_rules_into_schema()
|
|
303
|
+
self.schema = raw_schema
|
|
304
|
+
self.coercion_rules = self._extract_rules(coercion_rules) if coercion_rules else {}
|
|
305
|
+
self.validation_rules = self._extract_rules(validation_rules) if validation_rules else {}
|
|
306
|
+
self._schema_from_store = False # Let internal merge happen
|
|
304
307
|
|
|
305
308
|
def _load_from_metadata(self, metadata: ContractMetadata) -> None:
|
|
306
309
|
"""
|