easysnowdata 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/CHANGELOG.md +18 -0
  2. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/CITATION.cff +2 -2
  3. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/PKG-INFO +1 -1
  4. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/REVAMP_PLAN.md +6 -4
  5. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/_version.py +2 -2
  6. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/archive.py +133 -42
  7. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/pyproject.toml +1 -1
  8. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_archive.py +89 -8
  9. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/.editorconfig +0 -0
  10. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/.gitignore +0 -0
  11. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/.pre-commit-config.yaml +0 -0
  12. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/LICENSE +0 -0
  13. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/POTENTIAL_DATA_PRODUCTS_SOURCES_AND_EXAMPLES.md +0 -0
  14. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/README.md +0 -0
  15. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/WATCHLIST.toml +0 -0
  16. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/__init__.py +0 -0
  17. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/_gdal.py +0 -0
  18. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/aoi.py +0 -0
  19. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/__init__.py +0 -0
  20. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/_base.py +0 -0
  21. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/earthdata.py +0 -0
  22. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/earthengine.py +0 -0
  23. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/nve.py +0 -0
  24. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/planet.py +0 -0
  25. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/auth/planetary_computer.py +0 -0
  26. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/catalog/__init__.py +0 -0
  27. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/catalog/_access.py +0 -0
  28. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/catalog/_models.py +0 -0
  29. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/catalog/_registry.py +0 -0
  30. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/catalog/health.py +0 -0
  31. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/catalog/pages.py +0 -0
  32. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/climate/__init__.py +0 -0
  33. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/climate/era5.py +0 -0
  34. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/climate/koppen_geiger.py +0 -0
  35. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/config.py +0 -0
  36. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/hydro/__init__.py +0 -0
  37. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/hydro/basins.py +0 -0
  38. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/land/__init__.py +0 -0
  39. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/land/forest_cover.py +0 -0
  40. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/land/landcover.py +0 -0
  41. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/land/nlcd.py +0 -0
  42. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/optical/__init__.py +0 -0
  43. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/optical/hls.py +0 -0
  44. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/optical/planetscope.py +0 -0
  45. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/optical/sentinel2.py +0 -0
  46. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/plotting/__init__.py +0 -0
  47. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/__init__.py +0 -0
  48. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/categorical.py +0 -0
  49. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/contract.py +0 -0
  50. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/masks.py +0 -0
  51. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/optical.py +0 -0
  52. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/sar.py +0 -0
  53. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/snow.py +0 -0
  54. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/processing/wateryear.py +0 -0
  55. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/__init__.py +0 -0
  56. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/earthdata.py +0 -0
  57. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/gee.py +0 -0
  58. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/planet.py +0 -0
  59. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/raster_http.py +0 -0
  60. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/stac.py +0 -0
  61. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/vector_http.py +0 -0
  62. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/providers/zarr_cloud.py +0 -0
  63. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/sar/__init__.py +0 -0
  64. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/sar/sentinel1.py +0 -0
  65. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/__init__.py +0 -0
  66. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/modis.py +0 -0
  67. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/mountain_snow_mask.py +0 -0
  68. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/snodas.py +0 -0
  69. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/snow_classification.py +0 -0
  70. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/ucla_sr.py +0 -0
  71. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/snow/viirs.py +0 -0
  72. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/__init__.py +0 -0
  73. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/_adapter.py +0 -0
  74. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/_catalog.py +0 -0
  75. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/_frames.py +0 -0
  76. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/README.md +0 -0
  77. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/__init__.py +0 -0
  78. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/_common.py +0 -0
  79. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/awdb/__init__.py +0 -0
  80. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/awdb/awdb_client.py +0 -0
  81. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/cdec/__init__.py +0 -0
  82. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/cdec/cdec_client.py +0 -0
  83. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/databc/__init__.py +0 -0
  84. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/databc/databc_client.py +0 -0
  85. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/nve/__init__.py +0 -0
  86. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/nve/nve_client.py +0 -0
  87. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/yukon/__init__.py +0 -0
  88. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/clients/yukon/yukon_client.py +0 -0
  89. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/stations/networks.py +0 -0
  90. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/temporal.py +0 -0
  91. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/terrain/__init__.py +0 -0
  92. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/terrain/chili.py +0 -0
  93. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/easysnowdata/terrain/dem.py +0 -0
  94. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/scripts/check_data_sources.py +0 -0
  95. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/scripts/feedstock_requirements.py +0 -0
  96. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/scripts/make_montage.py +0 -0
  97. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/scripts/report_health.py +0 -0
  98. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/scripts/update_readme_status.py +0 -0
  99. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/scripts/watch.py +0 -0
  100. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/__init__.py +0 -0
  101. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/cassettes/test_recorded/test_cmr_lpcloud_hls_search_is_open.yaml +0 -0
  102. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/cassettes/test_recorded/test_earth_search_sentinel2_search_with_time.yaml +0 -0
  103. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/cassettes/test_recorded/test_health_first_byte_probe_replays.yaml +0 -0
  104. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/cassettes/test_recorded/test_health_stac_probe_replays.yaml +0 -0
  105. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/cassettes/test_recorded/test_planetary_computer_dem_search.yaml +0 -0
  106. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/conftest.py +0 -0
  107. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/fixtures/make_fixtures.py +0 -0
  108. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/__init__.py +0 -0
  109. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/cassettes/test_land_landcover/TestSearchRecorded.test_search_filters_by_version.yaml +0 -0
  110. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/cassettes/test_terrain_dem/TestSearchRecorded.test_search_on_earth_search.yaml +0 -0
  111. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/cassettes/test_terrain_dem/TestSearchRecorded.test_search_returns_tiles.yaml +0 -0
  112. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/conftest.py +0 -0
  113. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/make_fixtures.py +0 -0
  114. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_hydro_basins.py +0 -0
  115. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_land_forest_cover.py +0 -0
  116. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_land_landcover.py +0 -0
  117. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_land_nlcd.py +0 -0
  118. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_snow_classification.py +0 -0
  119. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_snow_mountain_snow_mask.py +0 -0
  120. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_terrain_chili.py +0 -0
  121. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_terrain_dem.py +0 -0
  122. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/static/test_theme_common.py +0 -0
  123. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/__init__.py +0 -0
  124. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/conftest.py +0 -0
  125. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/fixtures/make_fixtures.py +0 -0
  126. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_adapter.py +0 -0
  127. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_adapter_offline.py +0 -0
  128. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_awdb_client.py +0 -0
  129. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_cdec_client.py +0 -0
  130. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_clients_offline.py +0 -0
  131. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_databc_client.py +0 -0
  132. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_nve_client.py +0 -0
  133. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/stations/test_yukon_client.py +0 -0
  134. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_aoi.py +0 -0
  135. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_auth.py +0 -0
  136. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_catalog.py +0 -0
  137. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_config.py +0 -0
  138. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_credentials.py +0 -0
  139. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_docs_pages.py +0 -0
  140. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_easysnowdata.py +0 -0
  141. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_edges.py +0 -0
  142. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_feedstock_requirements.py +0 -0
  143. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_health_reporting.py +0 -0
  144. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_montage.py +0 -0
  145. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_plotting.py +0 -0
  146. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_processing.py +0 -0
  147. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_providers.py +0 -0
  148. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_recorded.py +0 -0
  149. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_temporal.py +0 -0
  150. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/test_watch.py +0 -0
  151. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/__init__.py +0 -0
  152. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/cassettes/test_hls/test_search_cmr_lpcloud_is_open_and_labels_products.yaml +0 -0
  153. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/cassettes/test_hls/test_search_planetary_computer_mirror.yaml +0 -0
  154. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/cassettes/test_sentinel1/test_search_opera_rtc_is_open.yaml +0 -0
  155. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/cassettes/test_sentinel1/test_search_planetary_computer.yaml +0 -0
  156. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/cassettes/test_sentinel2/test_search_earth_search_collection_1.yaml +0 -0
  157. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/cassettes/test_sentinel2/test_search_planetary_computer.yaml +0 -0
  158. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/conftest.py +0 -0
  159. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/fixtures/make_fixtures.py +0 -0
  160. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_contract.py +0 -0
  161. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_era5.py +0 -0
  162. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_hls.py +0 -0
  163. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_koppen_geiger.py +0 -0
  164. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_modis.py +0 -0
  165. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_planetscope.py +0 -0
  166. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_sentinel1.py +0 -0
  167. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_sentinel2.py +0 -0
  168. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_snodas.py +0 -0
  169. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_ucla_sr.py +0 -0
  170. {easysnowdata-0.3.1 → easysnowdata-0.3.2}/tests/timeseries/test_viirs.py +0 -0
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  <!-- insertion marker -->
9
+ ## [v0.3.1](https://github.com/egagli/easysnowdata/releases/tag/v0.3.1) - 2026-09-22
10
+
11
+ <small>[Compare with v0.2.0](https://github.com/egagli/easysnowdata/compare/v0.2.0...v0.3.1)</small>
12
+
13
+ ### Fixed
14
+
15
+ - fix(planetscope): make a real order work end to end; gallery renders a real scene ([bd25c45](https://github.com/egagli/easysnowdata/commit/bd25c45c4212db5b1a5e212dec2fe295a9f1f5d6) by egagli).
16
+ - fix(archive): open a scheme-less store path as a local store ([d71b0ee](https://github.com/egagli/easysnowdata/commit/d71b0ee753cef56ec9fdc0dc248b85fda202b5b5) by Eric Gagliano).
17
+ - fix(sar): never fuse Sentinel-1 geometries — one raster per relative orbit ([c9fbe53](https://github.com/egagli/easysnowdata/commit/c9fbe53433a9ed8a36edadca1f72cafe67b387d8) by egagli).
18
+ - fix(hls): a failed band read raises instead of filling the Dataset with nodata ([101d97f](https://github.com/egagli/easysnowdata/commit/101d97f5aa482ed74c4185486dbc8a3710c4fb26) by egagli).
19
+ - fix(auth): log in at URS with the netrc for GDAL reads whenever a password exists ([1b7a1a1](https://github.com/egagli/easysnowdata/commit/1b7a1a13a66f3e71288967e41dcc61723dac5922) by egagli).
20
+ - fix(sar): drop the nominal Sentinel-1 heading and the constant 39° incidence angle ([736499e](https://github.com/egagli/easysnowdata/commit/736499e0fcfdef3c1f4bfdc9ea33c5417198806a) by egagli).
21
+ - fix(snow): Terra-only MODIS mosaics from Planetary Computer; real NSIDC-0768 file names ([8bbbdbd](https://github.com/egagli/easysnowdata/commit/8bbbdbdbe6d6c11411c867b07f5d578a672233ec) by egagli).
22
+ - fix(sar): incidence angle from each track's geometry, with the right sign; OPERA reads work ([d12dc9f](https://github.com/egagli/easysnowdata/commit/d12dc9f37684c9dfb73287c08e0c2472f11254f3) by egagli).
23
+ - fix(health): probe Planet through the SDK, send a User-Agent on first-byte probes ([e5aeb82](https://github.com/egagli/easysnowdata/commit/e5aeb82fc28802f3539aaea69c93e3d185b32553) by egagli).
24
+ - fix(stations): finish the transfer — format the clients, relabel the probes, refresh the archive ([fb1bf3e](https://github.com/egagli/easysnowdata/commit/fb1bf3e191e27ee7453e23830a99bd6c85731016) by egagli).
25
+ - fix(deprecation): the shims promised removal in the release they shipped in ([24e93b5](https://github.com/egagli/easysnowdata/commit/24e93b549e928cb6cfabd103b8ae40f9d1581b07) by egagli).
26
+
9
27
  ## [v0.2.0](https://github.com/egagli/easysnowdata/releases/tag/v0.2.0) - 2026-09-17
10
28
 
11
29
  <small>[Compare with v0.0.26](https://github.com/egagli/easysnowdata/compare/v0.0.26...v0.2.0)</small>
@@ -38,5 +38,5 @@ keywords:
38
38
  - snow classification
39
39
  - easysnowdata
40
40
  license: MIT
41
- version: v0.3.1
42
- date-released: '2026-09-22'
41
+ version: v0.3.2
42
+ date-released: '2026-09-23'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: easysnowdata
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A Python package to easily retrieve data relevant to snow science
5
5
  Project-URL: Homepage, https://github.com/egagli/easysnowdata
6
6
  Project-URL: Documentation, https://egagli.github.io/easysnowdata
@@ -912,10 +912,12 @@ that artefact (its `docs/STORAGE.md` and DESIGN.md §6.5). `easysnowdata`
912
912
  reads the inventory over HTTPS at call time; `archive.load()` reads the
913
913
  store by default, fetching only the chunks a request touches (one water year
914
914
  of every station 0.7 MB, one station's whole record 0.5 MB, against the
915
- 27 MB bundle), and falls back to the `pooch`-cached bundle with a warning
916
- when the store cannot be read, so a Pages outage degrades rather than fails.
917
- The bundle stays published until this version is on PyPI and conda-forge,
918
- because older pins read nothing else.
915
+ 27 MB bundle), and falls back with a warning to the bundle attached to that
916
+ repo's latest snapshot release when the store cannot be read, so a Pages
917
+ outage degrades to a possibly-stale snapshot rather than failing (Releases
918
+ are served from outside Pages). The daily-committed bundle on `main` was
919
+ retired once 0.3.2 carried this fallback; 0.2.x and 0.3.1 read that file and
920
+ must upgrade.
919
921
 
920
922
  **What `global_snow_networks` still owes this migration** is tracked in that
921
923
  repo as [`docs/EASYSNOWDATA_MIGRATION.md`](https://github.com/egagli/global_snow_networks/blob/main/docs/EASYSNOWDATA_MIGRATION.md):
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.1'
22
- __version_tuple__ = version_tuple = (0, 3, 1)
21
+ __version__ = version = '0.3.2'
22
+ __version_tuple__ = version_tuple = (0, 3, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -16,10 +16,12 @@ artefacts that this module reads (§9 step 3):
16
16
  range requests, so a query fetches the chunks it touches — well under a
17
17
  megabyte for either of those — instead of the whole bundle. The default
18
18
  route.
19
- ``data/all_station_csvs.tar.xz``
20
- One ``date,wteq_cm,snwd_cm`` CSV per daily-or-better station, bundled.
21
- About 28 MB, whole-file only; the route that predates the store, kept
22
- so older pins keep working and as the fallback when Pages is down.
19
+ ``all_station_csvs.tar.xz`` on that repo's latest GitHub Release
20
+ One ``date,wteq_cm,snwd_cm`` CSV per daily-or-better station, bundled:
21
+ the periodic, DOI'd snapshot (a few times a year), about 28 MB and
22
+ whole-file only. The fallback when Pages is down, since Releases are
23
+ served from elsewhere — and the route to use when a result should be
24
+ pinned to a citable snapshot rather than to today's archive.
23
25
 
24
26
  ::
25
27
 
@@ -64,10 +66,12 @@ __all__ = [
64
66
  "MANIFEST_URL",
65
67
  "PAGES_BASE",
66
68
  "PRODUCT",
69
+ "RELEASES_API",
67
70
  "REPO",
68
71
  "ZARR_URLS",
69
72
  "csv_url",
70
73
  "inventory",
74
+ "latest_snapshot",
71
75
  "load",
72
76
  "manifest",
73
77
  "network_of",
@@ -77,7 +81,15 @@ _logger = logging.getLogger(__name__)
77
81
 
78
82
  REPO = "https://github.com/egagli/global_snow_networks"
79
83
  INVENTORY_URL = f"{REPO}/raw/main/all_snow_stations.geojson"
80
- ARCHIVE_URL = f"{REPO}/raw/main/data/all_station_csvs.tar.xz"
84
+ #: The bundled CSVs of the latest snapshot release. Fixed-name asset, so this
85
+ #: URL always resolves to the newest release; :func:`latest_snapshot` asks the
86
+ #: API for the tag and date behind it. Until 2026-09 the bundle was a file
87
+ #: committed daily on ``main``; that stopped once the Pages store took over
88
+ #: the daily role (that repo's docs/STORAGE.md §3).
89
+ ARCHIVE_URL = f"{REPO}/releases/latest/download/all_station_csvs.tar.xz"
90
+ RELEASES_API = (
91
+ "https://api.github.com/repos/egagli/global_snow_networks/releases/latest"
92
+ )
81
93
  CSV_BASE = (
82
94
  "https://raw.githubusercontent.com/egagli/global_snow_networks/main/data/stations/"
83
95
  )
@@ -100,9 +112,10 @@ _BY_STATION_LIMIT = 64
100
112
  #: output contract keeps the two-letter names the CSV columns abbreviate.
101
113
  _STORE_COLUMNS = {"swe": "wteq_cm", "snow_depth": "snwd_cm"}
102
114
 
103
- #: How long a downloaded bundle is trusted before it is fetched again. The
104
- #: archive is rebuilt daily, so a day; the inventory, read over HTTP each
105
- #: time, would otherwise list stations the stale bundle does not hold.
115
+ #: How long a downloaded bundle is trusted before it is fetched again, when
116
+ #: the release it came from could not be identified (the API was unreachable)
117
+ #: and the cache file therefore cannot be named after its tag. A tag-named
118
+ #: file is immutable and never re-fetched.
106
119
  ARCHIVE_MAX_AGE = 24 * 3600
107
120
 
108
121
  #: Archive CSV column -> (standardized type, units).
@@ -269,6 +282,31 @@ def _fetch_json(url: str) -> dict[str, Any]:
269
282
  return response.json()
270
283
 
271
284
 
285
+ def latest_snapshot() -> dict[str, Any]:
286
+ """The newest snapshot release of ``global_snow_networks``.
287
+
288
+ ``{"tag", "published_at", "html_url", "tarball_url"}`` from the GitHub
289
+ releases API — the tag is what a Zenodo version DOI corresponds to, so
290
+ this is what to cite when a result came from the bundle. ``tarball_url``
291
+ is the bundled-CSV asset; ``None`` when the release carries none.
292
+ """
293
+ release = _fetch_json(RELEASES_API)
294
+ assets = release.get("assets") or []
295
+ tarball = None
296
+ for asset in assets:
297
+ name = str(asset.get("name", ""))
298
+ if name.startswith("all_station_csvs") and name.endswith(".tar.xz"):
299
+ tarball = asset.get("browser_download_url")
300
+ if name == "all_station_csvs.tar.xz":
301
+ break
302
+ return {
303
+ "tag": str(release.get("tag_name") or ""),
304
+ "published_at": str(release.get("published_at") or "")[:10],
305
+ "html_url": str(release.get("html_url") or ""),
306
+ "tarball_url": tarball,
307
+ }
308
+
309
+
272
310
  # ── reading the data ─────────────────────────────────────────────────────────
273
311
 
274
312
 
@@ -278,20 +316,53 @@ def _parse_csv(text: str, code: str) -> pd.DataFrame:
278
316
  return frame.set_index("date").sort_index()
279
317
 
280
318
 
281
- def _from_tarball(codes: set[str] | None) -> dict[str, pd.DataFrame]:
282
- """Every wanted station's CSV, out of the one bundled archive.
319
+ def _snapshot_tarball() -> tuple[str, dict[str, Any]]:
320
+ """URL of the latest snapshot's bundle, and what is known about the release.
321
+
322
+ When the releases API cannot be reached the fixed-name download URL is
323
+ used blind — it still resolves to the newest release — and the returned
324
+ metadata is empty.
325
+ """
326
+ try:
327
+ info = latest_snapshot()
328
+ except Exception as exc: # noqa: BLE001 — the download URL works without it
329
+ _logger.warning(
330
+ "Could not read the latest snapshot release (%s); downloading the "
331
+ "bundle without knowing its tag.",
332
+ exc,
333
+ )
334
+ return ARCHIVE_URL, {}
335
+ return info.get("tarball_url") or ARCHIVE_URL, info
336
+
337
+
338
+ def _from_tarball(
339
+ codes: set[str] | None,
340
+ ) -> tuple[dict[str, pd.DataFrame], dict[str, Any]]:
341
+ """Every wanted station's CSV, out of the latest snapshot's bundle.
283
342
 
284
- Downloaded into the package cache by pooch and re-read from there for a
285
- day (:data:`ARCHIVE_MAX_AGE`), after which the daily rebuild is fetched
286
- again. ``EASYSNOWDATA_CACHE_DIR`` moves the cache root.
343
+ Downloaded into the package cache by pooch. A bundle whose release tag is
344
+ known is cached under that tag and never re-fetched; one downloaded blind
345
+ is re-fetched after :data:`ARCHIVE_MAX_AGE`. ``EASYSNOWDATA_CACHE_DIR``
346
+ moves the cache root. Returns the frames and the release metadata.
287
347
  """
348
+ url, info = _snapshot_tarball()
349
+ tag = info.get("tag")
288
350
  path = providers.raster_http.fetch(
289
- ARCHIVE_URL,
290
- "all_station_csvs.tar.xz",
351
+ url,
352
+ f"all_station_csvs-{tag}.tar.xz" if tag else "all_station_csvs.tar.xz",
291
353
  subdir="stations",
292
- max_age=ARCHIVE_MAX_AGE,
354
+ max_age=None if tag else ARCHIVE_MAX_AGE,
293
355
  )
294
- _logger.info("Reading the bundled station archive at %s", path)
356
+ if tag:
357
+ _logger.info(
358
+ "Reading the bundled station archive of snapshot %s (%s) at %s; the "
359
+ "daily archive on Pages may be ahead of it.",
360
+ tag,
361
+ info.get("published_at") or "date unknown",
362
+ path,
363
+ )
364
+ else:
365
+ _logger.info("Reading the bundled station archive at %s", path)
295
366
  frames: dict[str, pd.DataFrame] = {}
296
367
  with tarfile.open(path, mode="r:xz") as tar:
297
368
  for member in tar:
@@ -305,7 +376,7 @@ def _from_tarball(codes: set[str] | None) -> dict[str, pd.DataFrame]:
305
376
  continue
306
377
  frames[code] = _parse_csv(handle.read().decode("utf-8"), code)
307
378
  _logger.info("Read %d station CSVs from the archive", len(frames))
308
- return frames
379
+ return frames, {**info, "tarball_url": url}
309
380
 
310
381
 
311
382
  def _from_csvs(codes: list[str]) -> dict[str, pd.DataFrame]:
@@ -420,7 +491,7 @@ def load(
420
491
  time
421
492
  Any form :func:`easysnowdata.temporal.parse_time` accepts. Each
422
493
  station's series is cut to the window before the dense grid is built,
423
- so a narrow window is cheap — but the bundle is still downloaded
494
+ so a narrow window is cheap — on the bundle route it is still downloaded
424
495
  whole, because that is how it is published.
425
496
  networks
426
497
  Keep only these networks.
@@ -432,12 +503,15 @@ def load(
432
503
  every station is one or two chunks). Its ``time`` axis is complete
433
504
  and daily between the first and last observation, and its values are
434
505
  stored as float32 (returned as float64), so they agree with the CSV
435
- routes to float32 precision. When the store
436
- cannot be read and no *source* was asked for, the tarball route below
437
- is used instead, with a warning.
438
- ``"github-tarball"`` downloads the one ~28 MB bundle and reads every
439
- wanted CSV out of it; its ``time`` axis holds only days on which some
440
- station observed.
506
+ routes to float32 precision. When the store cannot be read and no
507
+ *source* was asked for, the snapshot route below is used instead,
508
+ with a warning naming the snapshot.
509
+ ``"github-tarball"`` downloads the ~28 MB bundle of CSVs attached to
510
+ that repo's **latest snapshot release** — a few times a year, each
511
+ with a Zenodo DOI — and reads every wanted CSV out of it. Ask for it
512
+ when a result should be pinned to a citable snapshot; the Dataset's
513
+ ``snapshot_tag`` and ``snapshot_published_at`` say which one. Its
514
+ ``time`` axis holds only days on which some station observed.
441
515
  ``"github-csv"`` fetches one CSV per station instead, which needs
442
516
  neither a store nor a temporary file.
443
517
  hemisphere
@@ -500,19 +574,24 @@ def load(
500
574
  if source is not None:
501
575
  raise
502
576
  # The store is a build artefact of a Pages deploy; the bundle is
503
- # a committed file. Both hold the same observations, so a Pages
504
- # outage (or a deploy that has not happened yet) degrades to a
505
- # 28 MB download rather than an error (REVAMP_PLAN §9.3).
577
+ # a GitHub Release asset, served from elsewhere. So a Pages outage
578
+ # (or a deploy that has not happened yet) degrades to the latest
579
+ # snapshot — possibly months behind, and the warning says so —
580
+ # rather than to an error (REVAMP_PLAN §9.3).
581
+ src = product.source("github-tarball")
582
+ frames, snapshot = _from_tarball(set(codes) if codes is not None else None)
506
583
  _logger.warning(
507
- "Could not read the chunked archive at %s (%s); reading the "
508
- "bundled CSVs instead.",
584
+ "Could not read the chunked archive at %s (%s); read the bundled "
585
+ "CSVs of snapshot %s (%s) instead, which may be behind the daily "
586
+ "archive.",
509
587
  ZARR_URLS[layout],
510
588
  exc,
589
+ snapshot.get("tag") or "unknown",
590
+ snapshot.get("published_at") or "date unknown",
511
591
  )
512
- src = product.source("github-tarball")
513
- frames = _from_tarball(set(codes) if codes is not None else None)
514
592
  ds = _to_dataset(frames, wanted_types, codes, inv, hemisphere, window)
515
- source_url = ARCHIVE_URL
593
+ source_url = snapshot["tarball_url"]
594
+ ds.attrs.update(_snapshot_attrs(snapshot))
516
595
  else:
517
596
  ds = _finish(grid, inv, hemisphere)
518
597
  source_url = ZARR_URLS[layout]
@@ -526,15 +605,25 @@ def load(
526
605
  ds = _to_dataset(frames, wanted_types, codes, inv, hemisphere, window)
527
606
  source_url = CSV_BASE
528
607
  else:
529
- frames = _from_tarball(set(codes) if codes is not None else None)
608
+ frames, snapshot = _from_tarball(set(codes) if codes is not None else None)
530
609
  ds = _to_dataset(frames, wanted_types, codes, inv, hemisphere, window)
531
- source_url = ARCHIVE_URL
610
+ source_url = snapshot["tarball_url"]
611
+ ds.attrs.update(_snapshot_attrs(snapshot))
532
612
 
533
613
  ds.attrs.update(contract.provenance(product, src, source_url=source_url))
534
614
  ds.attrs["interval"] = "daily"
535
615
  return ds
536
616
 
537
617
 
618
+ def _snapshot_attrs(snapshot: dict[str, Any]) -> dict[str, str]:
619
+ """Which snapshot release a bundle-route Dataset came from, for its attrs."""
620
+ return {
621
+ "snapshot_tag": str(snapshot.get("tag") or ""),
622
+ "snapshot_published_at": str(snapshot.get("published_at") or ""),
623
+ "snapshot_url": str(snapshot.get("html_url") or ""),
624
+ }
625
+
626
+
538
627
  def _types(variables: Any) -> list[str]:
539
628
  known = {type_name for type_name, _ in COLUMNS.values()}
540
629
  if variables is None:
@@ -640,7 +729,8 @@ PRODUCT = Product(
640
729
  "pre-downloaded and published by global_snow_networks: a normalized "
641
730
  "station inventory as GeoJSON, a chunked Zarr store on its Pages site "
642
731
  "that a query reads only the touched chunks of, and one CSV per "
643
- "station bundled into a single ~28 MB archive. The fast path for "
732
+ "station bundled into a single ~28 MB archive on each snapshot release. "
733
+ "The fast path for "
644
734
  "'everything daily' without hitting five APIs, refreshed daily. Most "
645
735
  "stations start around 1980; "
646
736
  "a few long snow courses reach back to 1896. SWE and snow depth only; for "
@@ -676,13 +766,14 @@ PRODUCT = Product(
676
766
  location=ARCHIVE_URL,
677
767
  extent="western US, western Canada, Norway, Yukon, California, BC",
678
768
  temporal="1896/present (most stations from ~1980)",
679
- latency="daily rebuild",
769
+ latency="per snapshot release (a few times a year)",
680
770
  notes=(
681
- "one ~28 MB download holding every station CSV; cached, so "
682
- "the whole archive costs one request. The fallback when the "
683
- "Pages store cannot be read"
771
+ "one ~28 MB download holding every station CSV of the latest "
772
+ "snapshot release (each has a Zenodo DOI); cached per tag. The "
773
+ "citable route, and the fallback when the Pages store cannot "
774
+ "be read"
684
775
  ),
685
- title="global_snow_networks bundled archive",
776
+ title="global_snow_networks snapshot bundle (latest release)",
686
777
  # The inventory and CSV probes were once labelled "SNOTEL/CCSS …"
687
778
  # after the retired snotel_ccss_stations entry; the labels were
688
779
  # renamed in 0.3 and data_status/history.json rewritten with them,
@@ -694,7 +785,7 @@ PRODUCT = Product(
694
785
  ),
695
786
  Probe(
696
787
  "Snow station archive tarball (global_snow_networks)",
697
- partial(health.http_first_byte, ARCHIVE_URL),
788
+ partial(health.http_first_byte, RELEASES_API),
698
789
  ),
699
790
  ),
700
791
  ),
@@ -105,7 +105,7 @@ exclude = [
105
105
  # The tag it creates is what hatch-vcs reads; the files below are the ones
106
106
  # that still carry a literal version.
107
107
  [tool.bumpversion]
108
- current_version = "0.3.1"
108
+ current_version = "0.3.2"
109
109
  commit = true
110
110
  tag = true
111
111
 
@@ -28,17 +28,45 @@ from .conftest import MORSE_LAKE_CODE, PARADISE_CODE, RAINIER
28
28
  pytestmark = pytest.mark.allow_hosts(["127.0.0.1"])
29
29
 
30
30
 
31
+ FAKE_RELEASE = {
32
+ "tag_name": "v2026.09.22",
33
+ "published_at": "2026-09-22T19:31:00Z",
34
+ "html_url": "https://github.com/egagli/global_snow_networks/releases/tag/v2026.09.22",
35
+ "assets": [
36
+ {
37
+ "name": "all_station_csvs-v2026.09.22.tar.xz",
38
+ "browser_download_url": "https://example.test/all_station_csvs-v2026.09.22.tar.xz",
39
+ },
40
+ {
41
+ "name": "all_station_csvs.tar.xz",
42
+ "browser_download_url": "https://example.test/all_station_csvs.tar.xz",
43
+ },
44
+ {"name": "by_time.zarr-v2026.09.22.zip", "browser_download_url": "x"},
45
+ ],
46
+ }
47
+
48
+
31
49
  @pytest.fixture
32
50
  def local_archive(station_fixtures, monkeypatch):
33
51
  """Point the archive module at the fixtures instead of GitHub."""
34
52
  monkeypatch.setattr(
35
53
  archive, "INVENTORY_URL", str(station_fixtures["inventory"]), raising=False
36
54
  )
37
- monkeypatch.setattr(
38
- archive.providers.raster_http,
39
- "fetch",
40
- lambda *a, **k: station_fixtures["archive"],
41
- )
55
+ fetched: list[dict] = []
56
+
57
+ def fake_fetch(url, fname=None, **kwargs):
58
+ fetched.append({"url": url, "fname": fname, **kwargs})
59
+ return station_fixtures["archive"]
60
+
61
+ monkeypatch.setattr(archive.providers.raster_http, "fetch", fake_fetch)
62
+ station_fixtures["fetched"] = fetched # type: ignore[assignment]
63
+
64
+ def fake_json(url):
65
+ if url == archive.RELEASES_API:
66
+ return FAKE_RELEASE
67
+ raise AssertionError(f"unexpected JSON fetch: {url}")
68
+
69
+ monkeypatch.setattr(archive, "_fetch_json", fake_json)
42
70
  monkeypatch.setattr(
43
71
  archive,
44
72
  "ZARR_URLS",
@@ -83,7 +111,10 @@ def test_catalog_entry_is_registered_from_this_module():
83
111
 
84
112
  def test_urls_point_at_the_published_artefacts():
85
113
  assert archive.INVENTORY_URL.endswith("/all_snow_stations.geojson")
86
- assert archive.ARCHIVE_URL.endswith("/data/all_station_csvs.tar.xz")
114
+ assert archive.ARCHIVE_URL.endswith(
115
+ "/releases/latest/download/all_station_csvs.tar.xz"
116
+ )
117
+ assert archive.RELEASES_API.endswith("/global_snow_networks/releases/latest")
87
118
  assert archive.csv_url(PARADISE_CODE).endswith(f"/stations/{PARADISE_CODE}.csv")
88
119
  assert archive.PAGES_BASE == "https://egagli.github.io/global_snow_networks/archive"
89
120
  assert archive.ZARR_URLS["by_time"].endswith("/archive/by_time.zarr")
@@ -184,7 +215,47 @@ def test_tarball_route_builds_the_station_time_dataset(local_archive):
184
215
  assert ds.sizes == {"station": 3, "time": 7}
185
216
  assert ds["swe"].dtype == np.float64
186
217
  assert ds.attrs["source"] == "github-tarball"
187
- assert ds.attrs["source_url"] == archive.ARCHIVE_URL
218
+ # The bundle is the latest snapshot release's fixed-name asset, and the
219
+ # result says which snapshot it came from.
220
+ assert ds.attrs["source_url"] == "https://example.test/all_station_csvs.tar.xz"
221
+ assert ds.attrs["snapshot_tag"] == "v2026.09.22"
222
+ assert ds.attrs["snapshot_published_at"] == "2026-09-22"
223
+ assert ds.attrs["snapshot_url"].endswith("/releases/tag/v2026.09.22")
224
+ fetched = local_archive["fetched"][-1]
225
+ assert fetched["fname"] == "all_station_csvs-v2026.09.22.tar.xz" # cached per tag
226
+ assert fetched["max_age"] is None # a tagged bundle is immutable
227
+
228
+
229
+ def test_latest_snapshot_reads_the_release(monkeypatch):
230
+ monkeypatch.setattr(archive, "_fetch_json", lambda url: FAKE_RELEASE)
231
+ info = archive.latest_snapshot()
232
+ assert info["tag"] == "v2026.09.22"
233
+ assert info["published_at"] == "2026-09-22"
234
+ assert info["tarball_url"] == "https://example.test/all_station_csvs.tar.xz"
235
+
236
+
237
+ def test_latest_snapshot_accepts_a_release_with_only_the_tagged_bundle(monkeypatch):
238
+ release = {**FAKE_RELEASE, "assets": FAKE_RELEASE["assets"][:1]}
239
+ monkeypatch.setattr(archive, "_fetch_json", lambda url: release)
240
+ assert archive.latest_snapshot()["tarball_url"].endswith("-v2026.09.22.tar.xz")
241
+
242
+
243
+ @pytest.mark.recorded
244
+ def test_bundle_route_survives_an_unreachable_releases_api(
245
+ local_archive, monkeypatch, caplog
246
+ ):
247
+ def boom(url):
248
+ raise RuntimeError("HTTP 403 rate limited")
249
+
250
+ monkeypatch.setattr(archive, "_fetch_json", boom)
251
+ ds = archive.load(source="github-tarball")
252
+ assert ds.sizes == {"station": 3, "time": 7}
253
+ assert ds.attrs["source_url"] == archive.ARCHIVE_URL # the fixed-name URL, blind
254
+ assert ds.attrs["snapshot_tag"] == ""
255
+ fetched = local_archive["fetched"][-1]
256
+ assert fetched["fname"] == "all_station_csvs.tar.xz"
257
+ assert fetched["max_age"] == archive.ARCHIVE_MAX_AGE
258
+ assert "without knowing its tag" in caplog.text
188
259
 
189
260
 
190
261
  @pytest.mark.recorded
@@ -241,7 +312,9 @@ def test_an_unreachable_store_falls_back_to_the_tarball_with_a_warning(
241
312
  ds = archive.load()
242
313
  assert ds.attrs["source"] == "github-tarball"
243
314
  assert ds.sizes == {"station": 3, "time": 7}
244
- assert "reading the bundled CSVs instead" in caplog.text
315
+ assert ds.attrs["snapshot_tag"] == "v2026.09.22"
316
+ assert "snapshot v2026.09.22 (2026-09-22)" in caplog.text
317
+ assert "may be behind the daily archive" in caplog.text
245
318
 
246
319
 
247
320
  @pytest.mark.recorded
@@ -377,10 +450,18 @@ def test_live_archive_store_route():
377
450
  assert archive.manifest()["built_from_commit"]
378
451
 
379
452
 
453
+ @pytest.mark.live
454
+ def test_live_latest_snapshot_has_a_bundle():
455
+ info = archive.latest_snapshot()
456
+ assert info["tag"].startswith("v")
457
+ assert info["tarball_url"] and info["tarball_url"].endswith(".tar.xz")
458
+
459
+
380
460
  @pytest.mark.live
381
461
  def test_live_archive_routes_agree():
382
462
  when = "2024-01-01/2024-03-31"
383
463
  bundled = archive.load([PARADISE_CODE], source="github-tarball", time=when)
464
+ assert bundled.attrs["snapshot_tag"]
384
465
  per_station = archive.load([PARADISE_CODE], source="github-csv", time=when)
385
466
  chunked = archive.load([PARADISE_CODE], source="github-pages-zarr", time=when)
386
467
  assert bundled.attrs["source"] == "github-tarball"
File without changes
File without changes
File without changes
File without changes