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,384 @@
|
|
|
1
|
+
"""Parse one STAC ``/collections`` response into public-source records.
|
|
2
|
+
|
|
3
|
+
Records declare JSON because it is the supported STAC response encoding. Asset encodings remain
|
|
4
|
+
in descriptive text. Missing optional fields are accepted. Text and identifiers are bounded by
|
|
5
|
+
the shared harvest protocol; unusable records are skipped.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from mostlyright.data_harness.canonical import CanonicalJSONError, parse_foreign_json
|
|
13
|
+
from mostlyright.data_harness.sources.catalog.harvest.ckan import MAX_HARVEST_RECORDS
|
|
14
|
+
from mostlyright.data_harness.sources.catalog.harvest.protocol import (
|
|
15
|
+
MAX_RECORD_PUBLISHER,
|
|
16
|
+
MAX_RECORD_TEXT,
|
|
17
|
+
MAX_RECORD_TITLE,
|
|
18
|
+
CatalogHarvestError,
|
|
19
|
+
HarvestCursor,
|
|
20
|
+
HarvestedRecord,
|
|
21
|
+
HarvesterDescriptor,
|
|
22
|
+
HarvestPage,
|
|
23
|
+
HarvestResponseEvidence,
|
|
24
|
+
bounded_record_text,
|
|
25
|
+
carries_a_zone_offset,
|
|
26
|
+
declared_timestamp,
|
|
27
|
+
harvest_limits,
|
|
28
|
+
usable_https_uri,
|
|
29
|
+
usable_license_id,
|
|
30
|
+
usable_record_id,
|
|
31
|
+
)
|
|
32
|
+
from mostlyright.data_harness.sources.contracts import EvidenceReference
|
|
33
|
+
|
|
34
|
+
STAC_LIMITS = harvest_limits("application/json", max_response_bytes=2 * 1024 * 1024)
|
|
35
|
+
|
|
36
|
+
SUPPORTED_STAC_MAJOR = "1."
|
|
37
|
+
|
|
38
|
+
ATTRIBUTION = "attribution"
|
|
39
|
+
|
|
40
|
+
# SPDX identifiers, which is a different vocabulary from CKAN's licence register -- hence a second
|
|
41
|
+
# table rather than a shared one. The mapping idiom is the same; the identifiers genuinely differ.
|
|
42
|
+
STAC_LICENCE_RIGHTS: dict[str, tuple[str, tuple[str, ...], str | None]] = {
|
|
43
|
+
"cc0-1.0": ("approved", ("analysis", "publication"), None),
|
|
44
|
+
"public-domain": ("approved", ("analysis", "publication"), None),
|
|
45
|
+
"pddl-1.0": ("approved", ("analysis", "publication"), None),
|
|
46
|
+
"cc-by-4.0": ("conditional", ("analysis", "publication"), ATTRIBUTION),
|
|
47
|
+
"cc-by-sa-4.0": ("conditional", ("analysis", "publication"), ATTRIBUTION),
|
|
48
|
+
"odc-by-1.0": ("conditional", ("analysis", "publication"), ATTRIBUTION),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
UNCLEAR_LICENCE_IDS = frozenset({"", "proprietary", "various", "other", "unknown"})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def stac_licence_rights(license_id: str | None) -> tuple[str, tuple[str, ...], str | None]:
|
|
55
|
+
"""Map a STAC SPDX licence identifier onto a rights status, allowed uses and an obligation."""
|
|
56
|
+
|
|
57
|
+
if license_id is None:
|
|
58
|
+
return ("unclear", (), None)
|
|
59
|
+
normalized = license_id.strip().lower()
|
|
60
|
+
if normalized in UNCLEAR_LICENCE_IDS:
|
|
61
|
+
return ("unclear", (), None)
|
|
62
|
+
return STAC_LICENCE_RIGHTS.get(normalized, ("unclear", (), None))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StacHarvester:
|
|
66
|
+
"""Read a STAC ``/collections`` document. Takes bytes; never fetches."""
|
|
67
|
+
|
|
68
|
+
_DESCRIPTOR = HarvesterDescriptor(
|
|
69
|
+
protocol="stac",
|
|
70
|
+
harvester_id="stac.collections",
|
|
71
|
+
harvester_version="1.0.0",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def descriptor(self) -> HarvesterDescriptor:
|
|
76
|
+
return self._DESCRIPTOR
|
|
77
|
+
|
|
78
|
+
def parse(
|
|
79
|
+
self,
|
|
80
|
+
payload: bytes,
|
|
81
|
+
*,
|
|
82
|
+
uri: str,
|
|
83
|
+
observed_at: str,
|
|
84
|
+
evidence: EvidenceReference,
|
|
85
|
+
) -> tuple[HarvestedRecord, ...]:
|
|
86
|
+
page = self.parse_page(
|
|
87
|
+
payload,
|
|
88
|
+
uri=uri,
|
|
89
|
+
observed_at=observed_at,
|
|
90
|
+
evidence=evidence,
|
|
91
|
+
response_evidence=None,
|
|
92
|
+
cursor=None,
|
|
93
|
+
)
|
|
94
|
+
if not page.records:
|
|
95
|
+
raise CatalogHarvestError(
|
|
96
|
+
"HARVEST_SHAPE", "stac.collections", "no collection carried a usable identity"
|
|
97
|
+
)
|
|
98
|
+
return page.records
|
|
99
|
+
|
|
100
|
+
def parse_page(
|
|
101
|
+
self,
|
|
102
|
+
payload: bytes,
|
|
103
|
+
*,
|
|
104
|
+
uri: str,
|
|
105
|
+
observed_at: str,
|
|
106
|
+
evidence: EvidenceReference,
|
|
107
|
+
response_evidence: HarvestResponseEvidence | None,
|
|
108
|
+
cursor: HarvestCursor | None,
|
|
109
|
+
) -> HarvestPage:
|
|
110
|
+
if cursor is not None and (cursor.protocol != "stac" or cursor.value != uri):
|
|
111
|
+
raise CatalogHarvestError("HARVEST_CURSOR", "stac.cursor", "cursor mismatch")
|
|
112
|
+
document = _document(payload)
|
|
113
|
+
collections = document.get("collections")
|
|
114
|
+
if not isinstance(collections, list):
|
|
115
|
+
raise CatalogHarvestError(
|
|
116
|
+
"HARVEST_SHAPE",
|
|
117
|
+
"stac.collections",
|
|
118
|
+
"a STAC collections response must carry a collections array",
|
|
119
|
+
)
|
|
120
|
+
if len(collections) > MAX_HARVEST_RECORDS:
|
|
121
|
+
raise CatalogHarvestError(
|
|
122
|
+
"HARVEST_RECORD_LIMIT",
|
|
123
|
+
"stac.collections",
|
|
124
|
+
f"a single response may not carry more than {MAX_HARVEST_RECORDS} collections",
|
|
125
|
+
)
|
|
126
|
+
skipped: list[str] = []
|
|
127
|
+
records: list[HarvestedRecord] = []
|
|
128
|
+
for position, collection in enumerate(collections):
|
|
129
|
+
if not isinstance(collection, dict):
|
|
130
|
+
raise CatalogHarvestError(
|
|
131
|
+
"HARVEST_SHAPE",
|
|
132
|
+
f"stac.collections[{position}]",
|
|
133
|
+
"each collection must be an object",
|
|
134
|
+
)
|
|
135
|
+
_require_supported_version(collection, document, position)
|
|
136
|
+
record = _record(
|
|
137
|
+
collection,
|
|
138
|
+
uri=uri,
|
|
139
|
+
observed_at=observed_at,
|
|
140
|
+
evidence=evidence,
|
|
141
|
+
skipped=skipped,
|
|
142
|
+
)
|
|
143
|
+
if record is not None:
|
|
144
|
+
records.append(record)
|
|
145
|
+
next_uri = _next_link(document)
|
|
146
|
+
provider_count = _matched_count(document)
|
|
147
|
+
if not collections and next_uri is not None:
|
|
148
|
+
raise CatalogHarvestError(
|
|
149
|
+
"HARVEST_EMPTY_PAGE", "stac.collections", "an empty page may not continue"
|
|
150
|
+
)
|
|
151
|
+
return HarvestPage(
|
|
152
|
+
protocol="stac",
|
|
153
|
+
records=tuple(records),
|
|
154
|
+
next_cursor=(
|
|
155
|
+
HarvestCursor(protocol="stac", kind="next", value=next_uri)
|
|
156
|
+
if next_uri is not None
|
|
157
|
+
else None
|
|
158
|
+
),
|
|
159
|
+
provider_count=provider_count,
|
|
160
|
+
count_basis="reported_total" if provider_count is not None else "not_reported",
|
|
161
|
+
skipped_record_ids=tuple(sorted(skipped)),
|
|
162
|
+
evidence=evidence,
|
|
163
|
+
response_evidence=response_evidence,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _next_link(document: dict[str, Any]) -> str | None:
|
|
168
|
+
links = document.get("links")
|
|
169
|
+
if links is None:
|
|
170
|
+
return None
|
|
171
|
+
if not isinstance(links, list):
|
|
172
|
+
raise CatalogHarvestError("HARVEST_SHAPE", "stac.links", "links must be an array")
|
|
173
|
+
found: list[str] = []
|
|
174
|
+
for position, link in enumerate(links):
|
|
175
|
+
if not isinstance(link, dict):
|
|
176
|
+
raise CatalogHarvestError(
|
|
177
|
+
"HARVEST_SHAPE", f"stac.links[{position}]", "each link must be an object"
|
|
178
|
+
)
|
|
179
|
+
if link.get("rel") != "next":
|
|
180
|
+
continue
|
|
181
|
+
href = usable_https_uri(link.get("href"))
|
|
182
|
+
if href is None:
|
|
183
|
+
raise CatalogHarvestError(
|
|
184
|
+
"HARVEST_CURSOR", f"stac.links[{position}].href", "next must be bounded HTTPS"
|
|
185
|
+
)
|
|
186
|
+
found.append(href)
|
|
187
|
+
if len(found) > 1:
|
|
188
|
+
raise CatalogHarvestError("HARVEST_CURSOR", "stac.links", "at most one next link is valid")
|
|
189
|
+
return found[0] if found else None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _matched_count(document: dict[str, Any]) -> int | None:
|
|
193
|
+
value = document.get("numberMatched")
|
|
194
|
+
context = document.get("context")
|
|
195
|
+
if value is None and isinstance(context, dict):
|
|
196
|
+
value = context.get("matched")
|
|
197
|
+
if value is None:
|
|
198
|
+
return None
|
|
199
|
+
if type(value) is not int or value < 0:
|
|
200
|
+
raise CatalogHarvestError("HARVEST_COUNT", "stac.matched", "must be nonnegative integer")
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _document(payload: bytes) -> dict[str, Any]:
|
|
205
|
+
# Fraction-tolerant on purpose. A STAC Collection is required by its own specification to carry
|
|
206
|
+
# `extent.spatial.bbox`, and every real bbox is fractional; so are `gsd`, `center_wavelength`,
|
|
207
|
+
# `proj:transform` and most `summaries` values. Parsing these with the integer-only canonical
|
|
208
|
+
# parser refuses every genuine /collections response. `parse_foreign_json` keeps the rest of the
|
|
209
|
+
# hardening -- duplicate keys, byte-order marks, strict UTF-8, NaN/Infinity, integer range --
|
|
210
|
+
# and no float reaches a `HarvestedRecord`: every field below is read through `_string`.
|
|
211
|
+
try:
|
|
212
|
+
document = parse_foreign_json(payload)
|
|
213
|
+
except CanonicalJSONError as error:
|
|
214
|
+
raise CatalogHarvestError(
|
|
215
|
+
"HARVEST_SHAPE",
|
|
216
|
+
"stac.response",
|
|
217
|
+
f"the response is not strict JSON: {error.detail}",
|
|
218
|
+
) from None
|
|
219
|
+
if not isinstance(document, dict):
|
|
220
|
+
raise CatalogHarvestError(
|
|
221
|
+
"HARVEST_SHAPE",
|
|
222
|
+
"stac.response",
|
|
223
|
+
"a STAC response must be a JSON object",
|
|
224
|
+
)
|
|
225
|
+
return document
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _require_supported_version(
|
|
229
|
+
collection: dict[str, Any],
|
|
230
|
+
document: dict[str, Any],
|
|
231
|
+
position: int,
|
|
232
|
+
) -> None:
|
|
233
|
+
declared = collection.get("stac_version") or document.get("stac_version")
|
|
234
|
+
if not isinstance(declared, str) or not declared.startswith(SUPPORTED_STAC_MAJOR):
|
|
235
|
+
raise CatalogHarvestError(
|
|
236
|
+
"HARVEST_STAC_VERSION",
|
|
237
|
+
f"stac.collections[{position}].stac_version",
|
|
238
|
+
f"this harvester reads STAC {SUPPORTED_STAC_MAJOR}x, found {declared!r}",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _record(
|
|
243
|
+
collection: dict[str, Any],
|
|
244
|
+
*,
|
|
245
|
+
uri: str,
|
|
246
|
+
observed_at: str,
|
|
247
|
+
evidence: EvidenceReference,
|
|
248
|
+
skipped: list[str],
|
|
249
|
+
) -> HarvestedRecord | None:
|
|
250
|
+
collection_id = usable_record_id(collection.get("id"))
|
|
251
|
+
if collection_id is None:
|
|
252
|
+
skipped.append("<collection without a usable id>")
|
|
253
|
+
return None
|
|
254
|
+
landing = usable_https_uri(_self_link(collection)) or usable_https_uri(
|
|
255
|
+
_collection_uri(uri, collection_id)
|
|
256
|
+
)
|
|
257
|
+
if landing is None:
|
|
258
|
+
skipped.append(collection_id)
|
|
259
|
+
return None
|
|
260
|
+
encodings = _asset_encodings(collection)
|
|
261
|
+
description = _string(collection.get("description")) or "No description published."
|
|
262
|
+
if encodings:
|
|
263
|
+
description = (
|
|
264
|
+
f"{description} Published asset encodings: {', '.join(encodings)}. "
|
|
265
|
+
"The harness reads this catalogue as JSON metadata; those encodings need a Reader."
|
|
266
|
+
)
|
|
267
|
+
title = _string(collection.get("title"))
|
|
268
|
+
return HarvestedRecord(
|
|
269
|
+
protocol="stac",
|
|
270
|
+
record_id=collection_id,
|
|
271
|
+
title=bounded_record_text(title, maximum=MAX_RECORD_TITLE) if title else collection_id,
|
|
272
|
+
description=bounded_record_text(description, maximum=MAX_RECORD_TEXT),
|
|
273
|
+
publisher=bounded_record_text(_publisher(collection), maximum=MAX_RECORD_PUBLISHER),
|
|
274
|
+
landing_uri=landing,
|
|
275
|
+
data_formats=("json",),
|
|
276
|
+
evidence=evidence,
|
|
277
|
+
license_id=usable_license_id(collection.get("license")),
|
|
278
|
+
license_uri=None,
|
|
279
|
+
declared_updated_at=_temporal_end(collection),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _publisher(collection: dict[str, Any]) -> str:
|
|
284
|
+
providers = collection.get("providers")
|
|
285
|
+
if isinstance(providers, list):
|
|
286
|
+
for provider in providers:
|
|
287
|
+
if isinstance(provider, dict):
|
|
288
|
+
name = _string(provider.get("name"))
|
|
289
|
+
if name is not None:
|
|
290
|
+
return name
|
|
291
|
+
return "Unnamed publisher"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _self_link(collection: dict[str, Any]) -> str | None:
|
|
295
|
+
links = collection.get("links")
|
|
296
|
+
if not isinstance(links, list):
|
|
297
|
+
return None
|
|
298
|
+
for link in links:
|
|
299
|
+
if not isinstance(link, dict) or link.get("rel") != "self":
|
|
300
|
+
continue
|
|
301
|
+
href = _string(link.get("href"))
|
|
302
|
+
if href is not None and href.startswith("https://"):
|
|
303
|
+
return href
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _collection_uri(uri: str, collection_id: str) -> str | None:
|
|
308
|
+
if not uri.startswith("https://"):
|
|
309
|
+
return None
|
|
310
|
+
base = uri.split("?", 1)[0].rstrip("/")
|
|
311
|
+
return f"{base}/{collection_id}"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _asset_encodings(collection: dict[str, Any]) -> tuple[str, ...]:
|
|
315
|
+
"""The media types this collection's item assets really publish, in declared order."""
|
|
316
|
+
|
|
317
|
+
assets = collection.get("item_assets")
|
|
318
|
+
if not isinstance(assets, dict):
|
|
319
|
+
return ()
|
|
320
|
+
encodings: list[str] = []
|
|
321
|
+
for value in assets.values():
|
|
322
|
+
if not isinstance(value, dict):
|
|
323
|
+
continue
|
|
324
|
+
media_type = _string(value.get("type"))
|
|
325
|
+
if media_type is not None and media_type not in encodings:
|
|
326
|
+
encodings.append(media_type)
|
|
327
|
+
return tuple(encodings[:16])
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _temporal_end(collection: dict[str, Any]) -> str | None:
|
|
331
|
+
"""The end of the first temporal interval, when the publisher declares a concrete one.
|
|
332
|
+
|
|
333
|
+
An open-ended interval leaves this ``None``. Reading it as "now" would invent a fact the
|
|
334
|
+
catalogue did not state, and cadence is one of the things the facts gate decides on.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
extent = collection.get("extent")
|
|
338
|
+
if not isinstance(extent, dict):
|
|
339
|
+
return None
|
|
340
|
+
temporal = extent.get("temporal")
|
|
341
|
+
if not isinstance(temporal, dict):
|
|
342
|
+
return None
|
|
343
|
+
interval = temporal.get("interval")
|
|
344
|
+
if not isinstance(interval, list) or not interval:
|
|
345
|
+
return None
|
|
346
|
+
first = interval[0]
|
|
347
|
+
if not isinstance(first, list) or len(first) < 2:
|
|
348
|
+
return None
|
|
349
|
+
end = _string(first[1])
|
|
350
|
+
if end is None:
|
|
351
|
+
return None
|
|
352
|
+
return _stac_timestamp(end)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _stac_timestamp(value: str) -> str | None:
|
|
356
|
+
"""Read a temporal-interval bound as a canonical UTC timestamp, or not at all.
|
|
357
|
+
|
|
358
|
+
STAC requires UTC, but the interval is publisher text and a real instance spells it however it
|
|
359
|
+
likes. The normalised result goes through ``declared_timestamp`` and becomes an undeclared
|
|
360
|
+
update time when it is not canonical -- without that, one collection's zone-bearing or
|
|
361
|
+
malformed bound refused every other collection in the same response. See that function.
|
|
362
|
+
|
|
363
|
+
The offset check runs before the fractional second is cut, because the offset rides on the
|
|
364
|
+
fraction and was being cut away with it -- see ``carries_a_zone_offset``.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
if carries_a_zone_offset(value.strip()):
|
|
368
|
+
return None
|
|
369
|
+
text = value.strip()
|
|
370
|
+
if text.endswith("Z"):
|
|
371
|
+
text = text[:-1]
|
|
372
|
+
if "." in text:
|
|
373
|
+
head, _, fraction = text.partition(".")
|
|
374
|
+
text = f"{head}.{fraction[:6]}" if fraction[:6].isdigit() else head
|
|
375
|
+
if len(text) < 19:
|
|
376
|
+
return None
|
|
377
|
+
return declared_timestamp(f"{text}Z")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _string(value: Any) -> str | None:
|
|
381
|
+
if not isinstance(value, str):
|
|
382
|
+
return None
|
|
383
|
+
text = value.strip()
|
|
384
|
+
return text or None
|