matchlab 0.1.0.dev2__tar.gz → 0.1.0.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/PKG-INFO +1 -1
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/guide/serialise.md +8 -14
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/document.py +44 -38
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/PKG-INFO +1 -1
- matchlab-0.1.0.dev3/src/matchlab.egg-info/scm_version.json +8 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/plan/test_plan.py +8 -3
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/test_document.py +67 -46
- matchlab-0.1.0.dev2/src/matchlab.egg-info/scm_version.json +0 -8
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/actions/setup-toolchain/action.yml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/dependabot.yml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/pull_request_template.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/skills/lucid-docs/README.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/skills/lucid-docs/SKILL.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/skills/lucid-docs/readability.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/workflows/cd.yml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/workflows/ci.yml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.github/workflows/codeql.yml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.gitignore +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.pre-commit-config.yaml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.vscode/launch.json +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/.vscode/settings.json +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/CODEOWNERS +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/LICENSE +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/README.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/README.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/__main__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/cases.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/data.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/measure.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/measurements.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/report.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/results/.gitkeep +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/runner.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/benchmarks/topologies.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/adapters.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/core/exceptions.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/core/hash.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/core/resolver_output.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/core/schemas.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/core/transform.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/document.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/eval-judgements.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/eval-metrics.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/eval-sampling.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/eval-tui.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/lineage.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/locations.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/models.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/progress.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/recordstep.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/resolvers.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/resources.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/sources.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/specs.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/steps.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/testkit.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/api/transformers.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/assets/matchbox-icon-dark.png +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/assets/matchbox-icon.svg +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/assets/matchbox-logo-dark.svg +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/assets/matchbox-logo-light.svg +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/contributing.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/glossary.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/guide/build-a-plan.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/guide/evaluate.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/guide/install.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/guide/query.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/index.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/migration/matchbox-to-matchlab.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/stylesheets/extra.css +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/use-cases.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/docs/why-matchlab.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/justfile +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/mkdocs.yml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/personal-data-exclusions.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/pyproject.toml +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/scripts/test_eval.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/setup.cfg +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/adapters/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/adapters/base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/adapters/duckdb.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/cli.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/dataframes.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/dsu.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/exceptions.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/hash.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/kinds.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/logging.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/resolver_output.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/schemas.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/core/sql.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/judgements.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/metrics.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/samples.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/app.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/modals.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/styles.tcss +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/widgets/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/widgets/assignment.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/widgets/styling.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/eval/tui/widgets/table.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/lineage.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/comparison.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/dedupers/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/dedupers/base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/dedupers/naive.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/linkers/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/linkers/base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/linkers/deterministic.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/linkers/splinklinker.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/linkers/weighteddeterministic.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/models/models.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/progress.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/recordstep.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/resolvers/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/resolvers/base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/resolvers/components.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/resolvers/resolvers.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/resources.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/sources/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/sources/base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/sources/dataframe.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/sources/relational.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/sources/sources.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/specs.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/steps.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/_generate.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/compare.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/entities.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/features.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/linked.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/matchers.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/models.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/testkit/sources.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/clean.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/explode.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/group.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/select.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab/transformers/transform.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/SOURCES.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/dependency_links.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/entry_points.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/requires.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/scm_file_list.json +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/src/matchlab.egg-info/top_level.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/README.md +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/adapters/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/adapters/conftest.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/adapters/test_base.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/adapters/test_contract.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/adapters/test_duckdb.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/conftest.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/core/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/core/test_dsu.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/core/test_eval.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/core/test_hash.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/core/test_resolver_output.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/eval/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/eval/test_samples.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/eval/tui/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/eval/tui/test_app.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/eval/tui/test_widgets.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/models/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/models/test_comparison.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/models/test_dedupers_deterministic.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/models/test_linkers_deterministic.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/models/test_linkers_probabilistic.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/models/test_models.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/plan/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/plan/test_ground_truth.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/plan/test_steps.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/resolvers/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/resolvers/test_components.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/resolvers/test_resolvers.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/sources/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/sources/conftest.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/sources/test_locations.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/test_benchmarks.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/test_cli.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/test_lineage.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/test_progress.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/test_resources.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/testkit/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/testkit/test_entities.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/testkit/test_linked.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/testkit/test_models.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/testkit/test_scores.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/testkit/test_sources.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/transformers/__init__.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/transformers/test_clean.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/transformers/test_explode.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/transformers/test_group.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/transformers/test_select.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/test/transformers/test_transform.py +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/trufflehog-exclude.txt +0 -0
- {matchlab-0.1.0.dev2 → matchlab-0.1.0.dev3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matchlab
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: A local-first library for building, running and evaluating entity resolution pipelines.
|
|
5
5
|
Author: Department for Business and Trade
|
|
6
6
|
Project-URL: Documentation, https://uktrade.github.io/matchlab/
|
|
@@ -9,11 +9,10 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import matchlab as mb
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
Path("plan.json").write_text(document.model_dump_json(indent=2))
|
|
12
|
+
Path("plan.json").write_text(mb.dump(entities))
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
`dump()` takes the apex of the plan and walks upstream, covering exactly what `collect()` would run.
|
|
15
|
+
`dump()` takes the apex of the plan and walks upstream, covering exactly what `collect()` would run. It hands back JSON, so where the document goes is your call: a file, a column, an object store.
|
|
17
16
|
|
|
18
17
|
## What a document holds
|
|
19
18
|
|
|
@@ -136,22 +135,17 @@ name rather than the value, and no credential is serialised.
|
|
|
136
135
|
|
|
137
136
|
## Loading
|
|
138
137
|
|
|
139
|
-
`load()` rebuilds the plan and returns its apex. Nothing is collected.
|
|
138
|
+
`load()` rebuilds the plan and returns its apex. Nothing is collected. Hand it the JSON:
|
|
140
139
|
|
|
141
140
|
```python
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
entities = mb.load(
|
|
142
|
+
Path("plan.json").read_text(),
|
|
143
|
+
resources={"warehouse": create_engine("postgresql://...")},
|
|
144
|
+
)
|
|
145
145
|
entities.collect()
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
`resources` is keyed by resource name
|
|
149
|
-
|
|
150
|
-
```python
|
|
151
|
-
document.required_resources() # {'warehouse'}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Miss one out and `load()` fails at once, rather than several steps into a collect.
|
|
148
|
+
`resources` is keyed by resource name. Miss any out and `load()` says so before rebuilding anything.
|
|
155
149
|
|
|
156
150
|
## Fingerprints transfer
|
|
157
151
|
|
|
@@ -158,15 +158,19 @@ class PlanDocument(BaseModel):
|
|
|
158
158
|
return {name for node in self.steps for name in node.resources.values()}
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def dump(root: Step) ->
|
|
161
|
+
def dump(root: Step, *, indent: int | None = None) -> str:
|
|
162
162
|
"""Describe `root` and everything it reads as a portable document.
|
|
163
163
|
|
|
164
|
+
JSON rather than an object, because a document exists to be put somewhere: a
|
|
165
|
+
column, an object store, a request body, a file.
|
|
166
|
+
|
|
164
167
|
Args:
|
|
165
168
|
root: The apex of the plan. Only what is reachable upstream of it is included,
|
|
166
169
|
exactly as `collect()` would run.
|
|
170
|
+
indent: How the JSON is laid out. `None`, the default, is compact.
|
|
167
171
|
|
|
168
172
|
Returns:
|
|
169
|
-
The document
|
|
173
|
+
The document, as JSON.
|
|
170
174
|
|
|
171
175
|
Raises:
|
|
172
176
|
ResourceError: If a resource was passed without a name, so there is nothing for
|
|
@@ -174,23 +178,9 @@ def dump(root: Step) -> PlanDocument:
|
|
|
174
178
|
earlier, when the plan is built.
|
|
175
179
|
"""
|
|
176
180
|
ordered = walk(root)
|
|
177
|
-
_check_resources(ordered)
|
|
178
|
-
position = {id(step): index for index, step in enumerate(ordered)}
|
|
179
|
-
return PlanDocument(
|
|
180
|
-
steps=tuple(
|
|
181
|
-
_node(step, tuple(position[id(parent)] for parent in step.parents))
|
|
182
|
-
for step in ordered
|
|
183
|
-
)
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _check_resources(steps: list[Step]) -> None:
|
|
188
|
-
"""Reject a plan whose resources have no names to record.
|
|
189
181
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"""
|
|
193
|
-
for position, step in enumerate(steps):
|
|
182
|
+
# Check resources
|
|
183
|
+
for position, step in enumerate(ordered):
|
|
194
184
|
for field, resource in step._resources().items():
|
|
195
185
|
if resource.name is None:
|
|
196
186
|
raise ResourceError(
|
|
@@ -199,6 +189,15 @@ def _check_resources(steps: list[Step]) -> None:
|
|
|
199
189
|
f'{field}=Resource("some_name", ...).'
|
|
200
190
|
)
|
|
201
191
|
|
|
192
|
+
position = {id(step): index for index, step in enumerate(ordered)}
|
|
193
|
+
document = PlanDocument(
|
|
194
|
+
steps=tuple(
|
|
195
|
+
_node(step, tuple(position[id(parent)] for parent in step.parents))
|
|
196
|
+
for step in ordered
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return document.model_dump_json(indent=indent)
|
|
200
|
+
|
|
202
201
|
|
|
203
202
|
def _node(step: Step, inputs: tuple[int, ...]) -> StepNode:
|
|
204
203
|
"""Describe one step, checking its spec is the one its kind implies."""
|
|
@@ -216,61 +215,68 @@ def _node(step: Step, inputs: tuple[int, ...]) -> StepNode:
|
|
|
216
215
|
)
|
|
217
216
|
|
|
218
217
|
|
|
219
|
-
def load(document:
|
|
218
|
+
def load(document: str | bytes, resources: Mapping[str, Any]) -> Step:
|
|
220
219
|
"""Rebuild a plan from a document, returning its apex.
|
|
221
220
|
|
|
222
221
|
Nothing is collected. This reconstructs the same lazy plan, so the returned step
|
|
223
222
|
fingerprints identically to the one that was dumped, given the same data.
|
|
224
223
|
|
|
225
224
|
Args:
|
|
226
|
-
document:
|
|
225
|
+
document: The JSON `dump` produced.
|
|
227
226
|
resources: Name to object, for every resource the document names.
|
|
228
|
-
`document.required_resources()` lists them.
|
|
229
227
|
|
|
230
228
|
Returns:
|
|
231
229
|
The plan's apex, the last step in the document.
|
|
232
230
|
|
|
233
231
|
Raises:
|
|
232
|
+
ValidationError: If the JSON does not describe a plan document.
|
|
234
233
|
ValueError: If the document is empty, names an unregistered location class, or
|
|
235
234
|
wires a step to an input of the wrong kind.
|
|
236
235
|
ResourceError: If a named resource was not supplied.
|
|
237
236
|
"""
|
|
238
|
-
|
|
237
|
+
parsed = PlanDocument.model_validate_json(document)
|
|
238
|
+
if not parsed.steps:
|
|
239
239
|
raise ValueError("A plan document needs at least one step")
|
|
240
240
|
|
|
241
|
+
_check_supplied(parsed, resources)
|
|
242
|
+
|
|
241
243
|
built: list[Step] = []
|
|
242
|
-
for position, node in enumerate(
|
|
244
|
+
for position, node in enumerate(parsed.steps):
|
|
243
245
|
built.append(
|
|
244
246
|
_rebuild(node, position, [built[i] for i in node.inputs], resources)
|
|
245
247
|
)
|
|
246
248
|
return built[-1]
|
|
247
249
|
|
|
248
250
|
|
|
249
|
-
def
|
|
250
|
-
|
|
251
|
-
) -> dict[str, Resource]:
|
|
252
|
-
"""Turn a node's `field -> resource name` map into bound resources.
|
|
251
|
+
def _check_supplied(document: PlanDocument, supplied: Mapping[str, Any]) -> None:
|
|
252
|
+
"""Reject a load missing any resource the document names, naming them all.
|
|
253
253
|
|
|
254
254
|
Raises:
|
|
255
255
|
ResourceError: If the document names a resource `supplied` does not have.
|
|
256
256
|
"""
|
|
257
|
-
|
|
258
|
-
for
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
f"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
missing: dict[str, str] = {}
|
|
258
|
+
for position, node in enumerate(document.steps):
|
|
259
|
+
for field, name in node.resources.items():
|
|
260
|
+
if name not in supplied and name not in missing:
|
|
261
|
+
missing[name] = f"step {position} ({node.kind}) reads it as '{field}'"
|
|
262
|
+
if not missing:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
wanted = "\n".join(f" '{name}' — {where}" for name, where in missing.items())
|
|
266
|
+
example = ", ".join(f"'{name}': ..." for name in missing)
|
|
267
|
+
raise ResourceError(
|
|
268
|
+
f"This plan needs resources that were not supplied:\n{wanted}\n"
|
|
269
|
+
f"Pass them when loading: resources={{{example}}}."
|
|
270
|
+
)
|
|
267
271
|
|
|
268
272
|
|
|
269
273
|
def _rebuild(
|
|
270
274
|
node: StepNode, position: int, inputs: list[Step], supplied: Mapping[str, Any]
|
|
271
275
|
) -> Step:
|
|
272
276
|
"""Rebuild one step from its node and its already-rebuilt inputs."""
|
|
273
|
-
resources =
|
|
277
|
+
resources = {
|
|
278
|
+
field: Resource(name, supplied[name]) for field, name in node.resources.items()
|
|
279
|
+
}
|
|
274
280
|
match node.kind:
|
|
275
281
|
case StepKind.SOURCE:
|
|
276
282
|
spec = _expect(node, position, SourceSpec)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matchlab
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: A local-first library for building, running and evaluating entity resolution pipelines.
|
|
5
5
|
Author: Department for Business and Trade
|
|
6
6
|
Project-URL: Documentation, https://uktrade.github.io/matchlab/
|
|
@@ -53,6 +53,11 @@ def _apex(source: Callable[..., mb.Source]) -> tuple[mb.Resolver, mb.Source, mb.
|
|
|
53
53
|
return apex, crn, dh
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def _required_resources(root: mb.Step) -> set[str]:
|
|
57
|
+
"""What `load` would have to be given, read off the dumped JSON."""
|
|
58
|
+
return mb.PlanDocument.model_validate_json(mb.dump(root)).required_resources()
|
|
59
|
+
|
|
60
|
+
|
|
56
61
|
def _ids_by_key(matches: pl.DataFrame, column: str) -> dict[str, int]:
|
|
57
62
|
return {
|
|
58
63
|
row[column]: row["root"]
|
|
@@ -293,7 +298,7 @@ def test_resource_sharing(warehouse: Engine) -> None:
|
|
|
293
298
|
model_settings={"comparisons": ["l.crn_company = r.dh_company"]},
|
|
294
299
|
).resolve()
|
|
295
300
|
|
|
296
|
-
assert
|
|
301
|
+
assert _required_resources(plan) == {"wh"}
|
|
297
302
|
|
|
298
303
|
|
|
299
304
|
def test_repeated_resource_name(
|
|
@@ -359,8 +364,8 @@ def test_resource_name_reuse(
|
|
|
359
364
|
left = source("crn", client=mb.Resource("wh", warehouse))
|
|
360
365
|
right = source("dh", client=mb.Resource("wh", sqlite_in_memory_warehouse))
|
|
361
366
|
|
|
362
|
-
assert
|
|
363
|
-
assert
|
|
367
|
+
assert _required_resources(left) == {"wh"}
|
|
368
|
+
assert _required_resources(right) == {"wh"}
|
|
364
369
|
|
|
365
370
|
|
|
366
371
|
def test_anonymous_resources_no_collision(
|
|
@@ -73,9 +73,12 @@ def plan(source: Callable[..., Source]) -> Resolver:
|
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def
|
|
77
|
-
"""
|
|
78
|
-
|
|
76
|
+
def _parse(root: Step) -> PlanDocument:
|
|
77
|
+
"""Dump a plan and read the JSON back, for tests about what a document says.
|
|
78
|
+
|
|
79
|
+
`load` takes the JSON as it stands. Parsing is for looking inside.
|
|
80
|
+
"""
|
|
81
|
+
return PlanDocument.model_validate_json(dump(root))
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
# A valid source spec, for tests about a node's shape rather than its settings.
|
|
@@ -95,7 +98,7 @@ def test_rebuild_fingerprints_match(plan: Resolver, warehouse: Engine) -> None:
|
|
|
95
98
|
original = plan
|
|
96
99
|
original.collect()
|
|
97
100
|
|
|
98
|
-
rebuilt = load(
|
|
101
|
+
rebuilt = load(dump(original), resources={"warehouse": warehouse})
|
|
99
102
|
rebuilt.collect()
|
|
100
103
|
|
|
101
104
|
before = [step._fp for step in lineage.walk(original)]
|
|
@@ -110,7 +113,7 @@ def test_rebuild_hits_cache(
|
|
|
110
113
|
"""A transferred plan must find the original's artifacts, not redo the work."""
|
|
111
114
|
plan.collect(adapter)
|
|
112
115
|
|
|
113
|
-
rebuilt = load(
|
|
116
|
+
rebuilt = load(dump(plan), resources={"warehouse": warehouse})
|
|
114
117
|
for step in lineage.walk(rebuilt):
|
|
115
118
|
|
|
116
119
|
def boom(*_a: object, _step: Step = step, **_k: object) -> None:
|
|
@@ -138,8 +141,7 @@ def test_rebuild_different_warehouse(plan: Resolver, tmp_path: Path) -> None:
|
|
|
138
141
|
original = plan
|
|
139
142
|
original.collect()
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
elsewhere = load(document, resources={"warehouse": other})
|
|
144
|
+
elsewhere = load(dump(original), resources={"warehouse": other})
|
|
143
145
|
elsewhere.collect()
|
|
144
146
|
|
|
145
147
|
assert elsewhere._fp != original._fp
|
|
@@ -152,26 +154,40 @@ def test_rebuild_different_warehouse(plan: Resolver, tmp_path: Path) -> None:
|
|
|
152
154
|
def test_rebuild_same_answer(plan: Resolver, warehouse: Engine) -> None:
|
|
153
155
|
"""A rebuilt plan collects to the same lookup as the original."""
|
|
154
156
|
original = plan
|
|
155
|
-
rebuilt = load(
|
|
157
|
+
rebuilt = load(dump(original), resources={"warehouse": warehouse})
|
|
156
158
|
|
|
157
159
|
expected = original.collect().get_lookup().sort("root")
|
|
158
160
|
actual = rebuilt.collect().get_lookup().sort("root")
|
|
159
161
|
assert actual.equals(expected)
|
|
160
162
|
|
|
161
163
|
|
|
164
|
+
def test_load_from_bytes(plan: Resolver, warehouse: Engine) -> None:
|
|
165
|
+
"""A store hands JSON back as bytes as often as text, so `load` takes both.
|
|
166
|
+
|
|
167
|
+
The text half of this is every other round trip in this file.
|
|
168
|
+
"""
|
|
169
|
+
plan.collect()
|
|
170
|
+
rebuilt = load(dump(plan).encode(), resources={"warehouse": warehouse})
|
|
171
|
+
rebuilt.collect()
|
|
172
|
+
|
|
173
|
+
assert [step._fp for step in lineage.walk(rebuilt)] == [
|
|
174
|
+
step._fp for step in lineage.walk(plan)
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
162
178
|
def test_document_carries_no_labels(plan: Resolver, warehouse: Engine) -> None:
|
|
163
179
|
"""Publishing is done to a result, so a label is not part of the plan.
|
|
164
180
|
|
|
165
181
|
A source's name is not a label: it prefixes that source's columns and tags its
|
|
166
182
|
rows, so it is part of the output rather than a way of finding it.
|
|
167
183
|
"""
|
|
168
|
-
document =
|
|
184
|
+
document = _parse(plan)
|
|
169
185
|
|
|
170
186
|
assert not any(hasattr(node, "name") for node in document.steps)
|
|
171
187
|
sources = [node.spec for node in document.steps if node.kind == "source"]
|
|
172
188
|
assert {spec.name for spec in sources} == {"crn", "dh"}
|
|
173
189
|
|
|
174
|
-
rebuilt = load(
|
|
190
|
+
rebuilt = load(dump(plan), resources={"warehouse": warehouse})
|
|
175
191
|
assert lineage.number(rebuilt) == {
|
|
176
192
|
id(step): index for index, step in enumerate(lineage.walk(rebuilt))
|
|
177
193
|
}
|
|
@@ -180,11 +196,11 @@ def test_document_carries_no_labels(plan: Resolver, warehouse: Engine) -> None:
|
|
|
180
196
|
def test_document_preserves_sharing(plan: Resolver, warehouse: Engine) -> None:
|
|
181
197
|
"""A view feeding two models is one node referenced twice, not two nodes."""
|
|
182
198
|
original = plan
|
|
183
|
-
document =
|
|
199
|
+
document = _parse(original)
|
|
184
200
|
|
|
185
201
|
assert len(document.steps) == len(lineage.walk(original))
|
|
186
202
|
|
|
187
|
-
rebuilt = load(
|
|
203
|
+
rebuilt = load(dump(original), resources={"warehouse": warehouse})
|
|
188
204
|
models = [step for step in lineage.walk(rebuilt) if isinstance(step, Model)]
|
|
189
205
|
linkers = [model for model in models if model.right is not None]
|
|
190
206
|
assert len(linkers) == 2
|
|
@@ -204,14 +220,13 @@ def test_bare_value(warehouse: Engine) -> None:
|
|
|
204
220
|
|
|
205
221
|
def test_document_carries_no_client(plan: Resolver) -> None:
|
|
206
222
|
"""Locations describe where data lives; connecting is the target's business."""
|
|
207
|
-
|
|
208
|
-
serialised = document.model_dump_json()
|
|
223
|
+
serialised = dump(plan)
|
|
209
224
|
|
|
210
225
|
assert "sqlite" not in serialised # the engine's URL never leaves
|
|
211
226
|
assert '"resources":{"client":"warehouse"}' in serialised # only its name does
|
|
212
227
|
|
|
213
|
-
with pytest.raises(ResourceError, match="
|
|
214
|
-
load(
|
|
228
|
+
with pytest.raises(ResourceError, match="'warehouse' — step"):
|
|
229
|
+
load(serialised, resources={})
|
|
215
230
|
|
|
216
231
|
|
|
217
232
|
def test_resource_rename_keeps_fingerprint(plan: Resolver, warehouse: Engine) -> None:
|
|
@@ -223,22 +238,21 @@ def test_resource_rename_keeps_fingerprint(plan: Resolver, warehouse: Engine) ->
|
|
|
223
238
|
original = plan
|
|
224
239
|
original.collect()
|
|
225
240
|
|
|
226
|
-
document =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
)
|
|
236
|
-
for node in document.steps
|
|
241
|
+
document = _parse(original)
|
|
242
|
+
elsewhere = document.model_copy(
|
|
243
|
+
update={
|
|
244
|
+
"steps": tuple(
|
|
245
|
+
node.model_copy(
|
|
246
|
+
update={
|
|
247
|
+
"resources": dict.fromkeys(node.resources, "somewhere_else")
|
|
248
|
+
}
|
|
237
249
|
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
for node in document.steps
|
|
251
|
+
)
|
|
252
|
+
}
|
|
241
253
|
)
|
|
254
|
+
|
|
255
|
+
renamed = load(elsewhere.model_dump_json(), resources={"somewhere_else": warehouse})
|
|
242
256
|
renamed.collect()
|
|
243
257
|
|
|
244
258
|
assert [step._fp for step in lineage.walk(renamed)] == [
|
|
@@ -251,7 +265,7 @@ def test_resource_rename_keeps_fingerprint(plan: Resolver, warehouse: Engine) ->
|
|
|
251
265
|
|
|
252
266
|
|
|
253
267
|
def test_required_resources_list(warehouse: Engine) -> None:
|
|
254
|
-
"""A caller can see what to supply before
|
|
268
|
+
"""A caller can see what to supply, before loading or from the error after."""
|
|
255
269
|
frame = pl.DataFrame({"pk": ["b1"], "company": ["acme"]})
|
|
256
270
|
db = read_database(
|
|
257
271
|
"crn", sql="select pk, company from crn", client=Resource("wh", warehouse)
|
|
@@ -263,16 +277,23 @@ def test_required_resources_list(warehouse: Engine) -> None:
|
|
|
263
277
|
model_settings={"comparisons": ["l.crn_company = r.dh_company"]},
|
|
264
278
|
).resolve()
|
|
265
279
|
|
|
266
|
-
document =
|
|
280
|
+
document = _parse(plan)
|
|
267
281
|
assert document.required_resources() == {"wh", "dh_frame"}
|
|
268
282
|
|
|
269
|
-
with pytest.raises(ResourceError, match="
|
|
270
|
-
load(
|
|
283
|
+
with pytest.raises(ResourceError, match="'dh_frame' — step"):
|
|
284
|
+
load(dump(plan), resources={"wh": warehouse})
|
|
285
|
+
|
|
286
|
+
# Supply none and every missing name comes back at once, so working from the
|
|
287
|
+
# error takes one more attempt rather than one per resource.
|
|
288
|
+
with pytest.raises(ResourceError) as complaint:
|
|
289
|
+
load(dump(plan), resources={})
|
|
290
|
+
assert "'wh'" in str(complaint.value)
|
|
291
|
+
assert "'dh_frame'" in str(complaint.value)
|
|
271
292
|
|
|
272
293
|
|
|
273
294
|
def test_document_custom_location(plan: Resolver, warehouse: Engine) -> None:
|
|
274
295
|
"""Documents can represent custom locations."""
|
|
275
|
-
document =
|
|
296
|
+
document = _parse(plan)
|
|
276
297
|
broken = document.model_copy(
|
|
277
298
|
update={
|
|
278
299
|
"steps": tuple(
|
|
@@ -291,10 +312,10 @@ def test_document_custom_location(plan: Resolver, warehouse: Engine) -> None:
|
|
|
291
312
|
)
|
|
292
313
|
|
|
293
314
|
# It parses — a document may legitimately name a class this codebase lacks.
|
|
294
|
-
assert
|
|
315
|
+
assert PlanDocument.model_validate_json(broken.model_dump_json())
|
|
295
316
|
|
|
296
317
|
with pytest.raises(ValueError, match="No location class named 'S3Location'"):
|
|
297
|
-
load(broken, resources={"warehouse": warehouse})
|
|
318
|
+
load(broken.model_dump_json(), resources={"warehouse": warehouse})
|
|
298
319
|
|
|
299
320
|
|
|
300
321
|
# -- what the document says -----------------------------------------------------------
|
|
@@ -302,7 +323,7 @@ def test_document_custom_location(plan: Resolver, warehouse: Engine) -> None:
|
|
|
302
323
|
|
|
303
324
|
def test_edges_point_backwards(plan: Resolver) -> None:
|
|
304
325
|
"""Topological order is the document's ordering guarantee."""
|
|
305
|
-
document =
|
|
326
|
+
document = _parse(plan)
|
|
306
327
|
|
|
307
328
|
assert document.steps[0].kind == "source"
|
|
308
329
|
assert document.steps[-1].kind == "resolver"
|
|
@@ -315,7 +336,7 @@ def test_settings_vs_resources(warehouse: Engine) -> None:
|
|
|
315
336
|
source = read_database(
|
|
316
337
|
"ch", sql="select id, c from ch", client=Resource("wh", warehouse)
|
|
317
338
|
)
|
|
318
|
-
node =
|
|
339
|
+
node = _parse(source).steps[0]
|
|
319
340
|
|
|
320
341
|
assert node.spec.location_settings == {"sql": "select id, c from ch"}
|
|
321
342
|
assert node.resources == {"client": "wh"}
|
|
@@ -323,7 +344,7 @@ def test_settings_vs_resources(warehouse: Engine) -> None:
|
|
|
323
344
|
|
|
324
345
|
def test_spec_has_settings_not_edges(plan: Resolver) -> None:
|
|
325
346
|
"""The split that makes a spec safe to hash and a document able to rebuild."""
|
|
326
|
-
document =
|
|
347
|
+
document = _parse(plan)
|
|
327
348
|
|
|
328
349
|
for node in document.steps:
|
|
329
350
|
spec = node.spec.model_dump(mode="json")
|
|
@@ -342,14 +363,14 @@ def test_setting_travels_as_position(plan: Resolver, warehouse: Engine) -> None:
|
|
|
342
363
|
A name would have been the alternative, and would have made the document depend on
|
|
343
364
|
names being stable — the thing positions exist to avoid.
|
|
344
365
|
"""
|
|
345
|
-
document =
|
|
366
|
+
document = _parse(plan)
|
|
346
367
|
apex = document.steps[-1]
|
|
347
368
|
|
|
348
369
|
assert apex.kind == "resolver"
|
|
349
370
|
# `1` is the second input, and JSON has stringified the key.
|
|
350
371
|
assert apex.spec.resolver_settings == {"thresholds": {"1": 0.5}}
|
|
351
372
|
|
|
352
|
-
rebuilt = load(
|
|
373
|
+
rebuilt = load(dump(plan), resources={"warehouse": warehouse})
|
|
353
374
|
assert rebuilt.resolver_instance.thresholds == {1: 0.5}
|
|
354
375
|
|
|
355
376
|
|
|
@@ -395,7 +416,7 @@ def test_spec_parsed_by_kind() -> None:
|
|
|
395
416
|
|
|
396
417
|
def test_load_rejects_wrong_kind(plan: Resolver, warehouse: Engine) -> None:
|
|
397
418
|
"""A malformed document fails at load, not with a confusing error much later."""
|
|
398
|
-
document =
|
|
419
|
+
document = _parse(plan)
|
|
399
420
|
resolver = next(node for node in document.steps if node.kind == "resolver")
|
|
400
421
|
broken = PlanDocument(
|
|
401
422
|
steps=tuple(
|
|
@@ -405,13 +426,13 @@ def test_load_rejects_wrong_kind(plan: Resolver, warehouse: Engine) -> None:
|
|
|
405
426
|
)
|
|
406
427
|
|
|
407
428
|
with pytest.raises(ValueError, match="must read only"):
|
|
408
|
-
load(broken, resources={"warehouse": warehouse})
|
|
429
|
+
load(broken.model_dump_json(), resources={"warehouse": warehouse})
|
|
409
430
|
|
|
410
431
|
|
|
411
432
|
def test_document_empty_rejected() -> None:
|
|
412
433
|
"""A document with no steps is rejected on load."""
|
|
413
434
|
with pytest.raises(ValueError, match="at least one step"):
|
|
414
|
-
load(PlanDocument(steps=()), resources={})
|
|
435
|
+
load(PlanDocument(steps=()).model_dump_json(), resources={})
|
|
415
436
|
|
|
416
437
|
|
|
417
438
|
def test_rebuild_reads_resolver_as_record_step(
|
|
@@ -449,7 +470,7 @@ def test_rebuild_transform_chain(
|
|
|
449
470
|
.resolve()
|
|
450
471
|
)
|
|
451
472
|
|
|
452
|
-
rebuilt = load(
|
|
473
|
+
rebuilt = load(dump(plan), resources={"warehouse": warehouse})
|
|
453
474
|
transforms = [step for step in lineage.walk(rebuilt) if isinstance(step, Transform)]
|
|
454
475
|
|
|
455
476
|
assert [t.transformer_class.__name__ for t in transforms] == ["Select", "Clean"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|