defair-data 0.4.0rc2__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.
- defair_data/__init__.py +113 -0
- defair_data/_grid_split.py +457 -0
- defair_data/_logging_defaults.py +63 -0
- defair_data/_path_utils.py +54 -0
- defair_data/_types.py +75 -0
- defair_data/cdm.py +1404 -0
- defair_data/cf_flags.py +459 -0
- defair_data/constants.py +19 -0
- defair_data/core.py +3025 -0
- defair_data/dask_manager.py +1343 -0
- defair_data/data_reader_plugin.py +545 -0
- defair_data/data_source_plugin.py +334 -0
- defair_data/data_writer_plugin.py +186 -0
- defair_data/logging.py +34 -0
- defair_data/py.typed +0 -0
- defair_data/readers/__init__.py +102 -0
- defair_data/readers/_metop_products.py +310 -0
- defair_data/readers/_mtg_chunk_naming.py +41 -0
- defair_data/readers/_reader_metadata.py +420 -0
- defair_data/readers/_time_utils.py +93 -0
- defair_data/readers/era5_grib/__init__.py +21 -0
- defair_data/readers/era5_grib/era5_grib_reader_plugin.py +901 -0
- defair_data/readers/metop/__init__.py +163 -0
- defair_data/readers/metop/_eps_native_base_reader.py +911 -0
- defair_data/readers/metop/_eps_parser.py +1636 -0
- defair_data/readers/metop/_gome2_parser.py +1259 -0
- defair_data/readers/metop/_iasi_snd_parser.py +1591 -0
- defair_data/readers/metop/_iasi_utils.py +65 -0
- defair_data/readers/metop/_metop_base_reader.py +904 -0
- defair_data/readers/metop/_metop_coordinate_utils.py +251 -0
- defair_data/readers/metop/_metop_metadata.py +2555 -0
- defair_data/readers/metop/_metop_netcdf_gridded_base.py +298 -0
- defair_data/readers/metop/_metop_netcdf_swath_base.py +376 -0
- defair_data/readers/metop/_netcdf_engine.py +116 -0
- defair_data/readers/metop/metop_amsul1_reader_plugin.py +268 -0
- defair_data/readers/metop/metop_ascszf1b_reader_plugin.py +567 -0
- defair_data/readers/metop/metop_ascszfr02_reader_plugin.py +70 -0
- defair_data/readers/metop/metop_ascszo1b_reader_plugin.py +544 -0
- defair_data/readers/metop/metop_ascszor02_reader_plugin.py +270 -0
- defair_data/readers/metop/metop_ascszr1b_reader_plugin.py +660 -0
- defair_data/readers/metop/metop_ascszrr02_reader_plugin.py +410 -0
- defair_data/readers/metop/metop_avhrr_amv_reader_plugin.py +392 -0
- defair_data/readers/metop/metop_avhrrl1_reader_plugin.py +1116 -0
- defair_data/readers/metop/metop_edlst_reader_plugin.py +747 -0
- defair_data/readers/metop/metop_glbsst_reader_plugin.py +100 -0
- defair_data/readers/metop/metop_gomel1_reader_plugin.py +1402 -0
- defair_data/readers/metop/metop_gomel1r03_reader_plugin.py +84 -0
- defair_data/readers/metop/metop_hirs_fdr_reader_plugin.py +494 -0
- defair_data/readers/metop/metop_hirsl1_reader_plugin.py +280 -0
- defair_data/readers/metop/metop_iasil1c_all_reader_plugin.py +537 -0
- defair_data/readers/metop/metop_iasisnd02_reader_plugin.py +1109 -0
- defair_data/readers/metop/metop_iasthr011_reader_plugin.py +714 -0
- defair_data/readers/metop/metop_mhsl1_reader_plugin.py +163 -0
- defair_data/readers/metop/metop_osi104_reader_plugin.py +239 -0
- defair_data/readers/metop/metop_osi150a_reader_plugin.py +90 -0
- defair_data/readers/metop/metop_osi150b_reader_plugin.py +95 -0
- defair_data/readers/metop/metop_somo12_reader_plugin.py +353 -0
- defair_data/readers/metop/metop_somo25_reader_plugin.py +347 -0
- defair_data/readers/metop/resources/eps_native_format/AMSAL1/band_to_record.yaml +279 -0
- defair_data/readers/metop/resources/eps_native_format/AMSAL1/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/AMSAL1/giadr_2_3.csv +86 -0
- defair_data/readers/metop/resources/eps_native_format/AMSAL1/mdr_2_3.csv +131 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/band_to_record.yaml +102 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/mdr_3_3.csv +21 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/mdr_3_4.csv +25 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/mdr_3_5.csv +25 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/viadr_4_2.csv +9 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/viadr_6_2.csv +15 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/band_to_record.yaml +561 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/mdr_2_2.csv +27 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/mdr_2_3.csv +30 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/mdr_2_4.csv +26 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/viadr_4_2.csv +9 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/viadr_6_2.csv +15 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/band_to_record.yaml +628 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/mdr_1_2.csv +27 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/mdr_1_3.csv +30 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/mdr_1_4.csv +26 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/viadr_4_2.csv +9 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/viadr_6_2.csv +15 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/band_to_record.yaml +282 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/mdr_5_0.csv +40 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/mdr_5_1.csv +51 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/mdr_5_2.csv +51 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/viadr_7_2.csv +30 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/band_to_record.yaml +271 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/mdr_4_0.csv +40 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/mdr_4_1.csv +51 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/mdr_4_2.csv +55 -0
- defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/viadr_7_2.csv +30 -0
- defair_data/readers/metop/resources/eps_native_format/AVHRRL1/band_to_record.yaml +198 -0
- defair_data/readers/metop/resources/eps_native_format/AVHRRL1/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/AVHRRL1/giadr_1_3.csv +50 -0
- defair_data/readers/metop/resources/eps_native_format/AVHRRL1/giadr_2_2.csv +114 -0
- defair_data/readers/metop/resources/eps_native_format/AVHRRL1/mdr_2_4.csv +85 -0
- defair_data/readers/metop/resources/eps_native_format/AVHRRL1/mdr_2_5.csv +85 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_4.csv +10 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_5.csv +10 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_6.csv +5 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_7.csv +7 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_6_5.csv +102 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_6_6.csv +105 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_7_fmv12.csv +38 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_7_fmv13.csv +39 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_8_fmv12.csv +40 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_8_fmv13.csv +41 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_9_fmv12.csv +44 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_9_fmv13.csv +45 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_band_m.csv +4 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_band_p.csv +5 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_geo_earth_actual.csv +10 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_polss.csv +6 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_polv.csv +4 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/sphr_1.csv +99 -0
- defair_data/readers/metop/resources/eps_native_format/GOME2L1/viadr_5.csv +16 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/band_to_record.yaml +328 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/giadr_1_1.csv +21 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/giadr_2_1.csv +17 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/giadr_3_1.csv +5 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/mdr_1_2.csv +59 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/viadr_1_1.csv +11 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/band_to_record.yaml +229 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/giadr_1_1.csv +21 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/giadr_2_1.csv +17 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/giadr_3_1.csv +5 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/mdr_1_2.csv +59 -0
- defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/viadr_1_1.csv +11 -0
- defair_data/readers/metop/resources/eps_native_format/HIRSL1/band_to_record.yaml +326 -0
- defair_data/readers/metop/resources/eps_native_format/HIRSL1/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/HIRSL1/giadr_1_2.csv +13 -0
- defair_data/readers/metop/resources/eps_native_format/HIRSL1/giadr_2_2.csv +21 -0
- defair_data/readers/metop/resources/eps_native_format/HIRSL1/mdr_2_2.csv +39 -0
- defair_data/readers/metop/resources/eps_native_format/IASIL1/band_to_record.yaml +217 -0
- defair_data/readers/metop/resources/eps_native_format/IASIL1/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/IASIL1/giadr_0_2.csv +16 -0
- defair_data/readers/metop/resources/eps_native_format/IASIL1/mdr_2_5.csv +65 -0
- defair_data/readers/metop/resources/eps_native_format/IASISND02/band_to_record.yaml +790 -0
- defair_data/readers/metop/resources/eps_native_format/IASISND02/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/IASISND02/giadr_1_3.csv +12 -0
- defair_data/readers/metop/resources/eps_native_format/IASISND02/giadr_1_4.csv +26 -0
- defair_data/readers/metop/resources/eps_native_format/IASISND02/mdr_1_3.csv +76 -0
- defair_data/readers/metop/resources/eps_native_format/IASISND02/mdr_1_4.csv +94 -0
- defair_data/readers/metop/resources/eps_native_format/MHSL1/band_to_record.yaml +169 -0
- defair_data/readers/metop/resources/eps_native_format/MHSL1/general_metadata.yaml +11 -0
- defair_data/readers/metop/resources/eps_native_format/MHSL1/giadr_1_3.csv +14 -0
- defair_data/readers/metop/resources/eps_native_format/MHSL1/giadr_2_3.csv +81 -0
- defair_data/readers/metop/resources/eps_native_format/MHSL1/giadr_3_1.csv +35 -0
- defair_data/readers/metop/resources/eps_native_format/MHSL1/mdr_2_3.csv +101 -0
- defair_data/readers/metop/resources/eps_native_format/mphr_0_2.csv +75 -0
- defair_data/readers/msg15/__init__.py +21 -0
- defair_data/readers/msg15/_coordinate_cache.py +276 -0
- defair_data/readers/msg15/_dt_msg.py +239 -0
- defair_data/readers/msg15/_dt_msg_15_native_reader.py +849 -0
- defair_data/readers/msg15/_msg15_xarray_backend.py +911 -0
- defair_data/readers/msg15/_msg_aux_metadata.py +184 -0
- defair_data/readers/msg15/_seviri_bt_coeffs.py +111 -0
- defair_data/readers/msg15/_seviri_refl_coeffs.py +89 -0
- defair_data/readers/msg15/msg15_native_reader_plugin.py +2364 -0
- defair_data/readers/msg15/resources/msg_native_format/records/15_header.csv +9 -0
- defair_data/readers/msg15/resources/msg_native_format/records/15_trailer.csv +7 -0
- defair_data/readers/msg15/resources/msg_native_format/sections/15_header.csv +14681 -0
- defair_data/readers/msg15/resources/msg_native_format/sections/15_trailer.csv +3930 -0
- defair_data/readers/mtg_fci/__init__.py +35 -0
- defair_data/readers/mtg_fci/_fci_aux_metadata.py +189 -0
- defair_data/readers/mtg_fci/_fci_bt_coeffs.py +96 -0
- defair_data/readers/mtg_fci/_fci_coordinate_cache.py +251 -0
- defair_data/readers/mtg_fci/_fci_metadata.py +114 -0
- defair_data/readers/mtg_fci/_fci_refl_coeffs.py +74 -0
- defair_data/readers/mtg_fci/_mtg_l2_flag_metadata.py +289 -0
- defair_data/readers/mtg_fci/_mtg_projection_gridded_base.py +844 -0
- defair_data/readers/mtg_fci/_mtg_segment_product_base.py +685 -0
- defair_data/readers/mtg_fci/mtg_fci_l1c_nc_reader_plugin.py +3590 -0
- defair_data/readers/mtg_fci/mtg_l2_amv_reader_plugin.py +429 -0
- defair_data/readers/mtg_fci/mtg_l2_asr_reader_plugin.py +735 -0
- defair_data/readers/mtg_fci/mtg_l2_clm_reader_plugin.py +104 -0
- defair_data/readers/mtg_fci/mtg_l2_gii_reader_plugin.py +127 -0
- defair_data/readers/mtg_fci/mtg_l2_oca_reader_plugin.py +154 -0
- defair_data/readers/mtg_fci/mtg_l2_olr_reader_plugin.py +89 -0
- defair_data/readers/mtg_li/__init__.py +58 -0
- defair_data/readers/mtg_li/_li_base_reader.py +1187 -0
- defair_data/readers/mtg_li/_li_coordinate_cache.py +252 -0
- defair_data/readers/mtg_li/_li_metadata.py +124 -0
- defair_data/readers/mtg_li/_li_point_base_reader.py +1281 -0
- defair_data/readers/mtg_li/_li_point_metadata.py +123 -0
- defair_data/readers/mtg_li/_li_sparse_reconstruction.py +579 -0
- defair_data/readers/mtg_li/mtg_li_af_reader_plugin.py +72 -0
- defair_data/readers/mtg_li/mtg_li_afa_reader_plugin.py +72 -0
- defair_data/readers/mtg_li/mtg_li_afr_reader_plugin.py +72 -0
- defair_data/readers/mtg_li/mtg_li_lef_reader_plugin.py +58 -0
- defair_data/readers/mtg_li/mtg_li_lfl_reader_plugin.py +63 -0
- defair_data/readers/mtg_li/mtg_li_lgr_reader_plugin.py +61 -0
- defair_data/readers/planck.py +193 -0
- defair_data/readers/reflectance.py +120 -0
- defair_data/readers/sentinel3/__init__.py +92 -0
- defair_data/readers/sentinel3/_olci_base.py +435 -0
- defair_data/readers/sentinel3/_olci_l1_base.py +292 -0
- defair_data/readers/sentinel3/_olci_l2_water_base.py +342 -0
- defair_data/readers/sentinel3/_safe_netcdf_merge.py +104 -0
- defair_data/readers/sentinel3/_safe_package.py +587 -0
- defair_data/readers/sentinel3/_sentinel3_base.py +883 -0
- defair_data/readers/sentinel3/_slstr_base.py +111 -0
- defair_data/readers/sentinel3/_sral_base.py +251 -0
- defair_data/readers/sentinel3/sentinel3_aod_reader_plugin.py +341 -0
- defair_data/readers/sentinel3/sentinel3_frp_reader_plugin.py +607 -0
- defair_data/readers/sentinel3/sentinel3_ol_1_efr_reader_plugin.py +55 -0
- defair_data/readers/sentinel3/sentinel3_ol_1_err_reader_plugin.py +51 -0
- defair_data/readers/sentinel3/sentinel3_ol_2_wfr_reader_plugin.py +69 -0
- defair_data/readers/sentinel3/sentinel3_ol_2_wrr_reader_plugin.py +42 -0
- defair_data/readers/sentinel3/sentinel3_sl_1_rbt_reader_plugin.py +625 -0
- defair_data/readers/sentinel3/sentinel3_sr1_sra_a_reader_plugin.py +397 -0
- defair_data/readers/sentinel3/sentinel3_sr1_sra_bs_reader_plugin.py +465 -0
- defair_data/readers/sentinel3/sentinel3_sr1_sra_reader_plugin.py +504 -0
- defair_data/readers/sentinel3/sentinel3_sr2_wat_reader_plugin.py +462 -0
- defair_data/readers/sentinel3/sentinel3_wst_reader_plugin.py +310 -0
- defair_data/sources/__init__.py +95 -0
- defair_data/sources/_zip_resolver.py +374 -0
- defair_data/sources/hda/__init__.py +21 -0
- defair_data/sources/hda/hda_stac_plugin.py +775 -0
- defair_data/sources/local/__init__.py +21 -0
- defair_data/sources/local/local_filesystem_plugin.py +573 -0
- defair_data/sources/s3/__init__.py +22 -0
- defair_data/sources/s3/_s3_kwargs.py +87 -0
- defair_data/sources/s3/s3_base.py +409 -0
- defair_data/sources/s3/s3_boto3_plugin.py +718 -0
- defair_data/sources/s3/s3_fsspec_plugin.py +488 -0
- defair_data/writers/__init__.py +82 -0
- defair_data/writers/_common/__init__.py +16 -0
- defair_data/writers/_common/column_writer.py +554 -0
- defair_data/writers/_common/columnar.py +709 -0
- defair_data/writers/_common/columnar_geometry.py +324 -0
- defair_data/writers/_common/columnar_metadata.py +493 -0
- defair_data/writers/_common/image_utils.py +133 -0
- defair_data/writers/_common/image_writer.py +353 -0
- defair_data/writers/_common/path_layout.py +145 -0
- defair_data/writers/_common/raster_geometry.py +253 -0
- defair_data/writers/_common/variable_filter.py +80 -0
- defair_data/writers/csv/__init__.py +20 -0
- defair_data/writers/csv/csv_writer.py +321 -0
- defair_data/writers/geotiff/geotiff_writer.py +62 -0
- defair_data/writers/hdf5/hdf5_writer.py +179 -0
- defair_data/writers/jpeg/jpeg_writer.py +56 -0
- defair_data/writers/netcdf4/netcdf4_writer.py +486 -0
- defair_data/writers/parquet/__init__.py +20 -0
- defair_data/writers/parquet/parquet_writer.py +321 -0
- defair_data/writers/png/png_writer.py +43 -0
- defair_data/writers/zarr/__init__.py +20 -0
- defair_data/writers/zarr/zarr_writer.py +1866 -0
- defair_data-0.4.0rc2.dist-info/METADATA +63 -0
- defair_data-0.4.0rc2.dist-info/RECORD +259 -0
- defair_data-0.4.0rc2.dist-info/WHEEL +4 -0
- defair_data-0.4.0rc2.dist-info/entry_points.txt +74 -0
- defair_data-0.4.0rc2.dist-info/licenses/LICENSE +175 -0
defair_data/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 EUMETSAT
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
import atexit
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
# CRITICAL: Must be set before ANY module imports zarr
|
|
22
|
+
# Zarr checks this at import time, and xarray imports zarr
|
|
23
|
+
os.environ.setdefault("ZARR_V3_EXPERIMENTAL_API", "1")
|
|
24
|
+
|
|
25
|
+
# Silence noisy S3/HTTP dependency loggers BEFORE any defair / defair_data
|
|
26
|
+
# submodule is imported, so the first boto3/botocore record (e.g. credential
|
|
27
|
+
# discovery on Dataset.from_source) never reaches pytest's caplog or any
|
|
28
|
+
# user-attached root handler. Required for consumers that never invoke
|
|
29
|
+
# ``defair.logging.setup_logging`` — e.g. the vendor pytest suite.
|
|
30
|
+
from defair_data import _logging_defaults as _logging_defaults # noqa: I001 side effect: silence noisy loggers
|
|
31
|
+
from importlib.metadata import version
|
|
32
|
+
|
|
33
|
+
from defair.plugin_manager import load_reader
|
|
34
|
+
from defair_data.constants import CF_VERSION
|
|
35
|
+
from defair_data.core import Dataset
|
|
36
|
+
from defair_data.dask_manager import S3CredentialsPlugin, propagate_s3_credentials
|
|
37
|
+
from defair_data.data_reader_plugin import DataReaderPlugin
|
|
38
|
+
from defair_data.logging import get_logger
|
|
39
|
+
from defair_data.readers import (
|
|
40
|
+
ReaderInfo,
|
|
41
|
+
ReaderProduct,
|
|
42
|
+
describe_readers,
|
|
43
|
+
find_reader_for_path,
|
|
44
|
+
get_reader,
|
|
45
|
+
get_reader_info,
|
|
46
|
+
list_readers,
|
|
47
|
+
)
|
|
48
|
+
from defair_data.readers.msg15.msg15_native_reader_plugin import MSG15NativeReaderPlugin
|
|
49
|
+
|
|
50
|
+
__version__ = version("defair-data")
|
|
51
|
+
|
|
52
|
+
logger = get_logger(__name__)
|
|
53
|
+
|
|
54
|
+
# The netCDF4 wheel bundles its own libhdf5 build and h5py bundles a different one. In a
|
|
55
|
+
# single process the library imported first binds the shared symbols, so when netCDF4 wins
|
|
56
|
+
# every HDF5-backed reader here fails with "Unspecified error in H5DSget_num_scales" and the
|
|
57
|
+
# interpreter can dump core at shutdown -- a failure whose message names neither library.
|
|
58
|
+
# defair-data no longer depends on netCDF4, but earthkit-data still installs it, and
|
|
59
|
+
# "import netCDF4" is a common first line in an Earth-observation notebook. Saying so at the
|
|
60
|
+
# only moment the clash is detectable is the whole defence a library has here.
|
|
61
|
+
if sys.modules.get("netCDF4") is not None:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"netCDF4 was imported before defair_data. The two bundle different libhdf5 builds and the "
|
|
64
|
+
"first import wins the shared symbols, so HDF5-backed readers (MTG FCI, MTG LI, Sentinel-3 "
|
|
65
|
+
"and others) may fail with 'Unspecified error in H5DSget_num_scales' and the process may "
|
|
66
|
+
"crash at exit. Import defair_data before netCDF4, or keep netCDF4 out of this process."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _close_native_handles_before_teardown() -> None:
|
|
71
|
+
"""Release HDF5 and projection handles while their libraries are still alive.
|
|
72
|
+
|
|
73
|
+
xarray deliberately stops closing cached files at exit, so an h5py handle left in
|
|
74
|
+
a CachingFileManager is finalised during interpreter teardown, after libhdf5 (and,
|
|
75
|
+
in environments where pyproj and fiona each bundle a libproj, after one of those)
|
|
76
|
+
has already been torn down. A plain FCI read then exits with SIGSEGV after printing
|
|
77
|
+
a correct result. Registered here, after xarray and the HDF5 stack were imported,
|
|
78
|
+
this runs before their own exit hooks and leaves nothing for teardown to finalise.
|
|
79
|
+
"""
|
|
80
|
+
import gc # noqa: PLC0415
|
|
81
|
+
|
|
82
|
+
from xarray.backends.file_manager import FILE_CACHE # noqa: PLC0415
|
|
83
|
+
|
|
84
|
+
from defair_data.readers.mtg_fci.mtg_fci_l1c_nc_reader_plugin import release_retired_managers # noqa: PLC0415
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
release_retired_managers(force=True)
|
|
88
|
+
FILE_CACHE.clear()
|
|
89
|
+
except Exception: # best effort: an exit hook must never raise
|
|
90
|
+
logger.debug("exit cleanup of native handles failed", exc_info=True)
|
|
91
|
+
gc.collect()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
atexit.register(_close_native_handles_before_teardown)
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"CF_VERSION",
|
|
98
|
+
"DataReaderPlugin",
|
|
99
|
+
"Dataset",
|
|
100
|
+
"MSG15NativeReaderPlugin",
|
|
101
|
+
"ReaderInfo",
|
|
102
|
+
"ReaderProduct",
|
|
103
|
+
"S3CredentialsPlugin",
|
|
104
|
+
"__version__",
|
|
105
|
+
"describe_readers",
|
|
106
|
+
"find_reader_for_path",
|
|
107
|
+
"get_logger",
|
|
108
|
+
"get_reader",
|
|
109
|
+
"get_reader_info",
|
|
110
|
+
"list_readers",
|
|
111
|
+
"load_reader",
|
|
112
|
+
"propagate_s3_credentials",
|
|
113
|
+
]
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 EUMETSAT
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
"""Helpers for presenting one multi-grid dataset as one dataset per grid.
|
|
17
|
+
|
|
18
|
+
A reader that puts two sampling grids in a single :class:`xarray.Dataset` has to
|
|
19
|
+
disambiguate the second grid's spatial names, because ``x``/``y``/``lat``/``lon``
|
|
20
|
+
are already taken by the first: MSG SEVIRI emits ``x_hrv``/``y_hrv``/``lat_hrv``/
|
|
21
|
+
``lon_hrv`` for HRV, MTG FCI emits ``x_1km``/``x_2km``/``x_500m``. Every consumer
|
|
22
|
+
then has to know those product-specific spellings.
|
|
23
|
+
|
|
24
|
+
Once a grid stands alone there is nothing left to collide with, so the suffix has
|
|
25
|
+
no reason to survive the split. These helpers strip it and repair the CF metadata
|
|
26
|
+
that named the old spelling, which is what lets a split grid be an ordinary
|
|
27
|
+
single-grid dataset that needs no product-specific handling downstream.
|
|
28
|
+
|
|
29
|
+
The georeferencing needs the same treatment for the same reason: a multi-grid
|
|
30
|
+
dataset stores one affine describing one of its grids, so isolating a grid has to
|
|
31
|
+
re-aim it as well. That part is used by every single-grid selection, not only by
|
|
32
|
+
the split.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from dataclasses import replace
|
|
38
|
+
from os.path import commonprefix
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
import xarray as xr
|
|
42
|
+
|
|
43
|
+
from defair_data.cdm import CommonDataModel, GriddedModel, SpatialModel
|
|
44
|
+
from defair_data.logging import get_logger
|
|
45
|
+
|
|
46
|
+
logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
# CF attributes whose value is purely a whitespace-separated list of variable
|
|
49
|
+
# names, so rewriting them is a token substitution. `grid_mapping` is deliberately
|
|
50
|
+
# absent: CF-1.13 §5.6 also allows the expanded `crs: x y` form, whose colon-tagged
|
|
51
|
+
# mapping variable is not a plain name, and a split never renames the mapping
|
|
52
|
+
# variable anyway.
|
|
53
|
+
_CF_NAME_LIST_ATTRS = ("coordinates", "ancillary_variables", "bounds")
|
|
54
|
+
|
|
55
|
+
# CF attributes that tag each variable with the role it fills, so only every
|
|
56
|
+
# second word names a variable: `cell_measures` (CF-1.13 §7.2, `area: cell_area`)
|
|
57
|
+
# and `formula_terms` (§4.3.3, `a: a b: b ps: ps`).
|
|
58
|
+
_CF_PAIR_ATTRS = ("cell_measures", "formula_terms")
|
|
59
|
+
|
|
60
|
+
# The name a role takes once its grid stands alone. A suffix is only stripped when
|
|
61
|
+
# every renamed name lands on its role's canonical spelling, so a semantically
|
|
62
|
+
# meaningful suffix (``along_track``/``across_track`` share ``_track``) is never
|
|
63
|
+
# mistaken for a grid discriminator.
|
|
64
|
+
_CANONICAL_BY_ROLE = {
|
|
65
|
+
"x_dim": frozenset({"x"}),
|
|
66
|
+
"y_dim": frozenset({"y"}),
|
|
67
|
+
"x_coord": frozenset({"x"}),
|
|
68
|
+
"y_coord": frozenset({"y"}),
|
|
69
|
+
"longitude": frozenset({"lon", "longitude"}),
|
|
70
|
+
"latitude": frozenset({"lat", "latitude"}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def grid_mapping_references(value: str) -> list[str]:
|
|
75
|
+
"""Return every variable a CF ``grid_mapping`` attribute refers to.
|
|
76
|
+
|
|
77
|
+
CF-1.13 §5.6 allows either a bare mapping-variable name or expanded clauses in
|
|
78
|
+
which a word ending in a colon names the mapping variable and the words after
|
|
79
|
+
it name the coordinates it applies to. Both the mapping variables and those
|
|
80
|
+
coordinates are real references, so a reference walk has to see all of them --
|
|
81
|
+
reading the raw words would look for a variable called ``crs:``.
|
|
82
|
+
"""
|
|
83
|
+
if ":" not in value:
|
|
84
|
+
return value.split()
|
|
85
|
+
names: list[str] = []
|
|
86
|
+
for token in value.split():
|
|
87
|
+
head, sep, tail = token.partition(":")
|
|
88
|
+
names.append(head if sep else token)
|
|
89
|
+
if sep and tail:
|
|
90
|
+
names.append(tail)
|
|
91
|
+
return names
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def pair_tagged_references(value: str) -> list[str]:
|
|
95
|
+
"""Return the variable names in a CF ``term: variable`` attribute.
|
|
96
|
+
|
|
97
|
+
``cell_measures`` and ``formula_terms`` pair a role tag with the variable
|
|
98
|
+
filling it, so reading every word would look for a variable called ``ps:``
|
|
99
|
+
and treat the tags as references.
|
|
100
|
+
"""
|
|
101
|
+
return value.split()[1::2]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rename_grid_mapping(value: str, renames: dict[str, str]) -> str:
|
|
105
|
+
"""Rewrite a CF ``grid_mapping`` value, renaming only the coordinates in it.
|
|
106
|
+
|
|
107
|
+
The mapping variable keeps its name and its colon; only the coordinate names a
|
|
108
|
+
clause applies to are renamed. A bare mapping name has no coordinates in it and
|
|
109
|
+
is returned unchanged.
|
|
110
|
+
"""
|
|
111
|
+
if ":" not in value:
|
|
112
|
+
return value
|
|
113
|
+
out: list[str] = []
|
|
114
|
+
for token in value.split():
|
|
115
|
+
head, sep, tail = token.partition(":")
|
|
116
|
+
if not sep:
|
|
117
|
+
out.append(renames.get(token, token))
|
|
118
|
+
elif tail:
|
|
119
|
+
out.append(f"{head}:{renames.get(tail, tail)}")
|
|
120
|
+
else:
|
|
121
|
+
out.append(f"{head}:")
|
|
122
|
+
return " ".join(out)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _common_suffix(left: str, right: str) -> str:
|
|
126
|
+
"""Return the longest common suffix of two names."""
|
|
127
|
+
return commonprefix([left[::-1], right[::-1]])[::-1]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _spatial_names(group: GriddedModel) -> dict[str, str]:
|
|
131
|
+
"""Map a regular grid's spatial-role slots to the names it declares.
|
|
132
|
+
|
|
133
|
+
Only regular grids are handled. A curvilinear or point group's coordinates are
|
|
134
|
+
not interchangeable with a canonical ``x``/``y`` axis pair, so there is no
|
|
135
|
+
role-preserving rename to make for them.
|
|
136
|
+
"""
|
|
137
|
+
names = {
|
|
138
|
+
"x_dim": group.x_dim,
|
|
139
|
+
"y_dim": group.y_dim,
|
|
140
|
+
"x_coord": group.x_coord,
|
|
141
|
+
"y_coord": group.y_coord,
|
|
142
|
+
}
|
|
143
|
+
geo = group.geostationary
|
|
144
|
+
spec = group.latlon.spec
|
|
145
|
+
# A grid read projection-only names its lat/lon pair in the spec rather than in
|
|
146
|
+
# the GEO parameters, and that latent name has to be canonicalised too, or a
|
|
147
|
+
# materialisation after the split would reintroduce the suffix.
|
|
148
|
+
names["latitude"] = (geo.latitude if geo is not None else None) or (spec.lat_name if spec else "")
|
|
149
|
+
names["longitude"] = (geo.longitude if geo is not None else None) or (spec.lon_name if spec else "")
|
|
150
|
+
return {role: name for role, name in names.items() if name}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def canonical_renames(ds: xr.Dataset, group: SpatialModel) -> dict[str, str]:
|
|
154
|
+
"""Return the {old: new} renames that drop *group*'s disambiguating suffix.
|
|
155
|
+
|
|
156
|
+
The suffix comes from the two spatial dimension names, since those are what
|
|
157
|
+
actually collide when several grids share one dataset. It is only accepted once
|
|
158
|
+
every name it would rename lands on the canonical spelling for that name's role
|
|
159
|
+
(``x_hrv`` -> ``x``, ``lat_1km`` -> ``lat``). That check is what keeps a
|
|
160
|
+
meaningful suffix from being stripped: ``along_track``/``across_track`` share
|
|
161
|
+
``_track``, but ``along`` is not a canonical x-axis name, so nothing is renamed.
|
|
162
|
+
|
|
163
|
+
Returns an empty mapping when there is no suffix to strip, when a renamed name
|
|
164
|
+
would not be canonical, or when stripping would collide with a name already in
|
|
165
|
+
use — a partial rename must never silently shadow an existing variable.
|
|
166
|
+
"""
|
|
167
|
+
if not isinstance(group, GriddedModel):
|
|
168
|
+
return {}
|
|
169
|
+
names = _spatial_names(group)
|
|
170
|
+
x_dim, y_dim = names.get("x_dim", ""), names.get("y_dim", "")
|
|
171
|
+
if not (x_dim and y_dim):
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
suffix = _common_suffix(x_dim, y_dim)
|
|
175
|
+
if not suffix.startswith("_"):
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
renames: dict[str, str] = {}
|
|
179
|
+
for role, name in names.items():
|
|
180
|
+
if not name.endswith(suffix):
|
|
181
|
+
continue # a role spelled without the suffix keeps its own name
|
|
182
|
+
stripped = name[: -len(suffix)]
|
|
183
|
+
if stripped not in _CANONICAL_BY_ROLE.get(role, frozenset()):
|
|
184
|
+
logger.debug(
|
|
185
|
+
"Not stripping the '{}' suffix from group '{}': '{}' would become '{}', "
|
|
186
|
+
"which is not a canonical {} name, so the suffix is meaningful.",
|
|
187
|
+
suffix,
|
|
188
|
+
group.id,
|
|
189
|
+
name,
|
|
190
|
+
stripped,
|
|
191
|
+
role,
|
|
192
|
+
)
|
|
193
|
+
return {}
|
|
194
|
+
renames[name] = stripped
|
|
195
|
+
if not renames:
|
|
196
|
+
return {}
|
|
197
|
+
|
|
198
|
+
taken = (set(ds.variables) | set(ds.sizes)) - set(renames)
|
|
199
|
+
collisions = sorted(new for new in renames.values() if new in taken)
|
|
200
|
+
if collisions or len(set(renames.values())) != len(renames):
|
|
201
|
+
logger.warning(
|
|
202
|
+
"Not stripping the '{}' suffix from group '{}': target name(s) {} already in use.",
|
|
203
|
+
suffix,
|
|
204
|
+
group.id,
|
|
205
|
+
collisions or sorted(renames.values()),
|
|
206
|
+
)
|
|
207
|
+
return {}
|
|
208
|
+
return renames
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _cf_stores(var: xr.Variable) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
212
|
+
"""Both places a CF reference lives on *var*: its attrs and its encoding.
|
|
213
|
+
|
|
214
|
+
``xarray`` moves ``coordinates`` and ``grid_mapping`` out of ``attrs`` into
|
|
215
|
+
``encoding`` when it decodes a file, and writes them back out of ``encoding``
|
|
216
|
+
on save. A repair that touches ``attrs`` alone is therefore inert for any
|
|
217
|
+
dataset that has been through disk, and the stale names it left behind reach
|
|
218
|
+
the next file: the Zarr writer copies the encoded ``grid_mapping`` back into
|
|
219
|
+
attrs, and a plain ``to_netcdf`` writes the encoded ``coordinates`` verbatim.
|
|
220
|
+
"""
|
|
221
|
+
return (var.attrs, var.encoding)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cf_reference_value(var: xr.Variable, key: str) -> str:
|
|
225
|
+
"""The live value of one CF reference attribute on *var*, reading both stores.
|
|
226
|
+
|
|
227
|
+
A decoded dataset carries its references only in ``encoding``; an in-memory
|
|
228
|
+
one only in ``attrs``. When both stores carry *key* they can disagree (an
|
|
229
|
+
``encoding`` leftover from an earlier decode goes stale the moment attrs are
|
|
230
|
+
edited), so attrs wins: it is the live user-visible store, and a union of
|
|
231
|
+
the two would follow the stale name as if it were a real reference.
|
|
232
|
+
"""
|
|
233
|
+
for store in _cf_stores(var):
|
|
234
|
+
if key in store:
|
|
235
|
+
return str(store[key])
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def rename_cf_references(ds: xr.Dataset, renames: dict[str, str]) -> None:
|
|
240
|
+
"""Rewrite CF reference attributes in place to follow *renames*.
|
|
241
|
+
|
|
242
|
+
``xarray.Dataset.rename`` moves variables but leaves attributes that quote the
|
|
243
|
+
old names, so a renamed ``lat_hrv`` would still be advertised as ``lat_hrv`` by
|
|
244
|
+
every ``coordinates`` attribute pointing at it.
|
|
245
|
+
|
|
246
|
+
``grid_mapping`` is rewritten under its own grammar (CF-1.13 §5.6): a
|
|
247
|
+
``<variable>:`` tag introduces the coordinates it maps and is not itself a
|
|
248
|
+
coordinate, so it is preserved while the names after it are renamed.
|
|
249
|
+
"""
|
|
250
|
+
for var in ds.variables.values():
|
|
251
|
+
for store in _cf_stores(var):
|
|
252
|
+
for attr in _CF_NAME_LIST_ATTRS:
|
|
253
|
+
value = store.get(attr)
|
|
254
|
+
if isinstance(value, str) and value:
|
|
255
|
+
store[attr] = " ".join(renames.get(tok, tok) for tok in value.split())
|
|
256
|
+
mapping = store.get("grid_mapping")
|
|
257
|
+
if isinstance(mapping, str) and mapping:
|
|
258
|
+
store["grid_mapping"] = rename_grid_mapping(mapping, renames)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _prune_dangling(store: dict[str, Any], known: set[str]) -> None:
|
|
262
|
+
"""Drop the entries of one attrs/encoding mapping that name a missing variable.
|
|
263
|
+
|
|
264
|
+
``grid_mapping`` is deliberately not pruned: the mapping container is kept by
|
|
265
|
+
the reference walk that precedes this, so a dangling one would mean the walk
|
|
266
|
+
is broken, and silently deleting the link would hide that.
|
|
267
|
+
"""
|
|
268
|
+
for attr in (*_CF_NAME_LIST_ATTRS, *_CF_PAIR_ATTRS):
|
|
269
|
+
value = store.get(attr)
|
|
270
|
+
if not isinstance(value, str) or not value:
|
|
271
|
+
continue
|
|
272
|
+
tokens = value.split()
|
|
273
|
+
if attr in _CF_PAIR_ATTRS:
|
|
274
|
+
if len(tokens) % 2:
|
|
275
|
+
logger.warning("CF '{}' attribute has an unpaired trailing token: {!r}", attr, value)
|
|
276
|
+
pairs = list(zip(tokens[::2], tokens[1::2], strict=False))
|
|
277
|
+
kept_pairs = [(tag, name) for tag, name in pairs if name in known]
|
|
278
|
+
# A formula is computed from all of its terms at once, so a partial one
|
|
279
|
+
# is not a weaker formula but an uncomputable one: drop it whole. Cell
|
|
280
|
+
# measures are independent of each other, so surviving ones still stand.
|
|
281
|
+
if attr == "formula_terms" and len(kept_pairs) != len(pairs):
|
|
282
|
+
kept_pairs = []
|
|
283
|
+
kept = " ".join(f"{tag} {name}" for tag, name in kept_pairs)
|
|
284
|
+
else:
|
|
285
|
+
kept = " ".join(tok for tok in tokens if tok in known)
|
|
286
|
+
if kept == value:
|
|
287
|
+
continue
|
|
288
|
+
if kept:
|
|
289
|
+
store[attr] = kept
|
|
290
|
+
else:
|
|
291
|
+
del store[attr]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def drop_dangling_cf_references(ds: xr.Dataset) -> None:
|
|
295
|
+
"""Drop CF references that no longer resolve in *ds*, in place.
|
|
296
|
+
|
|
297
|
+
Isolating one grid leaves the shared grid-mapping container advertising the
|
|
298
|
+
other grid's ``coordinates``. Leaving that dangling reference in place breaks
|
|
299
|
+
metadata-reference closure for any consumer that resolves it: CF-1.13's §5
|
|
300
|
+
conformance requirement that every name in a ``coordinates`` attribute exist in
|
|
301
|
+
the file applies wherever the attribute sits, so this is conformance rather
|
|
302
|
+
than tidiness. The container is left without the attribute rather than
|
|
303
|
+
repointed at the surviving grid, because CF Appendix A admits ``coordinates``
|
|
304
|
+
on data, geometry-container and domain variables only; the association between
|
|
305
|
+
a mapping and its coordinates belongs on the data variable, which the split
|
|
306
|
+
repairs directly.
|
|
307
|
+
|
|
308
|
+
Name lists lose the entries that stopped resolving; a ``term: variable``
|
|
309
|
+
attribute loses whole pairs, since a tag without its variable is not a
|
|
310
|
+
reference either.
|
|
311
|
+
"""
|
|
312
|
+
known = {str(name) for name in ds.variables}
|
|
313
|
+
for var in ds.variables.values():
|
|
314
|
+
for store in _cf_stores(var):
|
|
315
|
+
_prune_dangling(store, known)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def drop_container_coordinates(ds: xr.Dataset) -> None:
|
|
319
|
+
"""Remove ``coordinates`` from every grid-mapping container in *ds*, in place.
|
|
320
|
+
|
|
321
|
+
CF Appendix A admits ``coordinates`` on data, geometry-container and domain
|
|
322
|
+
variables; a grid-mapping variable is none of those, and CF-1.13 §5.6 makes it
|
|
323
|
+
a container for the mapping's parameters, associating it with coordinates
|
|
324
|
+
through the expanded ``grid_mapping`` syntax on the data variable instead. Our
|
|
325
|
+
readers write one anyway, and one container serves every grid in a multi-grid
|
|
326
|
+
dataset, so it can only ever name one of them.
|
|
327
|
+
|
|
328
|
+
Isolating a grid would otherwise leave that attribute naming the dropped grid,
|
|
329
|
+
or surviving on whichever grid it happened to name and not on its sibling.
|
|
330
|
+
Removing it is what makes the split's containers consistent, and loses nothing:
|
|
331
|
+
the data variables' own references carry the association and are repaired here.
|
|
332
|
+
``grid_mapping_name`` is the one attribute CF requires on a container, so it is
|
|
333
|
+
how a container is recognised.
|
|
334
|
+
"""
|
|
335
|
+
for var in ds.variables.values():
|
|
336
|
+
if "grid_mapping_name" not in var.attrs:
|
|
337
|
+
continue
|
|
338
|
+
for store in _cf_stores(var):
|
|
339
|
+
store.pop("coordinates", None)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _gdal_geotransform(ds: xr.Dataset, group: SpatialModel) -> str | None:
|
|
343
|
+
"""Return *group*'s own affine as a GDAL ``GeoTransform`` string, if it has one.
|
|
344
|
+
|
|
345
|
+
GDAL orders the six terms ``(origin_x, x_res, 0, origin_y, 0, y_res)`` and
|
|
346
|
+
anchors the origin on the outer cell edge, half a pixel beyond the first
|
|
347
|
+
centre. Two centres are needed to measure a pixel, so a grid only one cell
|
|
348
|
+
wide or tall cannot state its own resolution and gets no affine.
|
|
349
|
+
"""
|
|
350
|
+
if not isinstance(group, GriddedModel):
|
|
351
|
+
return None
|
|
352
|
+
x_name, y_name = group.x_coord, group.y_coord
|
|
353
|
+
if x_name not in ds.variables or y_name not in ds.variables:
|
|
354
|
+
return None
|
|
355
|
+
x, y = ds[x_name], ds[y_name]
|
|
356
|
+
if x.ndim != 1 or y.ndim != 1 or x.size < 2 or y.size < 2:
|
|
357
|
+
return None
|
|
358
|
+
x_res, y_res = float(x[1] - x[0]), float(y[1] - y[0])
|
|
359
|
+
return f"{float(x[0]) - x_res / 2} {x_res} 0.0 {float(y[0]) - y_res / 2} 0.0 {y_res}"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def retarget_geotransform(ds: xr.Dataset, group: SpatialModel) -> None:
|
|
363
|
+
"""Re-aim a cached ``GeoTransform`` at *group*'s own axes, in place.
|
|
364
|
+
|
|
365
|
+
A multi-grid dataset carries at most one affine, written for whichever grid the
|
|
366
|
+
reader georeferenced first. Consumers that prefer a stored affine to
|
|
367
|
+
recomputing one, because the stored value is byte-exact across a round trip
|
|
368
|
+
through disk, then read that grid's pixel size out of every other grid: our own
|
|
369
|
+
``ReferenceGrid`` does exactly that, and so does any GDAL reader of a file we
|
|
370
|
+
write. Recomputing keeps such a consumer working, and lands on the identical
|
|
371
|
+
string for the grid the affine already described.
|
|
372
|
+
|
|
373
|
+
A grid too small to state its own resolution has the stale value removed rather
|
|
374
|
+
than replaced, since deriving the affine from its coordinates is then the
|
|
375
|
+
consumer's only honest option.
|
|
376
|
+
"""
|
|
377
|
+
stale = [name for name, var in ds.variables.items() if "GeoTransform" in var.attrs]
|
|
378
|
+
if not stale:
|
|
379
|
+
return
|
|
380
|
+
geotransform = _gdal_geotransform(ds, group)
|
|
381
|
+
for name in stale:
|
|
382
|
+
if geotransform is None:
|
|
383
|
+
del ds[name].attrs["GeoTransform"]
|
|
384
|
+
else:
|
|
385
|
+
ds[name].attrs["GeoTransform"] = geotransform
|
|
386
|
+
if geotransform is None:
|
|
387
|
+
logger.warning(
|
|
388
|
+
"Group '{}' is too small to state its own affine, so the cached GeoTransform on {} was "
|
|
389
|
+
"dropped rather than re-aimed; consumers must derive the transform from the coordinates.",
|
|
390
|
+
group.id,
|
|
391
|
+
sorted(str(name) for name in stale),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def rename_group(group: SpatialModel, renames: dict[str, str]) -> SpatialModel:
|
|
396
|
+
"""Return *group* with every declared name and dim following *renames*.
|
|
397
|
+
|
|
398
|
+
Only the dims are rewritten for a non-regular group: ``canonical_renames``
|
|
399
|
+
produces renames for regular grids alone, so a sibling point or curvilinear
|
|
400
|
+
group carried alongside one keeps its own coordinate names.
|
|
401
|
+
"""
|
|
402
|
+
dims = tuple(renames.get(d, d) for d in group.dims)
|
|
403
|
+
|
|
404
|
+
def _renamed(name: str | None) -> str | None:
|
|
405
|
+
return renames.get(name, name) if name else name
|
|
406
|
+
|
|
407
|
+
if isinstance(group, GriddedModel):
|
|
408
|
+
geostationary = group.geostationary
|
|
409
|
+
if geostationary is not None:
|
|
410
|
+
geostationary = replace(
|
|
411
|
+
geostationary,
|
|
412
|
+
latitude=_renamed(geostationary.latitude),
|
|
413
|
+
longitude=_renamed(geostationary.longitude),
|
|
414
|
+
)
|
|
415
|
+
# The lat/lon spec names the pair this grid would carry, whether it is
|
|
416
|
+
# attached yet or not, so it follows the rename for the same reason the
|
|
417
|
+
# attached names do: a materialisation after the split must land on the
|
|
418
|
+
# canonical spelling, not resurrect the suffix the split just removed.
|
|
419
|
+
latlon = group.latlon
|
|
420
|
+
if latlon.spec is not None:
|
|
421
|
+
latlon = replace(
|
|
422
|
+
latlon,
|
|
423
|
+
spec=replace(
|
|
424
|
+
latlon.spec,
|
|
425
|
+
lat_name=renames.get(latlon.spec.lat_name, latlon.spec.lat_name),
|
|
426
|
+
lon_name=renames.get(latlon.spec.lon_name, latlon.spec.lon_name),
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
return replace(
|
|
430
|
+
group,
|
|
431
|
+
dims=dims,
|
|
432
|
+
x_dim=renames.get(group.x_dim, group.x_dim),
|
|
433
|
+
y_dim=renames.get(group.y_dim, group.y_dim),
|
|
434
|
+
x_coord=renames.get(group.x_coord, group.x_coord),
|
|
435
|
+
y_coord=renames.get(group.y_coord, group.y_coord),
|
|
436
|
+
geostationary=geostationary,
|
|
437
|
+
latlon=latlon,
|
|
438
|
+
)
|
|
439
|
+
return replace(group, dims=dims)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def rename_cdm(cdm: CommonDataModel, renames: dict[str, str]) -> CommonDataModel:
|
|
443
|
+
"""Return *cdm* with every referenced name following *renames*."""
|
|
444
|
+
temporal_groups = tuple(
|
|
445
|
+
replace(
|
|
446
|
+
group,
|
|
447
|
+
variables=tuple(renames.get(variable, variable) for variable in group.variables),
|
|
448
|
+
time_coord=renames.get(group.time_coord, group.time_coord),
|
|
449
|
+
sample_dim=renames.get(group.sample_dim, group.sample_dim) if group.sample_dim is not None else None,
|
|
450
|
+
)
|
|
451
|
+
for group in cdm.temporal_groups
|
|
452
|
+
)
|
|
453
|
+
return replace(
|
|
454
|
+
cdm,
|
|
455
|
+
spatial_groups=tuple(rename_group(group, renames) for group in cdm.spatial_groups),
|
|
456
|
+
temporal_groups=temporal_groups,
|
|
457
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 EUMETSAT
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
"""Import-time defaults for noisy third-party loggers.
|
|
18
|
+
|
|
19
|
+
Called from ``defair_data/__init__.py`` so the suppression is in place for any
|
|
20
|
+
consumer that imports the package — CLI, notebooks, ad-hoc scripts, and
|
|
21
|
+
externally-rooted pytest suites that never reach our ``setup_logging()`` fixture.
|
|
22
|
+
|
|
23
|
+
Politeness: a level is only installed when the logger is still at ``NOTSET`` —
|
|
24
|
+
so a caller who explicitly opted in (e.g. ``logging.getLogger("botocore").setLevel(INFO)``)
|
|
25
|
+
before importing DEFAIR is respected. ``setup_logging()`` keeps stricter
|
|
26
|
+
"always set" semantics for the explicit configuration path.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
|
|
34
|
+
# Third-party loggers whose INFO chatter pollutes DEFAIR output (and pytest
|
|
35
|
+
# captured-log sections under ``log_level = "INFO"``). Re-exported as the
|
|
36
|
+
# single source of truth for ``defair.logging.setup_logging``.
|
|
37
|
+
NOISY_LOGGERS: tuple[str, ...] = ("botocore", "boto3", "urllib3", "s3transfer", "aiobotocore")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _noisy_floor_from_env() -> int:
|
|
41
|
+
"""Resolve the noisy-logger floor from ``DEFAIR_LOG_LEVEL`` (default WARNING)."""
|
|
42
|
+
if os.environ.get("DEFAIR_LOG_LEVEL", "INFO").upper() in ("DEBUG", "TRACE"):
|
|
43
|
+
return logging.DEBUG
|
|
44
|
+
return logging.WARNING
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def configure_noisy_dependency_loggers() -> None:
|
|
48
|
+
"""Install a sensible default level on noisy third-party loggers.
|
|
49
|
+
|
|
50
|
+
Only adjusts a logger that is still at ``NOTSET``; a deliberately-set level
|
|
51
|
+
is left untouched. Safe to call multiple times and from any import path.
|
|
52
|
+
"""
|
|
53
|
+
floor = _noisy_floor_from_env()
|
|
54
|
+
for name in NOISY_LOGGERS:
|
|
55
|
+
logger_obj = logging.getLogger(name)
|
|
56
|
+
if logger_obj.level == logging.NOTSET:
|
|
57
|
+
logger_obj.setLevel(floor)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Apply at module import so ``import defair_data`` is sufficient — even for
|
|
61
|
+
# consumers that never call ``defair.logging.setup_logging`` (vendor pytest
|
|
62
|
+
# suite, notebooks, third-party scripts).
|
|
63
|
+
configure_noisy_dependency_loggers()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 EUMETSAT
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
"""Path utilities for glob pattern detection and manipulation."""
|
|
18
|
+
|
|
19
|
+
from os import PathLike
|
|
20
|
+
|
|
21
|
+
_GLOB_CHARS = {"*", "?", "[", "]"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_glob_pattern(path: str | PathLike) -> bool:
|
|
25
|
+
"""Check if path contains glob wildcards.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
path: Path string to check
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if path contains glob wildcard characters (*, ?, [, ])
|
|
32
|
+
"""
|
|
33
|
+
return any(c in str(path) for c in _GLOB_CHARS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def join_uri(base: str | PathLike, *parts: str) -> str:
|
|
37
|
+
"""Join a base URI or filesystem path with one or more child segments."""
|
|
38
|
+
out = str(base).rstrip("/")
|
|
39
|
+
|
|
40
|
+
for part in parts:
|
|
41
|
+
if part is None:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
s = str(part).strip("/")
|
|
45
|
+
|
|
46
|
+
# Skip empty or "." literals
|
|
47
|
+
if not s or s == ".":
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if ".." in s.split("/"):
|
|
51
|
+
raise ValueError(f"join_uri rejects '..' segments: {part!r}")
|
|
52
|
+
|
|
53
|
+
out = f"{out}/{s}"
|
|
54
|
+
return out
|