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
pycharter/__init__.py
CHANGED
|
@@ -166,6 +166,9 @@ from pycharter.runtime_validator import (
|
|
|
166
166
|
validate_input,
|
|
167
167
|
validate_output,
|
|
168
168
|
validate_with_contract_decorator,
|
|
169
|
+
# Schema utilities
|
|
170
|
+
merge_rules_into_schema,
|
|
171
|
+
get_merged_schema_from_contract,
|
|
169
172
|
)
|
|
170
173
|
|
|
171
174
|
# ============================================================================
|
|
@@ -345,6 +348,9 @@ __all__ = [
|
|
|
345
348
|
"validate_input",
|
|
346
349
|
"validate_output",
|
|
347
350
|
"validate_with_contract_decorator",
|
|
351
|
+
# Schema utilities
|
|
352
|
+
"merge_rules_into_schema",
|
|
353
|
+
"get_merged_schema_from_contract",
|
|
348
354
|
# Quality
|
|
349
355
|
"QualityCheck",
|
|
350
356
|
"QualityCheckOptions",
|
pycharter/api/README.md
CHANGED
|
@@ -183,7 +183,7 @@ List all schemas stored in the metadata store.
|
|
|
183
183
|
Get a schema by ID (optional version parameter).
|
|
184
184
|
|
|
185
185
|
#### `GET /api/v1/metadata/schemas/{schema_id}/complete`
|
|
186
|
-
Get complete schema with coercion and validation rules merged.
|
|
186
|
+
Get complete schema with coercion and validation rules merged (for display/docs). For validation, use the Validator class, which merges rules internally.
|
|
187
187
|
|
|
188
188
|
#### `POST /api/v1/metadata/schemas`
|
|
189
189
|
Store a schema in the metadata store.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication dependency for API routes.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Local/dev: initial credentials from env or pycharter.cfg; JWT issued and verified in-process.
|
|
6
|
+
- Optional auth service: token introspect via HTTP (when auth_service_url + introspect_path set).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hmac
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
import jwt
|
|
14
|
+
from fastapi import Depends, HTTPException, status
|
|
15
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
16
|
+
|
|
17
|
+
from pycharter.config import (
|
|
18
|
+
get_auth_initial_credentials,
|
|
19
|
+
get_auth_jwt_secret,
|
|
20
|
+
get_auth_service_introspect_path,
|
|
21
|
+
get_auth_service_url,
|
|
22
|
+
is_auth_disabled,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# HTTPBearer returns the token from Authorization: Bearer <token>
|
|
26
|
+
security = HTTPBearer(auto_error=False)
|
|
27
|
+
|
|
28
|
+
# Default JWT expiry (seconds)
|
|
29
|
+
ACCESS_TOKEN_EXPIRE_SECONDS = 3600 # 1 hour
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _constant_time_compare(a: str, b: str) -> bool:
|
|
33
|
+
"""Constant-time string comparison to avoid timing attacks."""
|
|
34
|
+
return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def verify_initial_credentials(username: str, password: str) -> bool:
|
|
38
|
+
"""Verify username/password against initial credentials (env or cfg). Returns True if valid."""
|
|
39
|
+
creds = get_auth_initial_credentials()
|
|
40
|
+
if not creds:
|
|
41
|
+
return False
|
|
42
|
+
u, p = creds
|
|
43
|
+
return _constant_time_compare(username, u) and _constant_time_compare(password, p)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_access_token(username: str, expires_delta_seconds: int = ACCESS_TOKEN_EXPIRE_SECONDS) -> str:
|
|
47
|
+
"""Create a JWT access token for the given username."""
|
|
48
|
+
secret = get_auth_jwt_secret()
|
|
49
|
+
if not secret:
|
|
50
|
+
raise ValueError("JWT secret not configured (set PYCHARTER_AUTH_JWT_SECRET or auth.jwt_secret)")
|
|
51
|
+
payload = {
|
|
52
|
+
"sub": username,
|
|
53
|
+
"exp": int(time.time()) + expires_delta_seconds,
|
|
54
|
+
"iat": int(time.time()),
|
|
55
|
+
}
|
|
56
|
+
return jwt.encode(payload, secret, algorithm="HS256")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def verify_jwt(token: str) -> Optional[dict[str, Any]]:
|
|
60
|
+
"""Verify JWT and return payload (with 'sub' = username) or None if invalid."""
|
|
61
|
+
secret = get_auth_jwt_secret()
|
|
62
|
+
if not secret:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
payload = jwt.decode(token, secret, algorithms=["HS256"])
|
|
66
|
+
return payload
|
|
67
|
+
except jwt.PyJWTError:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def introspect_token(token: str) -> Optional[dict[str, Any]]:
|
|
72
|
+
"""Call auth service introspect endpoint. Return payload if active, else None."""
|
|
73
|
+
base = get_auth_service_url()
|
|
74
|
+
path = get_auth_service_introspect_path()
|
|
75
|
+
if not base or not path:
|
|
76
|
+
return None
|
|
77
|
+
url = f"{base.rstrip('/')}{path}"
|
|
78
|
+
try:
|
|
79
|
+
import httpx
|
|
80
|
+
async with httpx.AsyncClient() as client:
|
|
81
|
+
# Common pattern: POST with token in body or header
|
|
82
|
+
r = await client.post(
|
|
83
|
+
url,
|
|
84
|
+
data={"token": token},
|
|
85
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
86
|
+
timeout=5.0,
|
|
87
|
+
)
|
|
88
|
+
if r.status_code != 200:
|
|
89
|
+
return None
|
|
90
|
+
data = r.json()
|
|
91
|
+
# Typical introspect: {"active": true, "sub": "username", ...}
|
|
92
|
+
if not data.get("active", False):
|
|
93
|
+
return None
|
|
94
|
+
return data
|
|
95
|
+
except Exception:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def get_current_user(
|
|
100
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Resolve current user from Bearer token. Uses JWT verify or auth service introspect.
|
|
104
|
+
When auth is disabled, returns a dummy user so routes do not need to branch.
|
|
105
|
+
"""
|
|
106
|
+
if is_auth_disabled():
|
|
107
|
+
return {"username": "anonymous", "auth_disabled": True}
|
|
108
|
+
|
|
109
|
+
token = None
|
|
110
|
+
if credentials and credentials.credentials:
|
|
111
|
+
token = credentials.credentials
|
|
112
|
+
|
|
113
|
+
if not token:
|
|
114
|
+
raise HTTPException(
|
|
115
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
116
|
+
detail="Not authenticated",
|
|
117
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Prefer JWT verify (no HTTP); fall back to introspect if configured
|
|
121
|
+
payload = verify_jwt(token)
|
|
122
|
+
if payload is None and (get_auth_service_url() and get_auth_service_introspect_path()):
|
|
123
|
+
payload = await introspect_token(token)
|
|
124
|
+
|
|
125
|
+
if payload is None:
|
|
126
|
+
raise HTTPException(
|
|
127
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
128
|
+
detail="Invalid or expired token",
|
|
129
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
username = payload.get("sub") or payload.get("username")
|
|
133
|
+
if not username:
|
|
134
|
+
raise HTTPException(
|
|
135
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
136
|
+
detail="Invalid token payload",
|
|
137
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {"username": username}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def get_optional_user(
|
|
144
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
145
|
+
) -> Optional[dict[str, Any]]:
|
|
146
|
+
"""Like get_current_user but returns None instead of 401 when no/invalid token. Useful for public endpoints."""
|
|
147
|
+
if is_auth_disabled():
|
|
148
|
+
return {"username": "anonymous", "auth_disabled": True}
|
|
149
|
+
token = credentials.credentials if credentials and credentials.credentials else None
|
|
150
|
+
if not token:
|
|
151
|
+
return None
|
|
152
|
+
payload = verify_jwt(token)
|
|
153
|
+
if payload is None and (get_auth_service_url() and get_auth_service_introspect_path()):
|
|
154
|
+
payload = await introspect_token(token)
|
|
155
|
+
if payload is None:
|
|
156
|
+
return None
|
|
157
|
+
username = payload.get("sub") or payload.get("username")
|
|
158
|
+
return {"username": username} if username else None
|
pycharter/api/main.py
CHANGED
|
@@ -17,6 +17,7 @@ from pycharter import __version__ as pycharter_version
|
|
|
17
17
|
|
|
18
18
|
# Import routers from v1
|
|
19
19
|
from pycharter.api.routes.v1 import (
|
|
20
|
+
auth,
|
|
20
21
|
contracts,
|
|
21
22
|
metadata,
|
|
22
23
|
quality,
|
|
@@ -27,7 +28,10 @@ from pycharter.api.routes.v1 import (
|
|
|
27
28
|
docs,
|
|
28
29
|
tracking,
|
|
29
30
|
evolution,
|
|
31
|
+
etl,
|
|
30
32
|
)
|
|
33
|
+
from pycharter.api.dependencies.auth import get_current_user
|
|
34
|
+
from fastapi import Depends
|
|
31
35
|
|
|
32
36
|
# Try to import validation_jobs router (requires worker component)
|
|
33
37
|
try:
|
|
@@ -102,57 +106,80 @@ def create_application() -> FastAPI:
|
|
|
102
106
|
allow_headers=["*"],
|
|
103
107
|
)
|
|
104
108
|
|
|
105
|
-
#
|
|
106
|
-
|
|
109
|
+
# Auth router: no global auth dependency (login/logout public; /auth/me uses Depends in route)
|
|
110
|
+
app.include_router(
|
|
111
|
+
auth.router,
|
|
112
|
+
prefix=f"/api/{API_VERSION}",
|
|
113
|
+
tags=["Auth"],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Protected routers: require valid Bearer token (when auth enabled; when disabled, get_current_user returns dummy user)
|
|
117
|
+
_auth_dep = [Depends(get_current_user)]
|
|
107
118
|
app.include_router(
|
|
108
119
|
contracts.router,
|
|
109
120
|
prefix=f"/api/{API_VERSION}",
|
|
110
121
|
tags=["Contracts"],
|
|
122
|
+
dependencies=_auth_dep,
|
|
111
123
|
)
|
|
112
124
|
app.include_router(
|
|
113
125
|
metadata.router,
|
|
114
126
|
prefix=f"/api/{API_VERSION}",
|
|
115
127
|
tags=["Metadata"],
|
|
128
|
+
dependencies=_auth_dep,
|
|
116
129
|
)
|
|
117
130
|
app.include_router(
|
|
118
131
|
schemas.router,
|
|
119
132
|
prefix=f"/api/{API_VERSION}",
|
|
120
133
|
tags=["Schemas"],
|
|
134
|
+
dependencies=_auth_dep,
|
|
121
135
|
)
|
|
122
136
|
app.include_router(
|
|
123
137
|
validation.router,
|
|
124
138
|
prefix=f"/api/{API_VERSION}",
|
|
125
139
|
tags=["Validation"],
|
|
140
|
+
dependencies=_auth_dep,
|
|
126
141
|
)
|
|
127
142
|
app.include_router(
|
|
128
143
|
quality.router,
|
|
129
144
|
prefix=f"/api/{API_VERSION}",
|
|
130
145
|
tags=["Quality"],
|
|
146
|
+
dependencies=_auth_dep,
|
|
131
147
|
)
|
|
132
148
|
app.include_router(
|
|
133
149
|
templates.router,
|
|
134
150
|
prefix=f"/api/{API_VERSION}",
|
|
135
151
|
tags=["Templates"],
|
|
152
|
+
dependencies=_auth_dep,
|
|
153
|
+
)
|
|
154
|
+
app.include_router(
|
|
155
|
+
etl.router,
|
|
156
|
+
prefix=f"/api/{API_VERSION}",
|
|
157
|
+
tags=["ETL"],
|
|
158
|
+
dependencies=_auth_dep,
|
|
136
159
|
)
|
|
137
160
|
app.include_router(
|
|
138
161
|
settings.router,
|
|
139
162
|
prefix=f"/api/{API_VERSION}",
|
|
140
163
|
tags=["Settings"],
|
|
164
|
+
dependencies=_auth_dep,
|
|
141
165
|
)
|
|
142
166
|
app.include_router(
|
|
143
167
|
docs.router,
|
|
144
168
|
prefix=f"/api/{API_VERSION}",
|
|
145
169
|
tags=["Documentation"],
|
|
170
|
+
dependencies=_auth_dep,
|
|
146
171
|
)
|
|
147
172
|
app.include_router(
|
|
148
173
|
tracking.router,
|
|
149
174
|
prefix=f"/api/{API_VERSION}",
|
|
150
175
|
tags=["Quality Tracking"],
|
|
176
|
+
dependencies=_auth_dep,
|
|
151
177
|
)
|
|
152
178
|
app.include_router(
|
|
153
179
|
evolution.router,
|
|
154
180
|
prefix=f"/api/{API_VERSION}",
|
|
155
181
|
tags=["Schema Evolution"],
|
|
182
|
+
dependencies=_auth_dep,
|
|
156
183
|
)
|
|
157
184
|
|
|
158
185
|
# Include validation_jobs router if worker component is available
|
|
@@ -161,6 +188,7 @@ def create_application() -> FastAPI:
|
|
|
161
188
|
validation_jobs.router,
|
|
162
189
|
prefix=f"/api/{API_VERSION}",
|
|
163
190
|
tags=["Validation Jobs"],
|
|
191
|
+
dependencies=_auth_dep,
|
|
164
192
|
)
|
|
165
193
|
|
|
166
194
|
# Root endpoint
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request/Response models for ETL run endpoint.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EtlRunRequest(BaseModel):
|
|
11
|
+
"""Request model for running an ETL pipeline from YAML configs."""
|
|
12
|
+
|
|
13
|
+
extract_yaml: str = Field(..., description="Extract config as YAML string")
|
|
14
|
+
load_yaml: str = Field(..., description="Load config as YAML string")
|
|
15
|
+
transform_yaml: Optional[str] = Field(
|
|
16
|
+
default=None,
|
|
17
|
+
description="Optional transform config as YAML string",
|
|
18
|
+
)
|
|
19
|
+
variables: Optional[Dict[str, str]] = Field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Optional variables for ${VAR} substitution in configs",
|
|
22
|
+
)
|
|
23
|
+
dry_run: bool = Field(
|
|
24
|
+
default=False,
|
|
25
|
+
description="If True, extract and transform but do not load",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EtlRunResponse(BaseModel):
|
|
30
|
+
"""Response model for ETL pipeline run result (mirrors PipelineResult.to_dict())."""
|
|
31
|
+
|
|
32
|
+
success: bool = Field(..., description="Whether the pipeline run succeeded")
|
|
33
|
+
rows_extracted: int = Field(0, description="Number of rows extracted")
|
|
34
|
+
rows_transformed: int = Field(0, description="Number of rows transformed")
|
|
35
|
+
rows_loaded: int = Field(0, description="Number of rows loaded")
|
|
36
|
+
rows_failed: int = Field(0, description="Number of rows that failed to load")
|
|
37
|
+
# Validation tracking
|
|
38
|
+
rows_quarantined_extract: int = Field(
|
|
39
|
+
0,
|
|
40
|
+
description="Number of rows quarantined at extract stage due to validation",
|
|
41
|
+
)
|
|
42
|
+
rows_quarantined_load: int = Field(
|
|
43
|
+
0,
|
|
44
|
+
description="Number of rows quarantined at load stage due to validation",
|
|
45
|
+
)
|
|
46
|
+
total_quarantined: int = Field(
|
|
47
|
+
0,
|
|
48
|
+
description="Total rows quarantined at both stages",
|
|
49
|
+
)
|
|
50
|
+
validation_errors_extract: List[str] = Field(
|
|
51
|
+
default_factory=list,
|
|
52
|
+
description="Validation error messages from extract stage",
|
|
53
|
+
)
|
|
54
|
+
validation_errors_load: List[str] = Field(
|
|
55
|
+
default_factory=list,
|
|
56
|
+
description="Validation error messages from load stage",
|
|
57
|
+
)
|
|
58
|
+
# Timing and metadata
|
|
59
|
+
duration_seconds: Optional[float] = Field(
|
|
60
|
+
None,
|
|
61
|
+
description="Total run duration in seconds",
|
|
62
|
+
)
|
|
63
|
+
batches_processed: int = Field(0, description="Number of batches processed")
|
|
64
|
+
errors: List[str] = Field(default_factory=list, description="Error messages if any")
|
|
65
|
+
pipeline_name: Optional[str] = Field(None, description="Pipeline name if set")
|
|
66
|
+
run_id: Optional[str] = Field(None, description="Run identifier")
|
|
@@ -7,6 +7,7 @@ with the `/api/v1` prefix.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from pycharter.api.routes.v1 import (
|
|
10
|
+
auth,
|
|
10
11
|
contracts,
|
|
11
12
|
metadata,
|
|
12
13
|
quality,
|
|
@@ -16,10 +17,12 @@ from pycharter.api.routes.v1 import (
|
|
|
16
17
|
docs,
|
|
17
18
|
tracking,
|
|
18
19
|
evolution,
|
|
20
|
+
etl,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
# Export all routers for automatic inclusion in main.py
|
|
22
24
|
__all__ = [
|
|
25
|
+
"auth",
|
|
23
26
|
"contracts",
|
|
24
27
|
"metadata",
|
|
25
28
|
"quality",
|
|
@@ -29,5 +32,6 @@ __all__ = [
|
|
|
29
32
|
"docs",
|
|
30
33
|
"tracking",
|
|
31
34
|
"evolution",
|
|
35
|
+
"etl",
|
|
32
36
|
]
|
|
33
37
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication routes: login, logout, me.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from pycharter.api.dependencies.auth import (
|
|
9
|
+
create_access_token,
|
|
10
|
+
get_current_user,
|
|
11
|
+
verify_initial_credentials,
|
|
12
|
+
ACCESS_TOKEN_EXPIRE_SECONDS,
|
|
13
|
+
)
|
|
14
|
+
from pycharter.config import (
|
|
15
|
+
get_auth_initial_credentials,
|
|
16
|
+
get_auth_jwt_secret,
|
|
17
|
+
is_auth_disabled,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LoginRequest(BaseModel):
|
|
24
|
+
username: str
|
|
25
|
+
password: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LoginResponse(BaseModel):
|
|
29
|
+
access_token: str
|
|
30
|
+
token_type: str = "bearer"
|
|
31
|
+
expires_in: int = ACCESS_TOKEN_EXPIRE_SECONDS
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UserResponse(BaseModel):
|
|
35
|
+
username: str
|
|
36
|
+
auth_disabled: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.post(
|
|
40
|
+
"/auth/login",
|
|
41
|
+
response_model=LoginResponse,
|
|
42
|
+
status_code=status.HTTP_200_OK,
|
|
43
|
+
summary="Login",
|
|
44
|
+
description="Exchange username and password for an access token. Uses initial credentials (env or pycharter.cfg) when configured.",
|
|
45
|
+
)
|
|
46
|
+
def login(request: LoginRequest) -> LoginResponse:
|
|
47
|
+
if is_auth_disabled():
|
|
48
|
+
raise HTTPException(
|
|
49
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
50
|
+
detail="Authentication is disabled",
|
|
51
|
+
)
|
|
52
|
+
creds = get_auth_initial_credentials()
|
|
53
|
+
if not creds:
|
|
54
|
+
raise HTTPException(
|
|
55
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
56
|
+
detail="Authentication not configured (set initial credentials or auth service)",
|
|
57
|
+
)
|
|
58
|
+
if not get_auth_jwt_secret():
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
61
|
+
detail="JWT secret not configured (set PYCHARTER_AUTH_JWT_SECRET or auth.jwt_secret)",
|
|
62
|
+
)
|
|
63
|
+
if not verify_initial_credentials(request.username, request.password):
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
66
|
+
detail="Invalid username or password",
|
|
67
|
+
)
|
|
68
|
+
token = create_access_token(request.username)
|
|
69
|
+
return LoginResponse(
|
|
70
|
+
access_token=token,
|
|
71
|
+
token_type="bearer",
|
|
72
|
+
expires_in=ACCESS_TOKEN_EXPIRE_SECONDS,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.post(
|
|
77
|
+
"/auth/logout",
|
|
78
|
+
status_code=status.HTTP_200_OK,
|
|
79
|
+
summary="Logout",
|
|
80
|
+
description="Client should discard the token. Server-side invalidation can be added later.",
|
|
81
|
+
)
|
|
82
|
+
def logout() -> dict:
|
|
83
|
+
return {"message": "OK"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get(
|
|
87
|
+
"/auth/me",
|
|
88
|
+
response_model=UserResponse,
|
|
89
|
+
status_code=status.HTTP_200_OK,
|
|
90
|
+
summary="Current user",
|
|
91
|
+
description="Return the current user from the Bearer token. Protected.",
|
|
92
|
+
)
|
|
93
|
+
def me(current_user: dict = Depends(get_current_user)) -> UserResponse:
|
|
94
|
+
return UserResponse(
|
|
95
|
+
username=current_user["username"],
|
|
96
|
+
auth_disabled=current_user.get("auth_disabled", False),
|
|
97
|
+
)
|
|
@@ -269,10 +269,11 @@ async def build_contract_from_id_endpoint(
|
|
|
269
269
|
model_name="CoercionRule"
|
|
270
270
|
)
|
|
271
271
|
if coercion_rules and coercion_rules.rules:
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
# Builder expects {"version": "...", "rules": {...}} so _extract_rules returns the inner dict
|
|
273
|
+
coercion_rules_data = {
|
|
274
|
+
"version": coercion_rules.version,
|
|
275
|
+
"rules": dict(coercion_rules.rules),
|
|
276
|
+
}
|
|
276
277
|
|
|
277
278
|
# Get validation rules (optional)
|
|
278
279
|
validation_rules_data = None
|
|
@@ -282,10 +283,11 @@ async def build_contract_from_id_endpoint(
|
|
|
282
283
|
model_name="ValidationRule"
|
|
283
284
|
)
|
|
284
285
|
if validation_rules and validation_rules.rules:
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
# Builder expects {"version": "...", "rules": {...}} so _extract_rules returns the inner dict
|
|
287
|
+
validation_rules_data = {
|
|
288
|
+
"version": validation_rules.version,
|
|
289
|
+
"rules": dict(validation_rules.rules),
|
|
290
|
+
}
|
|
289
291
|
|
|
290
292
|
# Get metadata (optional)
|
|
291
293
|
metadata_data = None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Route handlers for ETL pipeline execution.
|
|
3
|
+
|
|
4
|
+
Allows running an ETL pipeline from YAML config strings (extract, transform, load).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Union
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
from fastapi import APIRouter, HTTPException, status
|
|
12
|
+
|
|
13
|
+
from pycharter import Pipeline
|
|
14
|
+
from pycharter.api.models.etl import EtlRunRequest, EtlRunResponse
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
router = APIRouter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_yaml_config(yaml_str: str, field_name: str) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
|
22
|
+
"""
|
|
23
|
+
Parse a YAML string into a dict or list of dicts.
|
|
24
|
+
Empty or whitespace-only string returns {}.
|
|
25
|
+
Raises HTTPException 400 on parse error or invalid type.
|
|
26
|
+
"""
|
|
27
|
+
if not yaml_str or not yaml_str.strip():
|
|
28
|
+
return {}
|
|
29
|
+
try:
|
|
30
|
+
parsed = yaml.safe_load(yaml_str)
|
|
31
|
+
except yaml.YAMLError as e:
|
|
32
|
+
raise HTTPException(
|
|
33
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
34
|
+
detail=f"Invalid YAML in {field_name}: {str(e)}",
|
|
35
|
+
)
|
|
36
|
+
if parsed is None:
|
|
37
|
+
return {}
|
|
38
|
+
if isinstance(parsed, dict):
|
|
39
|
+
return parsed
|
|
40
|
+
if isinstance(parsed, list):
|
|
41
|
+
if all(isinstance(item, dict) for item in parsed):
|
|
42
|
+
return parsed
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
45
|
+
detail=f"{field_name} must be a YAML object or list of objects",
|
|
46
|
+
)
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
49
|
+
detail=f"{field_name} must be a YAML object or list of objects, got {type(parsed).__name__}",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.post(
|
|
54
|
+
"/etl/run",
|
|
55
|
+
response_model=EtlRunResponse,
|
|
56
|
+
status_code=status.HTTP_200_OK,
|
|
57
|
+
summary="Run ETL pipeline",
|
|
58
|
+
description="Run an ETL pipeline from extract, transform, and load YAML config strings. Optional variables are applied for ${VAR} substitution. Use dry_run=True to extract and transform without loading.",
|
|
59
|
+
response_description="Pipeline run result with row counts and any errors",
|
|
60
|
+
tags=["ETL"],
|
|
61
|
+
)
|
|
62
|
+
async def run_etl_pipeline(request: EtlRunRequest) -> EtlRunResponse:
|
|
63
|
+
"""
|
|
64
|
+
Run an ETL pipeline from the provided YAML configs.
|
|
65
|
+
|
|
66
|
+
Parses extract_yaml, load_yaml, and optional transform_yaml; builds a pipeline
|
|
67
|
+
via Pipeline.from_dict() (so variable substitution is applied), runs it, and
|
|
68
|
+
returns the result (row counts, success, errors).
|
|
69
|
+
"""
|
|
70
|
+
# Parse YAML configs
|
|
71
|
+
extract_dict = _parse_yaml_config(request.extract_yaml, "extract")
|
|
72
|
+
load_dict = _parse_yaml_config(request.load_yaml, "load")
|
|
73
|
+
transform_raw = _parse_yaml_config(request.transform_yaml or "", "transform")
|
|
74
|
+
|
|
75
|
+
if not isinstance(extract_dict, dict) or not extract_dict:
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
78
|
+
detail="Extract config is required and must be a non-empty YAML object",
|
|
79
|
+
)
|
|
80
|
+
if not isinstance(load_dict, dict) or not load_dict:
|
|
81
|
+
raise HTTPException(
|
|
82
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
83
|
+
detail="Load config is required and must be a non-empty YAML object",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Build config for Pipeline.from_dict
|
|
87
|
+
# The transform YAML can contain: transform (simple ops), jsonata, custom_function
|
|
88
|
+
# We pass the full parsed dict to from_dict which will extract these
|
|
89
|
+
config: Dict[str, Any] = {
|
|
90
|
+
"extract": extract_dict,
|
|
91
|
+
"load": load_dict,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# If transform_raw is a dict, include its keys in config (transform, jsonata, custom_function)
|
|
95
|
+
if isinstance(transform_raw, dict):
|
|
96
|
+
if "transform" in transform_raw:
|
|
97
|
+
config["transform"] = transform_raw["transform"]
|
|
98
|
+
if "jsonata" in transform_raw:
|
|
99
|
+
config["jsonata"] = transform_raw["jsonata"]
|
|
100
|
+
if "custom_function" in transform_raw:
|
|
101
|
+
config["custom_function"] = transform_raw["custom_function"]
|
|
102
|
+
elif isinstance(transform_raw, list):
|
|
103
|
+
# If it's a list, treat it as transform steps
|
|
104
|
+
config["transform"] = transform_raw
|
|
105
|
+
|
|
106
|
+
variables = request.variables or {}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
pipeline = Pipeline.from_dict(config, variables=variables)
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
raise HTTPException(
|
|
112
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
113
|
+
detail=str(e),
|
|
114
|
+
)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.exception("Pipeline build failed")
|
|
117
|
+
raise HTTPException(
|
|
118
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
119
|
+
detail=f"Pipeline config invalid: {str(e)}",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
result = await pipeline.run(dry_run=request.dry_run)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.exception("Pipeline run failed")
|
|
126
|
+
raise HTTPException(
|
|
127
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
128
|
+
detail=str(e),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return EtlRunResponse(**result.to_dict())
|
pycharter/cli.py
CHANGED
|
@@ -127,7 +127,7 @@ def main():
|
|
|
127
127
|
seed_parser.add_argument(
|
|
128
128
|
"seed_dir",
|
|
129
129
|
nargs="?",
|
|
130
|
-
help="Directory containing seed YAML files (default: data/seed)",
|
|
130
|
+
help="Directory containing seed YAML files (default: package data/seed)",
|
|
131
131
|
)
|
|
132
132
|
seed_parser.add_argument(
|
|
133
133
|
"database_url",
|