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.
Files changed (259) hide show
  1. defair_data/__init__.py +113 -0
  2. defair_data/_grid_split.py +457 -0
  3. defair_data/_logging_defaults.py +63 -0
  4. defair_data/_path_utils.py +54 -0
  5. defair_data/_types.py +75 -0
  6. defair_data/cdm.py +1404 -0
  7. defair_data/cf_flags.py +459 -0
  8. defair_data/constants.py +19 -0
  9. defair_data/core.py +3025 -0
  10. defair_data/dask_manager.py +1343 -0
  11. defair_data/data_reader_plugin.py +545 -0
  12. defair_data/data_source_plugin.py +334 -0
  13. defair_data/data_writer_plugin.py +186 -0
  14. defair_data/logging.py +34 -0
  15. defair_data/py.typed +0 -0
  16. defair_data/readers/__init__.py +102 -0
  17. defair_data/readers/_metop_products.py +310 -0
  18. defair_data/readers/_mtg_chunk_naming.py +41 -0
  19. defair_data/readers/_reader_metadata.py +420 -0
  20. defair_data/readers/_time_utils.py +93 -0
  21. defair_data/readers/era5_grib/__init__.py +21 -0
  22. defair_data/readers/era5_grib/era5_grib_reader_plugin.py +901 -0
  23. defair_data/readers/metop/__init__.py +163 -0
  24. defair_data/readers/metop/_eps_native_base_reader.py +911 -0
  25. defair_data/readers/metop/_eps_parser.py +1636 -0
  26. defair_data/readers/metop/_gome2_parser.py +1259 -0
  27. defair_data/readers/metop/_iasi_snd_parser.py +1591 -0
  28. defair_data/readers/metop/_iasi_utils.py +65 -0
  29. defair_data/readers/metop/_metop_base_reader.py +904 -0
  30. defair_data/readers/metop/_metop_coordinate_utils.py +251 -0
  31. defair_data/readers/metop/_metop_metadata.py +2555 -0
  32. defair_data/readers/metop/_metop_netcdf_gridded_base.py +298 -0
  33. defair_data/readers/metop/_metop_netcdf_swath_base.py +376 -0
  34. defair_data/readers/metop/_netcdf_engine.py +116 -0
  35. defair_data/readers/metop/metop_amsul1_reader_plugin.py +268 -0
  36. defair_data/readers/metop/metop_ascszf1b_reader_plugin.py +567 -0
  37. defair_data/readers/metop/metop_ascszfr02_reader_plugin.py +70 -0
  38. defair_data/readers/metop/metop_ascszo1b_reader_plugin.py +544 -0
  39. defair_data/readers/metop/metop_ascszor02_reader_plugin.py +270 -0
  40. defair_data/readers/metop/metop_ascszr1b_reader_plugin.py +660 -0
  41. defair_data/readers/metop/metop_ascszrr02_reader_plugin.py +410 -0
  42. defair_data/readers/metop/metop_avhrr_amv_reader_plugin.py +392 -0
  43. defair_data/readers/metop/metop_avhrrl1_reader_plugin.py +1116 -0
  44. defair_data/readers/metop/metop_edlst_reader_plugin.py +747 -0
  45. defair_data/readers/metop/metop_glbsst_reader_plugin.py +100 -0
  46. defair_data/readers/metop/metop_gomel1_reader_plugin.py +1402 -0
  47. defair_data/readers/metop/metop_gomel1r03_reader_plugin.py +84 -0
  48. defair_data/readers/metop/metop_hirs_fdr_reader_plugin.py +494 -0
  49. defair_data/readers/metop/metop_hirsl1_reader_plugin.py +280 -0
  50. defair_data/readers/metop/metop_iasil1c_all_reader_plugin.py +537 -0
  51. defair_data/readers/metop/metop_iasisnd02_reader_plugin.py +1109 -0
  52. defair_data/readers/metop/metop_iasthr011_reader_plugin.py +714 -0
  53. defair_data/readers/metop/metop_mhsl1_reader_plugin.py +163 -0
  54. defair_data/readers/metop/metop_osi104_reader_plugin.py +239 -0
  55. defair_data/readers/metop/metop_osi150a_reader_plugin.py +90 -0
  56. defair_data/readers/metop/metop_osi150b_reader_plugin.py +95 -0
  57. defair_data/readers/metop/metop_somo12_reader_plugin.py +353 -0
  58. defair_data/readers/metop/metop_somo25_reader_plugin.py +347 -0
  59. defair_data/readers/metop/resources/eps_native_format/AMSAL1/band_to_record.yaml +279 -0
  60. defair_data/readers/metop/resources/eps_native_format/AMSAL1/general_metadata.yaml +11 -0
  61. defair_data/readers/metop/resources/eps_native_format/AMSAL1/giadr_2_3.csv +86 -0
  62. defair_data/readers/metop/resources/eps_native_format/AMSAL1/mdr_2_3.csv +131 -0
  63. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/band_to_record.yaml +102 -0
  64. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/general_metadata.yaml +11 -0
  65. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/mdr_3_3.csv +21 -0
  66. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/mdr_3_4.csv +25 -0
  67. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/mdr_3_5.csv +25 -0
  68. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/viadr_4_2.csv +9 -0
  69. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZF/viadr_6_2.csv +15 -0
  70. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/band_to_record.yaml +561 -0
  71. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/general_metadata.yaml +11 -0
  72. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/mdr_2_2.csv +27 -0
  73. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/mdr_2_3.csv +30 -0
  74. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/mdr_2_4.csv +26 -0
  75. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/viadr_4_2.csv +9 -0
  76. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZO/viadr_6_2.csv +15 -0
  77. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/band_to_record.yaml +628 -0
  78. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/general_metadata.yaml +11 -0
  79. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/mdr_1_2.csv +27 -0
  80. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/mdr_1_3.csv +30 -0
  81. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/mdr_1_4.csv +26 -0
  82. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/viadr_4_2.csv +9 -0
  83. defair_data/readers/metop/resources/eps_native_format/ASCATL1SZR/viadr_6_2.csv +15 -0
  84. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/band_to_record.yaml +282 -0
  85. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/general_metadata.yaml +11 -0
  86. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/mdr_5_0.csv +40 -0
  87. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/mdr_5_1.csv +51 -0
  88. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/mdr_5_2.csv +51 -0
  89. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMO/viadr_7_2.csv +30 -0
  90. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/band_to_record.yaml +271 -0
  91. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/general_metadata.yaml +11 -0
  92. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/mdr_4_0.csv +40 -0
  93. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/mdr_4_1.csv +51 -0
  94. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/mdr_4_2.csv +55 -0
  95. defair_data/readers/metop/resources/eps_native_format/ASCATL2SMR/viadr_7_2.csv +30 -0
  96. defair_data/readers/metop/resources/eps_native_format/AVHRRL1/band_to_record.yaml +198 -0
  97. defair_data/readers/metop/resources/eps_native_format/AVHRRL1/general_metadata.yaml +11 -0
  98. defair_data/readers/metop/resources/eps_native_format/AVHRRL1/giadr_1_3.csv +50 -0
  99. defair_data/readers/metop/resources/eps_native_format/AVHRRL1/giadr_2_2.csv +114 -0
  100. defair_data/readers/metop/resources/eps_native_format/AVHRRL1/mdr_2_4.csv +85 -0
  101. defair_data/readers/metop/resources/eps_native_format/AVHRRL1/mdr_2_5.csv +85 -0
  102. defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_4.csv +10 -0
  103. defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_5.csv +10 -0
  104. defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_6.csv +5 -0
  105. defair_data/readers/metop/resources/eps_native_format/GOME2L1/giadr_7.csv +7 -0
  106. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_6_5.csv +102 -0
  107. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_6_6.csv +105 -0
  108. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_7_fmv12.csv +38 -0
  109. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_7_fmv13.csv +39 -0
  110. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_8_fmv12.csv +40 -0
  111. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_8_fmv13.csv +41 -0
  112. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_9_fmv12.csv +44 -0
  113. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_9_fmv13.csv +45 -0
  114. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_band_m.csv +4 -0
  115. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_band_p.csv +5 -0
  116. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_geo_earth_actual.csv +10 -0
  117. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_polss.csv +6 -0
  118. defair_data/readers/metop/resources/eps_native_format/GOME2L1/mdr_polv.csv +4 -0
  119. defair_data/readers/metop/resources/eps_native_format/GOME2L1/sphr_1.csv +99 -0
  120. defair_data/readers/metop/resources/eps_native_format/GOME2L1/viadr_5.csv +16 -0
  121. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/band_to_record.yaml +328 -0
  122. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/general_metadata.yaml +11 -0
  123. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/giadr_1_1.csv +21 -0
  124. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/giadr_2_1.csv +17 -0
  125. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/giadr_3_1.csv +5 -0
  126. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/mdr_1_2.csv +59 -0
  127. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_AOP/viadr_1_1.csv +11 -0
  128. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/band_to_record.yaml +229 -0
  129. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/general_metadata.yaml +11 -0
  130. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/giadr_1_1.csv +21 -0
  131. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/giadr_2_1.csv +17 -0
  132. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/giadr_3_1.csv +5 -0
  133. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/mdr_1_2.csv +59 -0
  134. defair_data/readers/metop/resources/eps_native_format/GOME_PMA_COP/viadr_1_1.csv +11 -0
  135. defair_data/readers/metop/resources/eps_native_format/HIRSL1/band_to_record.yaml +326 -0
  136. defair_data/readers/metop/resources/eps_native_format/HIRSL1/general_metadata.yaml +11 -0
  137. defair_data/readers/metop/resources/eps_native_format/HIRSL1/giadr_1_2.csv +13 -0
  138. defair_data/readers/metop/resources/eps_native_format/HIRSL1/giadr_2_2.csv +21 -0
  139. defair_data/readers/metop/resources/eps_native_format/HIRSL1/mdr_2_2.csv +39 -0
  140. defair_data/readers/metop/resources/eps_native_format/IASIL1/band_to_record.yaml +217 -0
  141. defair_data/readers/metop/resources/eps_native_format/IASIL1/general_metadata.yaml +11 -0
  142. defair_data/readers/metop/resources/eps_native_format/IASIL1/giadr_0_2.csv +16 -0
  143. defair_data/readers/metop/resources/eps_native_format/IASIL1/mdr_2_5.csv +65 -0
  144. defair_data/readers/metop/resources/eps_native_format/IASISND02/band_to_record.yaml +790 -0
  145. defair_data/readers/metop/resources/eps_native_format/IASISND02/general_metadata.yaml +11 -0
  146. defair_data/readers/metop/resources/eps_native_format/IASISND02/giadr_1_3.csv +12 -0
  147. defair_data/readers/metop/resources/eps_native_format/IASISND02/giadr_1_4.csv +26 -0
  148. defair_data/readers/metop/resources/eps_native_format/IASISND02/mdr_1_3.csv +76 -0
  149. defair_data/readers/metop/resources/eps_native_format/IASISND02/mdr_1_4.csv +94 -0
  150. defair_data/readers/metop/resources/eps_native_format/MHSL1/band_to_record.yaml +169 -0
  151. defair_data/readers/metop/resources/eps_native_format/MHSL1/general_metadata.yaml +11 -0
  152. defair_data/readers/metop/resources/eps_native_format/MHSL1/giadr_1_3.csv +14 -0
  153. defair_data/readers/metop/resources/eps_native_format/MHSL1/giadr_2_3.csv +81 -0
  154. defair_data/readers/metop/resources/eps_native_format/MHSL1/giadr_3_1.csv +35 -0
  155. defair_data/readers/metop/resources/eps_native_format/MHSL1/mdr_2_3.csv +101 -0
  156. defair_data/readers/metop/resources/eps_native_format/mphr_0_2.csv +75 -0
  157. defair_data/readers/msg15/__init__.py +21 -0
  158. defair_data/readers/msg15/_coordinate_cache.py +276 -0
  159. defair_data/readers/msg15/_dt_msg.py +239 -0
  160. defair_data/readers/msg15/_dt_msg_15_native_reader.py +849 -0
  161. defair_data/readers/msg15/_msg15_xarray_backend.py +911 -0
  162. defair_data/readers/msg15/_msg_aux_metadata.py +184 -0
  163. defair_data/readers/msg15/_seviri_bt_coeffs.py +111 -0
  164. defair_data/readers/msg15/_seviri_refl_coeffs.py +89 -0
  165. defair_data/readers/msg15/msg15_native_reader_plugin.py +2364 -0
  166. defair_data/readers/msg15/resources/msg_native_format/records/15_header.csv +9 -0
  167. defair_data/readers/msg15/resources/msg_native_format/records/15_trailer.csv +7 -0
  168. defair_data/readers/msg15/resources/msg_native_format/sections/15_header.csv +14681 -0
  169. defair_data/readers/msg15/resources/msg_native_format/sections/15_trailer.csv +3930 -0
  170. defair_data/readers/mtg_fci/__init__.py +35 -0
  171. defair_data/readers/mtg_fci/_fci_aux_metadata.py +189 -0
  172. defair_data/readers/mtg_fci/_fci_bt_coeffs.py +96 -0
  173. defair_data/readers/mtg_fci/_fci_coordinate_cache.py +251 -0
  174. defair_data/readers/mtg_fci/_fci_metadata.py +114 -0
  175. defair_data/readers/mtg_fci/_fci_refl_coeffs.py +74 -0
  176. defair_data/readers/mtg_fci/_mtg_l2_flag_metadata.py +289 -0
  177. defair_data/readers/mtg_fci/_mtg_projection_gridded_base.py +844 -0
  178. defair_data/readers/mtg_fci/_mtg_segment_product_base.py +685 -0
  179. defair_data/readers/mtg_fci/mtg_fci_l1c_nc_reader_plugin.py +3590 -0
  180. defair_data/readers/mtg_fci/mtg_l2_amv_reader_plugin.py +429 -0
  181. defair_data/readers/mtg_fci/mtg_l2_asr_reader_plugin.py +735 -0
  182. defair_data/readers/mtg_fci/mtg_l2_clm_reader_plugin.py +104 -0
  183. defair_data/readers/mtg_fci/mtg_l2_gii_reader_plugin.py +127 -0
  184. defair_data/readers/mtg_fci/mtg_l2_oca_reader_plugin.py +154 -0
  185. defair_data/readers/mtg_fci/mtg_l2_olr_reader_plugin.py +89 -0
  186. defair_data/readers/mtg_li/__init__.py +58 -0
  187. defair_data/readers/mtg_li/_li_base_reader.py +1187 -0
  188. defair_data/readers/mtg_li/_li_coordinate_cache.py +252 -0
  189. defair_data/readers/mtg_li/_li_metadata.py +124 -0
  190. defair_data/readers/mtg_li/_li_point_base_reader.py +1281 -0
  191. defair_data/readers/mtg_li/_li_point_metadata.py +123 -0
  192. defair_data/readers/mtg_li/_li_sparse_reconstruction.py +579 -0
  193. defair_data/readers/mtg_li/mtg_li_af_reader_plugin.py +72 -0
  194. defair_data/readers/mtg_li/mtg_li_afa_reader_plugin.py +72 -0
  195. defair_data/readers/mtg_li/mtg_li_afr_reader_plugin.py +72 -0
  196. defair_data/readers/mtg_li/mtg_li_lef_reader_plugin.py +58 -0
  197. defair_data/readers/mtg_li/mtg_li_lfl_reader_plugin.py +63 -0
  198. defair_data/readers/mtg_li/mtg_li_lgr_reader_plugin.py +61 -0
  199. defair_data/readers/planck.py +193 -0
  200. defair_data/readers/reflectance.py +120 -0
  201. defair_data/readers/sentinel3/__init__.py +92 -0
  202. defair_data/readers/sentinel3/_olci_base.py +435 -0
  203. defair_data/readers/sentinel3/_olci_l1_base.py +292 -0
  204. defair_data/readers/sentinel3/_olci_l2_water_base.py +342 -0
  205. defair_data/readers/sentinel3/_safe_netcdf_merge.py +104 -0
  206. defair_data/readers/sentinel3/_safe_package.py +587 -0
  207. defair_data/readers/sentinel3/_sentinel3_base.py +883 -0
  208. defair_data/readers/sentinel3/_slstr_base.py +111 -0
  209. defair_data/readers/sentinel3/_sral_base.py +251 -0
  210. defair_data/readers/sentinel3/sentinel3_aod_reader_plugin.py +341 -0
  211. defair_data/readers/sentinel3/sentinel3_frp_reader_plugin.py +607 -0
  212. defair_data/readers/sentinel3/sentinel3_ol_1_efr_reader_plugin.py +55 -0
  213. defair_data/readers/sentinel3/sentinel3_ol_1_err_reader_plugin.py +51 -0
  214. defair_data/readers/sentinel3/sentinel3_ol_2_wfr_reader_plugin.py +69 -0
  215. defair_data/readers/sentinel3/sentinel3_ol_2_wrr_reader_plugin.py +42 -0
  216. defair_data/readers/sentinel3/sentinel3_sl_1_rbt_reader_plugin.py +625 -0
  217. defair_data/readers/sentinel3/sentinel3_sr1_sra_a_reader_plugin.py +397 -0
  218. defair_data/readers/sentinel3/sentinel3_sr1_sra_bs_reader_plugin.py +465 -0
  219. defair_data/readers/sentinel3/sentinel3_sr1_sra_reader_plugin.py +504 -0
  220. defair_data/readers/sentinel3/sentinel3_sr2_wat_reader_plugin.py +462 -0
  221. defair_data/readers/sentinel3/sentinel3_wst_reader_plugin.py +310 -0
  222. defair_data/sources/__init__.py +95 -0
  223. defair_data/sources/_zip_resolver.py +374 -0
  224. defair_data/sources/hda/__init__.py +21 -0
  225. defair_data/sources/hda/hda_stac_plugin.py +775 -0
  226. defair_data/sources/local/__init__.py +21 -0
  227. defair_data/sources/local/local_filesystem_plugin.py +573 -0
  228. defair_data/sources/s3/__init__.py +22 -0
  229. defair_data/sources/s3/_s3_kwargs.py +87 -0
  230. defair_data/sources/s3/s3_base.py +409 -0
  231. defair_data/sources/s3/s3_boto3_plugin.py +718 -0
  232. defair_data/sources/s3/s3_fsspec_plugin.py +488 -0
  233. defair_data/writers/__init__.py +82 -0
  234. defair_data/writers/_common/__init__.py +16 -0
  235. defair_data/writers/_common/column_writer.py +554 -0
  236. defair_data/writers/_common/columnar.py +709 -0
  237. defair_data/writers/_common/columnar_geometry.py +324 -0
  238. defair_data/writers/_common/columnar_metadata.py +493 -0
  239. defair_data/writers/_common/image_utils.py +133 -0
  240. defair_data/writers/_common/image_writer.py +353 -0
  241. defair_data/writers/_common/path_layout.py +145 -0
  242. defair_data/writers/_common/raster_geometry.py +253 -0
  243. defair_data/writers/_common/variable_filter.py +80 -0
  244. defair_data/writers/csv/__init__.py +20 -0
  245. defair_data/writers/csv/csv_writer.py +321 -0
  246. defair_data/writers/geotiff/geotiff_writer.py +62 -0
  247. defair_data/writers/hdf5/hdf5_writer.py +179 -0
  248. defair_data/writers/jpeg/jpeg_writer.py +56 -0
  249. defair_data/writers/netcdf4/netcdf4_writer.py +486 -0
  250. defair_data/writers/parquet/__init__.py +20 -0
  251. defair_data/writers/parquet/parquet_writer.py +321 -0
  252. defair_data/writers/png/png_writer.py +43 -0
  253. defair_data/writers/zarr/__init__.py +20 -0
  254. defair_data/writers/zarr/zarr_writer.py +1866 -0
  255. defair_data-0.4.0rc2.dist-info/METADATA +63 -0
  256. defair_data-0.4.0rc2.dist-info/RECORD +259 -0
  257. defair_data-0.4.0rc2.dist-info/WHEEL +4 -0
  258. defair_data-0.4.0rc2.dist-info/entry_points.txt +74 -0
  259. defair_data-0.4.0rc2.dist-info/licenses/LICENSE +175 -0
@@ -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