mostlyright-data 0.9.0__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.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Fixed identifier partitions and bounded external merge for Data.gov v4 passes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from mostlyright.data_harness.canonical import canonical_sha256, sha256_bytes
|
|
13
|
+
from mostlyright.data_harness.sources.catalog.fill_staging import CatalogFillRefused, FillStaging
|
|
14
|
+
|
|
15
|
+
PARTITION_COUNT = 256
|
|
16
|
+
RUN_MAX_ITEMS = 128
|
|
17
|
+
MERGE_FAN_IN = 16
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class PassMapResult:
|
|
22
|
+
root_sha256: str
|
|
23
|
+
manifest_sha256: str
|
|
24
|
+
unique_records: int
|
|
25
|
+
conflicting_identifiers: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_partition_runs(staging: FillStaging, records: tuple[Any, ...]) -> list[dict[str, Any]]:
|
|
29
|
+
"""Route exact identifier bytes by SHA-256 prefix into immutable bounded sorted runs."""
|
|
30
|
+
|
|
31
|
+
descriptors: list[dict[str, Any]] = []
|
|
32
|
+
for partition, entries, conflicting_identifiers in plan_partition_runs(records):
|
|
33
|
+
digest = _write_run(
|
|
34
|
+
staging,
|
|
35
|
+
partition=partition,
|
|
36
|
+
entries=entries,
|
|
37
|
+
conflicting_identifiers=conflicting_identifiers,
|
|
38
|
+
)
|
|
39
|
+
descriptors.append({"partition": partition, "run_sha256": digest})
|
|
40
|
+
return descriptors
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def plan_partition_runs(
|
|
44
|
+
records: Iterable[Any],
|
|
45
|
+
) -> Iterable[tuple[str, list[tuple[str, str]], set[str]]]:
|
|
46
|
+
"""Yield the exact bounded page runs used by both installation and validation."""
|
|
47
|
+
|
|
48
|
+
groups: dict[str, list[tuple[str, str]]] = {}
|
|
49
|
+
for record in records:
|
|
50
|
+
identifier = getattr(record, "record_id", None)
|
|
51
|
+
semantic_sha256 = getattr(record, "digest", None)
|
|
52
|
+
if not isinstance(identifier, str) or not _is_digest(semantic_sha256):
|
|
53
|
+
raise CatalogFillRefused(
|
|
54
|
+
"DATAGOV_PARTITION", "fill.records", "record identity/digest is invalid"
|
|
55
|
+
)
|
|
56
|
+
partition = sha256_bytes(identifier.encode("utf-8", errors="strict"))[:2]
|
|
57
|
+
groups.setdefault(partition, []).append((identifier, semantic_sha256))
|
|
58
|
+
for partition in sorted(groups):
|
|
59
|
+
entries, conflicting_identifiers = _deduplicate_sorted(groups[partition])
|
|
60
|
+
for offset in range(0, len(entries), RUN_MAX_ITEMS):
|
|
61
|
+
chunk = entries[offset : offset + RUN_MAX_ITEMS]
|
|
62
|
+
yield (
|
|
63
|
+
partition,
|
|
64
|
+
chunk,
|
|
65
|
+
conflicting_identifiers & {identifier for identifier, _semantic in chunk},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def external_merge_pass(
|
|
70
|
+
staging: FillStaging,
|
|
71
|
+
run_descriptors: Iterable[dict[str, Any]],
|
|
72
|
+
) -> PassMapResult:
|
|
73
|
+
"""Merge a descriptor stream through bounded disk-backed fixed-size batches."""
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
return _external_merge_pass(staging, run_descriptors)
|
|
77
|
+
except CatalogFillRefused:
|
|
78
|
+
raise
|
|
79
|
+
except (OSError, sqlite3.Error) as error:
|
|
80
|
+
raise CatalogFillRefused(
|
|
81
|
+
"DATAGOV_EXTERNAL_MERGE",
|
|
82
|
+
"fill.partition_runs",
|
|
83
|
+
"bounded external-merge scratch storage failed",
|
|
84
|
+
) from error
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _external_merge_pass(
|
|
88
|
+
staging: FillStaging,
|
|
89
|
+
run_descriptors: Iterable[dict[str, Any]],
|
|
90
|
+
) -> PassMapResult:
|
|
91
|
+
|
|
92
|
+
with tempfile.TemporaryDirectory(
|
|
93
|
+
prefix="mostlyright-datagov-pass-", dir=staging.root.parent
|
|
94
|
+
) as directory:
|
|
95
|
+
database_path = Path(directory) / "merge.sqlite3"
|
|
96
|
+
connection = sqlite3.connect(database_path)
|
|
97
|
+
try:
|
|
98
|
+
connection.execute("PRAGMA journal_mode=OFF")
|
|
99
|
+
connection.execute("PRAGMA synchronous=OFF")
|
|
100
|
+
connection.execute("PRAGMA temp_store=FILE")
|
|
101
|
+
connection.execute(
|
|
102
|
+
"CREATE TABLE merged ("
|
|
103
|
+
"partition TEXT NOT NULL, identifier TEXT NOT NULL, semantic TEXT NOT NULL, "
|
|
104
|
+
"conflicting INTEGER NOT NULL, PRIMARY KEY (partition, identifier)"
|
|
105
|
+
") WITHOUT ROWID"
|
|
106
|
+
)
|
|
107
|
+
descriptor_count = 0
|
|
108
|
+
for descriptor in run_descriptors:
|
|
109
|
+
if (
|
|
110
|
+
not isinstance(descriptor, dict)
|
|
111
|
+
or set(descriptor) != {"partition", "run_sha256"}
|
|
112
|
+
or not _valid_partition(descriptor["partition"])
|
|
113
|
+
or not _is_digest(descriptor["run_sha256"])
|
|
114
|
+
):
|
|
115
|
+
raise CatalogFillRefused(
|
|
116
|
+
"DATAGOV_PARTITION", "fill.partition_runs", "run descriptor is invalid"
|
|
117
|
+
)
|
|
118
|
+
entries, inherited_conflicts = _validate_run(
|
|
119
|
+
staging.read_shard("partition_run", descriptor["run_sha256"]),
|
|
120
|
+
descriptor["partition"],
|
|
121
|
+
)
|
|
122
|
+
connection.executemany(
|
|
123
|
+
"INSERT INTO merged(partition, identifier, semantic, conflicting) "
|
|
124
|
+
"VALUES (?, ?, ?, ?) ON CONFLICT(partition, identifier) DO UPDATE SET "
|
|
125
|
+
"conflicting = merged.conflicting OR excluded.conflicting "
|
|
126
|
+
"OR merged.semantic <> excluded.semantic, "
|
|
127
|
+
"semantic = min(merged.semantic, excluded.semantic)",
|
|
128
|
+
(
|
|
129
|
+
(
|
|
130
|
+
descriptor["partition"],
|
|
131
|
+
identifier,
|
|
132
|
+
semantic,
|
|
133
|
+
int(identifier in inherited_conflicts),
|
|
134
|
+
)
|
|
135
|
+
for identifier, semantic in entries
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
descriptor_count += 1
|
|
139
|
+
if descriptor_count % MERGE_FAN_IN == 0:
|
|
140
|
+
connection.commit()
|
|
141
|
+
connection.commit()
|
|
142
|
+
partition_manifests, unique_records, conflicts = _write_spooled_partitions(
|
|
143
|
+
staging, connection
|
|
144
|
+
)
|
|
145
|
+
finally:
|
|
146
|
+
connection.close()
|
|
147
|
+
manifest = {
|
|
148
|
+
"partition_count": PARTITION_COUNT,
|
|
149
|
+
"partitions": partition_manifests,
|
|
150
|
+
"unique_records": unique_records,
|
|
151
|
+
"conflicting_identifiers": conflicts,
|
|
152
|
+
}
|
|
153
|
+
root = canonical_sha256(manifest)
|
|
154
|
+
manifest_sha256 = staging.write_shard("pass_map", {**manifest, "root_sha256": root})
|
|
155
|
+
return PassMapResult(
|
|
156
|
+
root_sha256=root,
|
|
157
|
+
manifest_sha256=manifest_sha256,
|
|
158
|
+
unique_records=unique_records,
|
|
159
|
+
conflicting_identifiers=conflicts,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _write_spooled_partitions(
|
|
164
|
+
staging: FillStaging, connection: sqlite3.Connection
|
|
165
|
+
) -> tuple[list[dict[str, Any]], int, int]:
|
|
166
|
+
partition_manifests: list[dict[str, Any]] = []
|
|
167
|
+
unique_records = 0
|
|
168
|
+
conflicts = 0
|
|
169
|
+
partitions = connection.execute("SELECT DISTINCT partition FROM merged ORDER BY partition")
|
|
170
|
+
for (partition,) in partitions:
|
|
171
|
+
rows = connection.execute(
|
|
172
|
+
"SELECT identifier, semantic, conflicting FROM merged "
|
|
173
|
+
"WHERE partition = ? ORDER BY identifier",
|
|
174
|
+
(partition,),
|
|
175
|
+
)
|
|
176
|
+
run_sha256s: list[str] = []
|
|
177
|
+
partition_records = 0
|
|
178
|
+
partition_conflicts = 0
|
|
179
|
+
while chunk := rows.fetchmany(RUN_MAX_ITEMS):
|
|
180
|
+
entries = [(identifier, semantic) for identifier, semantic, _conflict in chunk]
|
|
181
|
+
conflicting_identifiers = {
|
|
182
|
+
identifier for identifier, _semantic, conflict in chunk if conflict
|
|
183
|
+
}
|
|
184
|
+
run_sha256s.append(
|
|
185
|
+
_write_run(
|
|
186
|
+
staging,
|
|
187
|
+
partition=partition,
|
|
188
|
+
entries=entries,
|
|
189
|
+
conflicting_identifiers=conflicting_identifiers,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
partition_records += len(entries)
|
|
193
|
+
partition_conflicts += len(conflicting_identifiers)
|
|
194
|
+
unique_records += partition_records
|
|
195
|
+
conflicts += partition_conflicts
|
|
196
|
+
partition_manifests.append(
|
|
197
|
+
{
|
|
198
|
+
"partition": partition,
|
|
199
|
+
"run_sha256s": run_sha256s,
|
|
200
|
+
"record_count": partition_records,
|
|
201
|
+
"conflicting_identifiers": partition_conflicts,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
return partition_manifests, unique_records, conflicts
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def dry_run_external_merge_pass(
|
|
208
|
+
staging: FillStaging,
|
|
209
|
+
run_descriptors: Iterable[dict[str, Any]],
|
|
210
|
+
) -> PassMapResult:
|
|
211
|
+
"""Recompute a pass in bounded scratch space without mutating authenticated staging."""
|
|
212
|
+
|
|
213
|
+
with tempfile.TemporaryDirectory(
|
|
214
|
+
prefix="mostlyright-datagov-merge-", dir=staging.root.parent
|
|
215
|
+
) as directory:
|
|
216
|
+
scratch = FillStaging(Path(directory))
|
|
217
|
+
with scratch.locked():
|
|
218
|
+
adapter = _DryRunStaging(staging, scratch)
|
|
219
|
+
return external_merge_pass(adapter, run_descriptors) # type: ignore[arg-type]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class _DryRunStaging:
|
|
223
|
+
def __init__(self, source: FillStaging, scratch: FillStaging) -> None:
|
|
224
|
+
self._source = source
|
|
225
|
+
self._scratch = scratch
|
|
226
|
+
self._written: set[tuple[str, str]] = set()
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def root(self) -> Path:
|
|
230
|
+
return self._scratch.root
|
|
231
|
+
|
|
232
|
+
def write_shard(self, kind: str, payload: dict[str, Any]) -> str:
|
|
233
|
+
digest = self._scratch.write_shard(kind, payload)
|
|
234
|
+
self._written.add((kind, digest))
|
|
235
|
+
return digest
|
|
236
|
+
|
|
237
|
+
def read_shard(self, kind: str, digest: str) -> dict[str, Any]:
|
|
238
|
+
if (kind, digest) in self._written:
|
|
239
|
+
return self._scratch.read_shard(kind, digest)
|
|
240
|
+
return self._source.read_shard(kind, digest)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def validate_partition_run(staging: FillStaging, descriptor: dict[str, Any]) -> tuple[int, int]:
|
|
244
|
+
"""Authenticate one indexed run descriptor and its exact immutable shard."""
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
not isinstance(descriptor, dict)
|
|
248
|
+
or set(descriptor) != {"partition", "run_sha256"}
|
|
249
|
+
or not _valid_partition(descriptor["partition"])
|
|
250
|
+
or not _is_digest(descriptor["run_sha256"])
|
|
251
|
+
):
|
|
252
|
+
raise CatalogFillRefused(
|
|
253
|
+
"DATAGOV_PARTITION", "fill.partition_runs", "run descriptor is invalid"
|
|
254
|
+
)
|
|
255
|
+
entries, conflicts = _validate_run(
|
|
256
|
+
staging.read_shard("partition_run", descriptor["run_sha256"]),
|
|
257
|
+
descriptor["partition"],
|
|
258
|
+
)
|
|
259
|
+
return len(entries), len(conflicts)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def read_partition_run(
|
|
263
|
+
staging: FillStaging, descriptor: dict[str, Any]
|
|
264
|
+
) -> tuple[str, list[tuple[str, str]], set[str]]:
|
|
265
|
+
"""Authenticate and return one bounded run for exact cross-stream reconciliation."""
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
not isinstance(descriptor, dict)
|
|
269
|
+
or set(descriptor) != {"partition", "run_sha256"}
|
|
270
|
+
or not _valid_partition(descriptor["partition"])
|
|
271
|
+
or not _is_digest(descriptor["run_sha256"])
|
|
272
|
+
):
|
|
273
|
+
raise CatalogFillRefused(
|
|
274
|
+
"DATAGOV_PARTITION", "fill.partition_runs", "run descriptor is invalid"
|
|
275
|
+
)
|
|
276
|
+
entries, conflicts = _validate_run(
|
|
277
|
+
staging.read_shard("partition_run", descriptor["run_sha256"]),
|
|
278
|
+
descriptor["partition"],
|
|
279
|
+
)
|
|
280
|
+
return descriptor["partition"], entries, set(conflicts)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def validate_pass_map(
|
|
284
|
+
staging: FillStaging, manifest_sha256: str, expected_root_sha256: str
|
|
285
|
+
) -> PassMapResult:
|
|
286
|
+
"""Stream-authenticate the terminal pass manifest and every referenced final run."""
|
|
287
|
+
|
|
288
|
+
payload = staging.read_shard("pass_map", manifest_sha256)
|
|
289
|
+
if (
|
|
290
|
+
set(payload)
|
|
291
|
+
!= {
|
|
292
|
+
"partition_count",
|
|
293
|
+
"partitions",
|
|
294
|
+
"unique_records",
|
|
295
|
+
"conflicting_identifiers",
|
|
296
|
+
"root_sha256",
|
|
297
|
+
}
|
|
298
|
+
or payload["partition_count"] != PARTITION_COUNT
|
|
299
|
+
or not isinstance(payload["partitions"], list)
|
|
300
|
+
or type(payload["unique_records"]) is not int
|
|
301
|
+
or type(payload["conflicting_identifiers"]) is not int
|
|
302
|
+
or payload["root_sha256"] != expected_root_sha256
|
|
303
|
+
):
|
|
304
|
+
raise CatalogFillRefused("DATAGOV_PASS_MAP", "fill.pass_map", "pass manifest is invalid")
|
|
305
|
+
manifest = {key: value for key, value in payload.items() if key != "root_sha256"}
|
|
306
|
+
if canonical_sha256(manifest) != expected_root_sha256:
|
|
307
|
+
raise CatalogFillRefused("DATAGOV_PASS_MAP", "fill.pass_map", "pass manifest root differs")
|
|
308
|
+
prior_partition: str | None = None
|
|
309
|
+
records = 0
|
|
310
|
+
conflicts = 0
|
|
311
|
+
for partition in payload["partitions"]:
|
|
312
|
+
if (
|
|
313
|
+
not isinstance(partition, dict)
|
|
314
|
+
or set(partition)
|
|
315
|
+
!= {
|
|
316
|
+
"partition",
|
|
317
|
+
"run_sha256s",
|
|
318
|
+
"record_count",
|
|
319
|
+
"conflicting_identifiers",
|
|
320
|
+
}
|
|
321
|
+
or not _valid_partition(partition["partition"])
|
|
322
|
+
or (prior_partition is not None and partition["partition"] <= prior_partition)
|
|
323
|
+
or not isinstance(partition["run_sha256s"], list)
|
|
324
|
+
or not partition["run_sha256s"]
|
|
325
|
+
or type(partition["record_count"]) is not int
|
|
326
|
+
or type(partition["conflicting_identifiers"]) is not int
|
|
327
|
+
):
|
|
328
|
+
raise CatalogFillRefused(
|
|
329
|
+
"DATAGOV_PASS_MAP", "fill.pass_map", "partition manifest is invalid"
|
|
330
|
+
)
|
|
331
|
+
run_records = 0
|
|
332
|
+
run_conflicts = 0
|
|
333
|
+
for digest in partition["run_sha256s"]:
|
|
334
|
+
count, conflict_count = validate_partition_run(
|
|
335
|
+
staging,
|
|
336
|
+
{"partition": partition["partition"], "run_sha256": digest},
|
|
337
|
+
)
|
|
338
|
+
run_records += count
|
|
339
|
+
run_conflicts += conflict_count
|
|
340
|
+
if (
|
|
341
|
+
run_records != partition["record_count"]
|
|
342
|
+
or run_conflicts != partition["conflicting_identifiers"]
|
|
343
|
+
):
|
|
344
|
+
raise CatalogFillRefused("DATAGOV_PASS_MAP", "fill.pass_map", "partition totals differ")
|
|
345
|
+
records += run_records
|
|
346
|
+
conflicts += run_conflicts
|
|
347
|
+
prior_partition = partition["partition"]
|
|
348
|
+
if records != payload["unique_records"] or conflicts != payload["conflicting_identifiers"]:
|
|
349
|
+
raise CatalogFillRefused("DATAGOV_PASS_MAP", "fill.pass_map", "pass totals differ")
|
|
350
|
+
return PassMapResult(
|
|
351
|
+
root_sha256=expected_root_sha256,
|
|
352
|
+
manifest_sha256=manifest_sha256,
|
|
353
|
+
unique_records=records,
|
|
354
|
+
conflicting_identifiers=conflicts,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _write_run(
|
|
359
|
+
staging: FillStaging,
|
|
360
|
+
*,
|
|
361
|
+
partition: str,
|
|
362
|
+
entries: list[tuple[str, str]],
|
|
363
|
+
conflicting_identifiers: set[str],
|
|
364
|
+
) -> str:
|
|
365
|
+
if not _valid_partition(partition) or not 1 <= len(entries) <= RUN_MAX_ITEMS:
|
|
366
|
+
raise CatalogFillRefused(
|
|
367
|
+
"DATAGOV_PARTITION", "fill.partition_run", "bounded run is invalid"
|
|
368
|
+
)
|
|
369
|
+
return staging.write_shard(
|
|
370
|
+
"partition_run",
|
|
371
|
+
{
|
|
372
|
+
"partition": partition,
|
|
373
|
+
"entries": [
|
|
374
|
+
{"identifier": identifier, "semantic_sha256": semantic}
|
|
375
|
+
for identifier, semantic in entries
|
|
376
|
+
],
|
|
377
|
+
"conflicting_identifiers": sorted(conflicting_identifiers),
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _validate_run(
|
|
383
|
+
payload: dict[str, Any], expected_partition: str
|
|
384
|
+
) -> tuple[list[tuple[str, str]], set[str]]:
|
|
385
|
+
if (
|
|
386
|
+
set(payload) != {"partition", "entries", "conflicting_identifiers"}
|
|
387
|
+
or payload["partition"] != expected_partition
|
|
388
|
+
or not isinstance(payload["entries"], list)
|
|
389
|
+
or not 1 <= len(payload["entries"]) <= RUN_MAX_ITEMS
|
|
390
|
+
or not isinstance(payload["conflicting_identifiers"], list)
|
|
391
|
+
):
|
|
392
|
+
raise CatalogFillRefused(
|
|
393
|
+
"DATAGOV_PARTITION", "fill.partition_run", "run payload is invalid"
|
|
394
|
+
)
|
|
395
|
+
result: list[tuple[str, str]] = []
|
|
396
|
+
for item in payload["entries"]:
|
|
397
|
+
if (
|
|
398
|
+
not isinstance(item, dict)
|
|
399
|
+
or set(item) != {"identifier", "semantic_sha256"}
|
|
400
|
+
or not isinstance(item["identifier"], str)
|
|
401
|
+
or not _is_digest(item["semantic_sha256"])
|
|
402
|
+
):
|
|
403
|
+
raise CatalogFillRefused(
|
|
404
|
+
"DATAGOV_PARTITION", "fill.partition_run", "run entry is invalid"
|
|
405
|
+
)
|
|
406
|
+
if (
|
|
407
|
+
sha256_bytes(item["identifier"].encode("utf-8", errors="strict"))[:2]
|
|
408
|
+
!= expected_partition
|
|
409
|
+
):
|
|
410
|
+
raise CatalogFillRefused(
|
|
411
|
+
"DATAGOV_PARTITION", "fill.partition_run", "entry is in the wrong partition"
|
|
412
|
+
)
|
|
413
|
+
result.append((item["identifier"], item["semantic_sha256"]))
|
|
414
|
+
if result != sorted(result) or len({identifier for identifier, _ in result}) != len(result):
|
|
415
|
+
raise CatalogFillRefused(
|
|
416
|
+
"DATAGOV_PARTITION", "fill.partition_run", "run is not unique and sorted"
|
|
417
|
+
)
|
|
418
|
+
conflicts = payload["conflicting_identifiers"]
|
|
419
|
+
identifiers = {identifier for identifier, _semantic in result}
|
|
420
|
+
if (
|
|
421
|
+
any(not isinstance(identifier, str) for identifier in conflicts)
|
|
422
|
+
or conflicts != sorted(set(conflicts))
|
|
423
|
+
or not set(conflicts) <= identifiers
|
|
424
|
+
):
|
|
425
|
+
raise CatalogFillRefused(
|
|
426
|
+
"DATAGOV_PARTITION", "fill.partition_run", "conflict membership is invalid"
|
|
427
|
+
)
|
|
428
|
+
return result, set(conflicts)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _deduplicate_sorted(
|
|
432
|
+
entries: list[tuple[str, str]],
|
|
433
|
+
) -> tuple[list[tuple[str, str]], set[str]]:
|
|
434
|
+
result: list[tuple[str, str]] = []
|
|
435
|
+
conflicts: set[str] = set()
|
|
436
|
+
for identifier, semantic in sorted(entries):
|
|
437
|
+
if result and result[-1][0] == identifier:
|
|
438
|
+
if result[-1][1] != semantic:
|
|
439
|
+
conflicts.add(identifier)
|
|
440
|
+
result[-1] = (identifier, min(result[-1][1], semantic))
|
|
441
|
+
continue
|
|
442
|
+
result.append((identifier, semantic))
|
|
443
|
+
return result, conflicts
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _valid_partition(value: Any) -> bool:
|
|
447
|
+
return (
|
|
448
|
+
isinstance(value, str)
|
|
449
|
+
and len(value) == 2
|
|
450
|
+
and all(character in "0123456789abcdef" for character in value)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _is_digest(value: Any) -> bool:
|
|
455
|
+
return (
|
|
456
|
+
isinstance(value, str)
|
|
457
|
+
and len(value) == 64
|
|
458
|
+
and all(character in "0123456789abcdef" for character in value)
|
|
459
|
+
)
|