spai 2026.7.7.dev2__tar.gz → 2026.8.6.dev0__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 (260) hide show
  1. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/.gitignore +1 -0
  2. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/PKG-INFO +3 -4
  3. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/makefile +1 -0
  4. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/pyproject.toml +8 -4
  5. spai-2026.8.6.dev0/spai/__init__.py +1 -0
  6. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/cli.py +0 -1
  7. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/copernicus/download_copernicus.py +1 -1
  8. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/models/StorageConfig.py +1 -0
  9. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/deploy_folder.py +2 -2
  10. {spai-2026.7.7.dev2/spai/data/sources → spai-2026.8.6.dev0/spai/project/project_template}/__init__.py +0 -0
  11. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/run_local.py +5 -0
  12. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/repos/APIRepo.py +14 -0
  13. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/LocalStorage.py +3 -1
  14. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/S3Storage.py +68 -35
  15. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/Storage.py +6 -4
  16. spai-2026.8.6.dev0/spai/storage/presigned_object_response.py +28 -0
  17. spai-2026.8.6.dev0/spai/storage/s3_get_object_store.py +44 -0
  18. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/config}/__init__.py +0 -0
  19. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/tests/config/test_vars.py +1 -1
  20. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/task_logging/test_task_logger.py +1 -1
  21. {spai-2026.7.7.dev2/tests/config → spai-2026.8.6.dev0/tests/deprecated/test_processing}/__init__.py +0 -0
  22. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_autocategorize1D.py +1 -1
  23. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_mask_raster.py +1 -1
  24. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_normalised_difference.py +1 -1
  25. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_px_count.py +1 -1
  26. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_read_raster.py +1 -1
  27. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_save_table.py +1 -1
  28. spai-2026.8.6.dev0/tests/deprecated/test_pulses/spai +1 -0
  29. spai-2026.8.6.dev0/tests/unit/storage/conftest.py +127 -0
  30. spai-2026.8.6.dev0/tests/unit/storage/factories.py +354 -0
  31. spai-2026.8.6.dev0/tests/unit/storage/output/array.npy +0 -0
  32. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.nc +0 -0
  33. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/.zattrs +1 -0
  34. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/.zgroup +3 -0
  35. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/.zmetadata +87 -0
  36. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/temp/.zarray +22 -0
  37. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/temp/.zattrs +6 -0
  38. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/temp/0.0 +0 -0
  39. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/x/.zarray +20 -0
  40. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/x/.zattrs +5 -0
  41. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/x/0 +0 -0
  42. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/y/.zarray +20 -0
  43. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/y/.zattrs +5 -0
  44. spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/y/0 +0 -0
  45. spai-2026.8.6.dev0/tests/unit/storage/output/dict.geojson +1 -0
  46. spai-2026.8.6.dev0/tests/unit/storage/output/dict.json +1 -0
  47. spai-2026.8.6.dev0/tests/unit/storage/output/from_path.txt +1 -0
  48. spai-2026.8.6.dev0/tests/unit/storage/output/hello.txt +1 -0
  49. spai-2026.8.6.dev0/tests/unit/storage/output/image.png +0 -0
  50. spai-2026.8.6.dev0/tests/unit/storage/output/table.csv +3 -0
  51. spai-2026.8.6.dev0/tests/unit/storage/output/table.json +1 -0
  52. spai-2026.8.6.dev0/tests/unit/storage/output/table.parquet +0 -0
  53. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.cpg +1 -0
  54. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.dbf +0 -0
  55. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.geojson +9 -0
  56. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.gml +24 -0
  57. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.parquet +0 -0
  58. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.prj +1 -0
  59. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.shp +0 -0
  60. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.shx +0 -0
  61. spai-2026.8.6.dev0/tests/unit/storage/output/vectors.xsd +53 -0
  62. spai-2026.8.6.dev0/tests/unit/storage/test_local_storage.py +351 -0
  63. spai-2026.8.6.dev0/tests/unit/storage/test_s3_storage.py +444 -0
  64. spai-2026.8.6.dev0/tests/unit/storage/test_s3_storage_managed.py +183 -0
  65. spai-2026.7.7.dev2/examples/test_era5.ipynb +0 -37
  66. spai-2026.7.7.dev2/spai/__init__.py +0 -1
  67. spai-2026.7.7.dev2/tests/test_pulses/__init__.py +0 -0
  68. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/README.md +0 -0
  69. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/00_setup.ipynb +0 -0
  70. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/00bis_download_satellite_imagery.ipynb +0 -0
  71. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/01_satellite_data_access.ipynb +0 -0
  72. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/02_geospatial_data_access.ipynb +0 -0
  73. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/03_storage.ipynb +0 -0
  74. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/04_infra_management.ipynb +0 -0
  75. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/05_mails.ipynb +0 -0
  76. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/06_cogs.ipynb +0 -0
  77. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/99_new_data_access.ipynb +0 -0
  78. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/03_xyz_api.ipynb +0 -0
  79. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/03_xyz_api.py +0 -0
  80. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/05_nominatim_example.ipynb +0 -0
  81. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/06_colorize.ipynb +0 -0
  82. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/10_select_one_gdf_from_map.ipynb +0 -0
  83. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/11_forest_monitoring_flow.ipynb +0 -0
  84. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/12_forest_monitoring.ipynb +0 -0
  85. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/13_ms_buildings_download.ipynb +0 -0
  86. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/13_water_quality_flow.ipynb +0 -0
  87. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/14_download_dem.ipynb +0 -0
  88. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/14_water_quality.ipynb +0 -0
  89. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/15_perform_analysis.ipynb +0 -0
  90. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/00_download_satellite_imagery.ipynb +0 -0
  91. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/01_process_satellite_imagery.ipynb +0 -0
  92. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/02_analytics.ipynb +0 -0
  93. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/04_download_satellite_imagery_advanced.ipynb +0 -0
  94. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/21_storage.ipynb +0 -0
  95. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/llanca 2.geojson +0 -0
  96. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/llanca.geojson +0 -0
  97. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/map.geojson +0 -0
  98. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/sample.pdf +0 -0
  99. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/samples/AOI_Vegetation_Growth.json +0 -0
  100. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/main.py +0 -0
  101. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/__init__.py +0 -0
  102. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/forest_monitoring.py +0 -0
  103. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/utils.py +0 -0
  104. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/water_quality.py +0 -0
  105. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/__init__.py +0 -0
  106. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/auth.py +0 -0
  107. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/is_logged.py +0 -0
  108. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/logout.py +0 -0
  109. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/retrieve_credentials.py +0 -0
  110. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0/spai/commands}/__init__.py +0 -0
  111. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/auth.py +0 -0
  112. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/delete.py +0 -0
  113. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/list.py +0 -0
  114. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/stop.py +0 -0
  115. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/workspace.py +0 -0
  116. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/__init__.py +0 -0
  117. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/config.py +0 -0
  118. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/validate.py +0 -0
  119. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/vars.py +0 -0
  120. {spai-2026.7.7.dev2/spai/commands → spai-2026.8.6.dev0/spai/data}/__init__.py +0 -0
  121. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/README.md +0 -0
  122. {spai-2026.7.7.dev2/spai/data → spai-2026.8.6.dev0/spai/data/climate}/__init__.py +0 -0
  123. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/area_of_interest.py +0 -0
  124. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/earthdatahub/download_earthdatahub.py +0 -0
  125. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/ecmwf/download_ecmwf.py +0 -0
  126. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/era5.py +0 -0
  127. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/era5_vars.py +0 -0
  128. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/ecosystems/__init__.py +0 -0
  129. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/ecosystems/protected_areas.py +0 -0
  130. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/fire/__init__.py +0 -0
  131. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/fire/fwi/__init__.py +0 -0
  132. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/fire/fwi/fwi_nasa.py +0 -0
  133. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/hidrology/__init__.py +0 -0
  134. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/hidrology/waterbodies.py +0 -0
  135. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/hidrology/waterways.py +0 -0
  136. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/__init__.py +0 -0
  137. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/download.py +0 -0
  138. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/explore.py +0 -0
  139. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/load.py +0 -0
  140. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PCH4Downloader.py +0 -0
  141. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PCODownloader.py +0 -0
  142. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PDownloader.py +0 -0
  143. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PHCHODownloader.py +0 -0
  144. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PNO2Downloader.py +0 -0
  145. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PO3Downloader.py +0 -0
  146. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PSO2Downloader.py +0 -0
  147. {spai-2026.7.7.dev2/spai/data/climate → spai-2026.8.6.dev0/spai/data/satellite/sentinelhub/S5P}/__init__.py +0 -0
  148. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHCloudMaskDownloader.py +0 -0
  149. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHDEM30Downloader.py +0 -0
  150. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHDownloader.py +0 -0
  151. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHExplorer.py +0 -0
  152. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS1Downloader.py +0 -0
  153. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS2Downloader.py +0 -0
  154. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS2L1CDownloader.py +0 -0
  155. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS2L2ADownloader.py +0 -0
  156. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS3OLCIDownloader.py +0 -0
  157. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS3SLSTRDownloader.py +0 -0
  158. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/__init__.py +0 -0
  159. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/STACDownloader.py +0 -0
  160. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/__init__.py +0 -0
  161. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDEM30Downloader.py +0 -0
  162. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDEM90Downloader.py +0 -0
  163. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDEMDownloader.py +0 -0
  164. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDownloader.py +0 -0
  165. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSS2L2ADownloader.py +0 -0
  166. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/__init__.py +0 -0
  167. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/decorators.py +0 -0
  168. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCAlosPalsarAnnualDownloader.py +0 -0
  169. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCDownloader.py +0 -0
  170. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCESAWCDownloader.py +0 -0
  171. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCLandsat8Downloader.py +0 -0
  172. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCModisBurnedDownloader.py +0 -0
  173. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCModisSnow8Downloader.py +0 -0
  174. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCModisSnowDayDownloader.py +0 -0
  175. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCS1GRDDownloader.py +0 -0
  176. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCS1RTCDownloader.py +0 -0
  177. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/__init__.py +0 -0
  178. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/utils.py +0 -0
  179. {spai-2026.7.7.dev2/spai/data/satellite/sentinelhub/S5P → spai-2026.8.6.dev0/spai/data/sources}/__init__.py +0 -0
  180. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/sources/osm.py +0 -0
  181. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/__init__.py +0 -0
  182. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/buildings.py +0 -0
  183. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/pipelines.py +0 -0
  184. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/power.py +0 -0
  185. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/railroads.py +0 -0
  186. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/roads.py +0 -0
  187. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/errors/__init__.py +0 -0
  188. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/errors/auth.py +0 -0
  189. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/__init__.py +0 -0
  190. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/utils.py +0 -0
  191. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/__init__.py +0 -0
  192. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/cache.py +0 -0
  193. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/errors.py +0 -0
  194. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/get_image_tile.py +0 -0
  195. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/image_utils.py +0 -0
  196. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/logging/log.py +0 -0
  197. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/models/Config.py +0 -0
  198. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/models/__init__.py +0 -0
  199. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/__init__.py +0 -0
  200. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/autocategorize1D.py +0 -0
  201. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/colorize_raster.py +0 -0
  202. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/convert_array_to_vector.py +0 -0
  203. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/mask_raster.py +0 -0
  204. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/normalised_difference.py +0 -0
  205. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/px_count.py +0 -0
  206. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/rasterio_mask.py +0 -0
  207. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/read_raster.py +0 -0
  208. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/save_table.py +0 -0
  209. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/utils.py +0 -0
  210. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/__init__.py +0 -0
  211. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/delete_project.py +0 -0
  212. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/deploy_template.py +0 -0
  213. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/download_template.py +0 -0
  214. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_logs.py +0 -0
  215. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_project_by_name.py +0 -0
  216. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_projects.py +0 -0
  217. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_service_by_name_type_project.py +0 -0
  218. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_services.py +0 -0
  219. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/init_project.py +0 -0
  220. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/install_requirements.py +0 -0
  221. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/README.md +0 -0
  222. {spai-2026.7.7.dev2/spai/project/project_template → spai-2026.8.6.dev0/spai/project/project_template/apis}/__init__.py +0 -0
  223. {spai-2026.7.7.dev2/spai/project/project_template/apis → spai-2026.8.6.dev0/spai/project/project_template/apis/analytics}/__init__.py +0 -0
  224. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/analytics/main.py +0 -0
  225. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/analytics/requirements.txt +0 -0
  226. {spai-2026.7.7.dev2/spai/project/project_template/apis/analytics → spai-2026.8.6.dev0/spai/project/project_template/apis/xyz}/__init__.py +0 -0
  227. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/xyz/main.py +0 -0
  228. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/xyz/requirements.txt +0 -0
  229. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/notebooks/analytics/main.ipynb +0 -0
  230. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/notebooks/analytics/requirements.txt +0 -0
  231. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/analytics/main.py +0 -0
  232. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/analytics/requirements.txt +0 -0
  233. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/downloader/main.py +0 -0
  234. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/downloader/requirements.txt +0 -0
  235. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/spai.config.local.yaml +0 -0
  236. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/spai.config.yaml +0 -0
  237. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/spai.vars.json +0 -0
  238. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/uis/map/main.py +0 -0
  239. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/uis/map/requirements.txt +0 -0
  240. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/stop_service.py +0 -0
  241. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/repos/AuthRepo.py +0 -0
  242. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/repos/__init__.py +0 -0
  243. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/BaseStorage.py +0 -0
  244. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/CloudStorage.py +0 -0
  245. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/__init__.py +0 -0
  246. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/create_s3.py +0 -0
  247. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/decorators.py +0 -0
  248. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/minio.py +0 -0
  249. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/task_logging/task_logger.py +0 -0
  250. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/add_user_to_workspace.py +0 -0
  251. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/change_user_role.py +0 -0
  252. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/create_workspace.py +0 -0
  253. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/get_workspaces.py +0 -0
  254. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/remove_user_from_workspace.py +0 -0
  255. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/remove_workspace.py +0 -0
  256. {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/update_workspace.py +0 -0
  257. {spai-2026.7.7.dev2/spai/project/project_template/apis/xyz → spai-2026.8.6.dev0/tests}/__init__.py +0 -0
  258. {spai-2026.7.7.dev2/tests/test_processing → spai-2026.8.6.dev0/tests/deprecated/test_pulses}/__init__.py +0 -0
  259. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_pulses/results_forest_monitoring.ipynb +0 -0
  260. {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_pulses/results_water_quality.ipynb +0 -0
@@ -2,6 +2,7 @@ __pycache__
2
2
  .pytest_cache
3
3
  data
4
4
  outputs
5
+ spai/tests/unit/storage/output
5
6
  .env
6
7
  .spai
7
8
  tmp
@@ -1,9 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spai
3
- Version: 2026.7.7.dev2
3
+ Version: 2026.8.6.dev0
4
4
  Author-email: Jan Sensio <it@earthpulse.es>
5
5
  Requires-Python: >=3.12
6
- Requires-Dist: bson>=0.5.10
7
6
  Requires-Dist: pandas>=2.0.2
8
7
  Requires-Dist: pillow>=10.2.0
9
8
  Requires-Dist: pydantic>=2.0.3
@@ -22,7 +21,7 @@ Requires-Dist: odc-stac>=0.4.0; extra == 'all'
22
21
  Requires-Dist: planetary-computer>=1.0.0; extra == 'all'
23
22
  Requires-Dist: pyarrow>=19.0.0; extra == 'all'
24
23
  Requires-Dist: pystac-client>=0.9.0; extra == 'all'
25
- Requires-Dist: rasterio<=1.3.10; extra == 'all'
24
+ Requires-Dist: rasterio>=1.3.11; extra == 'all'
26
25
  Requires-Dist: rioxarray>=0.18.2; extra == 'all'
27
26
  Requires-Dist: s3fs>=2025.9.0; extra == 'all'
28
27
  Requires-Dist: xarray>=2025.1.1; extra == 'all'
@@ -37,7 +36,7 @@ Requires-Dist: odc-geo==0.4.7; extra == 'data'
37
36
  Requires-Dist: odc-stac>=0.4.0; extra == 'data'
38
37
  Requires-Dist: planetary-computer>=1.0.0; extra == 'data'
39
38
  Requires-Dist: pystac-client>=0.9.0; extra == 'data'
40
- Requires-Dist: rasterio<=1.3.10; extra == 'data'
39
+ Requires-Dist: rasterio>=1.3.11; extra == 'data'
41
40
  Requires-Dist: rioxarray>=0.18.2; extra == 'data'
42
41
  Requires-Dist: xarray>=2025.1.1; extra == 'data'
43
42
  Provides-Extra: storage
@@ -13,3 +13,4 @@ build:
13
13
 
14
14
  publish:
15
15
  uv publish --username "__token__" --password $(token)
16
+
@@ -1,12 +1,11 @@
1
1
  [project]
2
2
  name = "spai"
3
- version = "2026.7.7.dev2"
3
+ version = "2026.08.06.dev0"
4
4
  description = ""
5
5
  authors = [{name = "Jan Sensio", email = "it@earthpulse.es"}]
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.12"
8
8
  dependencies = [
9
- "bson>=0.5.10",
10
9
  "pandas>=2.0.2",
11
10
  "pillow>=10.2.0",
12
11
  "pydantic>=2.0.3",
@@ -21,7 +20,7 @@ dependencies = [
21
20
 
22
21
  [project.optional-dependencies]
23
22
  data = [
24
- "rasterio<=1.3.10",
23
+ "rasterio>=1.3.11",
25
24
  "geopandas>=1.1.1",
26
25
  "xarray>=2025.1.1",
27
26
  "rioxarray>=0.18.2",
@@ -49,7 +48,7 @@ all = [
49
48
  "pystac-client>=0.9.0",
50
49
  "planetary-computer>=1.0.0",
51
50
  "pyarrow>=19.0.0",
52
- "rasterio<=1.3.10",
51
+ "rasterio>=1.3.11",
53
52
  "s3fs>=2025.9.0",
54
53
  "rioxarray>=0.18.2",
55
54
  "xarray>=2025.1.1",
@@ -66,3 +65,8 @@ build-backend = "hatchling.build"
66
65
  [tool.hatch.build.targets.wheel]
67
66
  packages = ["spai"]
68
67
 
68
+ [tool.pytest.ini_options]
69
+ addopts = "--import-mode=importlib"
70
+ # Needed with importlib mode so `import spai` resolves without an editable install (e.g. CI).
71
+ pythonpath = ["."]
72
+
@@ -0,0 +1 @@
1
+ __version__ = "2026.08.06.dev0"
@@ -157,7 +157,6 @@ def clone(
157
157
  typer.echo(f"Error: {e}")
158
158
 
159
159
 
160
-
161
160
  @app.command(help="get owned project")
162
161
  def get_owned_project(
163
162
  project_name: str = typer.Argument(..., help="Project name"),
@@ -188,7 +188,7 @@ def download_era5_copernicus(
188
188
  end_date, aoi, end_month_days, variables
189
189
  )
190
190
  else:
191
- request = populate_reananalysis_request(end_date, aoi, end_month_days)
191
+ request = populate_reananalysis_request(end_date, aoi, end_month_days, variables)
192
192
  logger.debug(f"Request: {request}")
193
193
  zip_file = target_dir / "second_month.zip"
194
194
  client.retrieve(dataset, request).download(zip_file)
@@ -41,6 +41,7 @@ class S3StorageCredentials(BaseModel):
41
41
 
42
42
  class S3StorageConfig(StorageConfig):
43
43
  credentials: Optional[S3StorageCredentials] = None
44
+ managed: bool = False # whether the bucket is managed by SPAI or the user
44
45
 
45
46
  @field_validator("type")
46
47
  @classmethod
@@ -65,7 +65,7 @@ def deploy_folder(user, path, variables, typer, verbose=False):
65
65
 
66
66
  if data:
67
67
  if (
68
- os.getenv("SPAI_API_URL", "https://spai-test.earthpulse.ai/").strip("/")
68
+ os.getenv("SPAI_API_URL").strip("/")
69
69
  == "https://spai.staging.api.earthpulse.ai"
70
70
  ):
71
71
  typer.echo(
@@ -73,7 +73,7 @@ def deploy_folder(user, path, variables, typer, verbose=False):
73
73
  f"Check status at: https://spai.staging.earthpulse.ai/projects/{data['project_id']}"
74
74
  )
75
75
  elif any(
76
- host in (os.getenv("SPAI_API_URL", "https://spai-test.earthpulse.ai/"))
76
+ host in (os.getenv("SPAI_API_URL"))
77
77
  for host in ("localhost", "0.0.0.0")
78
78
  ):
79
79
  typer.echo("Deployed to development")
@@ -108,6 +108,7 @@ def run_local(dir, variables, typer):
108
108
  storage.credentials = create_or_retrieve_s3_bucket(
109
109
  config.project, storage.name
110
110
  )
111
+ storage.managed = True
111
112
  # set env variables
112
113
  prefix = "SPAI_STORAGE"
113
114
  names = []
@@ -128,6 +129,10 @@ def run_local(dir, variables, typer):
128
129
  )
129
130
  os.environ[f"{prefix}_S3_{name}_REGION"] = storage.credentials.region
130
131
  os.environ[f"{prefix}_S3_{name}_BUCKET"] = storage.credentials.bucket
132
+ if storage.managed:
133
+ os.environ[f"{prefix}_S3_{name}_MANAGED"] = "True"
134
+ else:
135
+ os.environ[f"{prefix}_S3_{name}_MANAGED"] = "False"
131
136
  os.environ[f"{prefix}_NAMES"] = ",".join(names)
132
137
  if config.scripts:
133
138
  typer.echo("Deploying scripts...")
@@ -233,3 +233,17 @@ class APIRepo:
233
233
  timeout=10,
234
234
  )
235
235
  return self.format_response(response)
236
+
237
+ def generate_presigned_url(
238
+ self, project_name, storage_name, object_name, workspace_name
239
+ ):
240
+ api_key = os.getenv("SPAI_API_KEY")
241
+ if not api_key:
242
+ raise Exception("SPAI_API_KEY is required to generate presigned URLs")
243
+ response = requests.get(
244
+ self.url
245
+ + f"data/{object_name}?project_name={project_name}&storage_name={storage_name}&workspace_name={workspace_name}",
246
+ headers={"X-API-Key": api_key},
247
+ timeout=10,
248
+ )
249
+ return self.format_response(response)
@@ -19,8 +19,9 @@ DRIVER_MAP = {"shp": "ESRI Shapefile", "gml": "GML"}
19
19
 
20
20
 
21
21
  class LocalStorage(BaseStorage):
22
- def __init__(self, path="data"):
22
+ def __init__(self, name, path="data"):
23
23
  super().__init__()
24
+ self.name = name
24
25
  self.path = path
25
26
  if not os.path.exists(self.path):
26
27
  os.makedirs(self.path)
@@ -235,6 +236,7 @@ class LocalStorage(BaseStorage):
235
236
  gdf = gpd.GeoDataFrame(df, crs="EPSG:4326", geometry=geom_column)
236
237
 
237
238
  return gdf
239
+ return df
238
240
 
239
241
  @with_geopandas
240
242
  def read_from_geo_file(self, gpd, name, driver):
@@ -13,26 +13,37 @@ from .decorators import (
13
13
  with_geopandas,
14
14
  with_rioxarray,
15
15
  with_pyarrow,
16
- with_s3fs,
17
16
  with_xarray,
18
17
  with_zarr,
19
18
  )
20
19
  from pathlib import Path
20
+ import requests
21
21
 
22
-
23
- # Ver funciones en CloudStorage si las de aquí no acaban de funcionar...
22
+ from ..repos import APIRepo
23
+ from spai.config import SPAIConfig
24
+ from .presigned_object_response import PresignedObjectResponse
25
+ from .s3_get_object_store import S3GetObjectStore
24
26
 
25
27
 
26
28
  class S3Storage(BaseStorage):
27
- def __init__(self, url, access, secret, bucket, region=None):
29
+ def __init__(self, name, url, access, secret, bucket, region=None, managed=False):
28
30
  super().__init__()
31
+ self.name = name
29
32
  self.url = url
30
33
  self.access = access
31
34
  self.secret = secret
32
35
  self.region = region
33
36
  self.bucket = bucket
34
-
35
- # print("holaaa", self.url, self.access, self.secret, self.region, self.bucket)
37
+ self.managed = managed
38
+
39
+ self.config = SPAIConfig()
40
+ self.workspace = self.config["workspace"]
41
+ self.api_repo = None
42
+ if managed:
43
+ print("This is a SPAI managed S3 storage")
44
+ self.api_repo = APIRepo()
45
+ else:
46
+ print("This is a user managed S3 storage")
36
47
 
37
48
  if self.access and self.secret:
38
49
  # Create a client
@@ -70,14 +81,32 @@ class S3Storage(BaseStorage):
70
81
  self.client.remove_object(self.bucket, name)
71
82
 
72
83
  def get_url(self, name):
73
- return self.client.presigned_get_object(self.bucket, name)
84
+ if self.managed:
85
+ return self.api_repo.generate_presigned_url(
86
+ self.config["project"],
87
+ self.name,
88
+ name,
89
+ self.workspace if self.workspace else "default",
90
+ )
91
+ else:
92
+ return self.client.presigned_get_object(self.bucket, name)
93
+
94
+ def get_object(self, name):
95
+ if self.managed:
96
+ url = self.get_url(name)
97
+ if isinstance(url, tuple):
98
+ url, error = url
99
+ if error:
100
+ raise RuntimeError(error)
101
+ response = requests.get(url, stream=True)
102
+ response.raise_for_status()
103
+ return PresignedObjectResponse(response)
104
+ return self.client.get_object(self.bucket, name)
74
105
 
75
106
  def exists(self, name):
76
107
  try:
77
108
  self.client.stat_object(self.bucket, name)
78
109
  return True
79
- # except error.S3Error: # this is not working in soil moisture template
80
- # return False
81
110
  except Exception:
82
111
  return False
83
112
 
@@ -325,10 +354,9 @@ class S3Storage(BaseStorage):
325
354
  parquet_name = name.rsplit(".", 1)[0] + ".parquet"
326
355
  return self.create_from_parquet(data, parquet_name)
327
356
 
328
- @with_s3fs
329
357
  @with_zarr
330
358
  @with_xarray
331
- def create_from_zarr(self, xr, s3fs, data, name):
359
+ def create_from_zarr(self, xr, data, name):
332
360
  # Create a temporary directory to store the zarr
333
361
  with tempfile.TemporaryDirectory() as temp_dir:
334
362
  # First save the zarr locally
@@ -364,7 +392,14 @@ class S3Storage(BaseStorage):
364
392
  return self.get_url(name)
365
393
 
366
394
  def read_object(self, name):
367
- return BytesIO(self.client.get_object(self.bucket, name).read())
395
+ response = None
396
+ try:
397
+ response = self.get_object(name)
398
+ return BytesIO(response.read())
399
+ finally:
400
+ if response:
401
+ response.close()
402
+ response.release_conn()
368
403
 
369
404
  def read_from_json(self, name):
370
405
  return pd.read_json(self.read_object(name))
@@ -376,7 +411,7 @@ class S3Storage(BaseStorage):
376
411
  def read_from_geojson(self, gpd, name):
377
412
  response = None
378
413
  try:
379
- response = self.client.get_object(self.bucket, name)
414
+ response = self.get_object(name)
380
415
  data = json.load(response)
381
416
  finally:
382
417
  if response:
@@ -394,8 +429,14 @@ class S3Storage(BaseStorage):
394
429
  @with_geopandas
395
430
  @with_pyarrow
396
431
  def read_from_parquet(self, pa, gpd, name):
397
- response = self.client.get_object(self.bucket, name)
398
- data = response.read()
432
+ response = None
433
+ try:
434
+ response = self.get_object(name)
435
+ data = response.read()
436
+ finally:
437
+ if response:
438
+ response.close()
439
+ response.release_conn()
399
440
  df = pd.read_parquet(BytesIO(data))
400
441
  if "geometry" in df.columns or "geom" in df.columns:
401
442
  from shapely import wkb
@@ -410,34 +451,26 @@ class S3Storage(BaseStorage):
410
451
 
411
452
  @with_xarray
412
453
  def read_from_netcdf(self, xr, name):
413
- return xr.open_dataset(self.read_object(name), decode_timedelta=True)
454
+ # netCDF backends generally need a real filesystem path, not a BytesIO
455
+ with tempfile.NamedTemporaryFile(suffix=".nc") as temp_file:
456
+ temp_file.write(self.read_object(name).read())
457
+ temp_file.flush()
458
+ with xr.open_dataset(temp_file.name, decode_timedelta=True) as ds:
459
+ return ds.load()
414
460
 
415
- @with_s3fs
416
461
  @with_zarr
417
462
  @with_xarray
418
- def read_from_zarr(self, xr, s3fs, name):
419
- # Construye el path al archivo Zarr en el bucket S3
420
- s3_path = f"s3://{self.bucket}/{name}"
421
-
422
- # Lee los datos desde Zarr utilizando xarray
423
- ds = xr.open_zarr(
424
- s3_path,
425
- storage_options={
426
- "key": self.access,
427
- "secret": self.secret,
428
- "client_kwargs": {"endpoint_url": f"https://{self.url}"},
429
- },
430
- decode_timedelta=True,
431
- )
432
-
433
- return ds
463
+ def read_from_zarr(self, xr, name):
464
+ # Zarr is a multi-object store; each key is fetched via get_object
465
+ # so managed (presigned) and unmanaged (MinIO) both work.
466
+ return xr.open_zarr(S3GetObjectStore(self, name), decode_timedelta=True)
434
467
 
435
468
  def read_file(self, name):
436
469
  # Using BytesIO to be equivalent to LocalStorage
437
- return io.BytesIO(self.client.get_object(self.bucket, name).read())
470
+ return self.read_object(name)
438
471
 
439
472
  async def data_stream(self, name):
440
- with self.client.get_object(self.bucket, name) as stream:
473
+ with self.get_object(name) as stream:
441
474
  for chunk in stream.stream(1024 * 1024): # Stream in chunks of 1MB
442
475
  yield chunk
443
476
 
@@ -27,13 +27,15 @@ class Storage(BaseStorage):
27
27
  if key.startswith(f"SPAI_STORAGE_{storage_name.upper()}")
28
28
  }
29
29
  envs = {key.split("_")[-1].lower(): value for key, value in envs.items()}
30
- if storage_name.split("_")[0] == "local":
30
+ storage_type = storage_name.split("_")[0]
31
+ storage_name = storage_name.split("_")[1]
32
+ if storage_type == "local":
31
33
  self.initialize_local(storage_name, envs)
32
- elif storage_name.split("_")[0] == "s3":
34
+ elif storage_type == "s3":
33
35
  self.initialize_s3(storage_name, envs)
34
36
 
35
37
  def initialize_local(self, name, envs):
36
- self.storages[f"{name.split('_')[1]}"] = LocalStorage(**envs)
38
+ self.storages[name] = LocalStorage(**envs, name=name)
37
39
 
38
40
  def initialize_s3(self, name, envs):
39
- self.storages[f"{name.split('_')[1]}"] = S3Storage(**envs)
41
+ self.storages[name] = S3Storage(**envs, name=name)
@@ -0,0 +1,28 @@
1
+ import requests
2
+
3
+
4
+ class PresignedObjectResponse:
5
+ """MinIO-compatible wrapper for object data fetched via presigned URL."""
6
+
7
+ def __init__(self, http_response: requests.Response):
8
+ self._http_response = http_response
9
+ self._raw = http_response.raw
10
+
11
+ def read(self, amt=None):
12
+ return self._raw.read(amt)
13
+
14
+ def stream(self, amt=32 * 1024):
15
+ yield from self._raw.stream(amt)
16
+
17
+ def close(self):
18
+ self._raw.close()
19
+
20
+ def release_conn(self):
21
+ self._raw.release_conn()
22
+
23
+ def __enter__(self):
24
+ self._raw.__enter__()
25
+ return self
26
+
27
+ def __exit__(self, *args):
28
+ return self._raw.__exit__(*args)
@@ -0,0 +1,44 @@
1
+ from collections.abc import MutableMapping
2
+
3
+
4
+ class S3GetObjectStore(MutableMapping):
5
+ """Read-only zarr store that fetches keys via S3Storage.get_object."""
6
+
7
+ def __init__(self, storage, root):
8
+ self.storage = storage
9
+ self.root = root.rstrip("/")
10
+
11
+ def _path(self, key):
12
+ key = str(key).lstrip("/")
13
+ return f"{self.root}/{key}" if key else self.root
14
+
15
+ def __getitem__(self, key):
16
+ response = None
17
+ try:
18
+ response = self.storage.get_object(self._path(key))
19
+ return response.read()
20
+ except Exception as exc:
21
+ raise KeyError(key) from exc
22
+ finally:
23
+ if response:
24
+ response.close()
25
+ response.release_conn()
26
+
27
+ def __setitem__(self, key, value):
28
+ raise PermissionError("S3GetObjectStore is read-only")
29
+
30
+ def __delitem__(self, key):
31
+ raise PermissionError("S3GetObjectStore is read-only")
32
+
33
+ def __contains__(self, key):
34
+ return self.storage.exists(self._path(key))
35
+
36
+ def __iter__(self):
37
+ prefix = f"{self.root}/"
38
+ for obj in self.storage.client.list_objects(
39
+ self.storage.bucket, prefix=prefix, recursive=True
40
+ ):
41
+ yield obj.object_name[len(prefix) :]
42
+
43
+ def __len__(self):
44
+ return sum(1 for _ in self)
@@ -2,7 +2,7 @@ import json
2
2
  import pathlib
3
3
  import pytest
4
4
 
5
- from spai.spai.config.vars import SPAIVars
5
+ from spai.config.vars import SPAIVars
6
6
 
7
7
  @pytest.fixture(autouse=False)
8
8
  def temp_vars_file(request):
@@ -1,7 +1,7 @@
1
1
  from httpx import Response
2
2
  import respx
3
3
 
4
- from spai.spai.task_logging.task_logger import TaskLogger, TaskCreate
4
+ from spai.task_logging.task_logger import TaskLogger, TaskCreate
5
5
 
6
6
 
7
7
  def test_task_logger_init():
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  from unittest.mock import patch
3
3
 
4
- from spai.spai.processing import autocategorize1D
4
+ from spai.processing import autocategorize1D
5
5
 
6
6
 
7
7
  @patch('numpy.argmin', side_effect=lambda arr, axis: np.zeros(arr.shape[0], dtype=int))
@@ -2,7 +2,7 @@ from unittest import mock
2
2
  from unittest.mock import patch
3
3
  import pytest
4
4
 
5
- from spai.spai.processing.mask_raster import mask_raster
5
+ from spai.processing.mask_raster import mask_raster
6
6
 
7
7
  # Test mask_raster function
8
8
  @patch('rasterio.mask.mask')
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  import pytest
3
3
 
4
- from spai.spai.processing import normalised_difference
4
+ from spai.processing import normalised_difference
5
5
 
6
6
  # Test cases
7
7
 
@@ -1,6 +1,6 @@
1
1
  import numpy as np
2
2
 
3
- from spai.spai.processing.px_count import px_count
3
+ from spai.processing.px_count import px_count
4
4
 
5
5
 
6
6
  # Test px_count function
@@ -1,7 +1,7 @@
1
1
  from unittest import mock
2
2
  import pytest
3
3
 
4
- from spai.spai.processing.read_raster import read_raster
4
+ from spai.processing.read_raster import read_raster
5
5
 
6
6
  def test_read_raster_from_rasterio():
7
7
  storage = mock.Mock()
@@ -1,6 +1,6 @@
1
1
  from unittest import mock
2
2
  import pandas as pd
3
- from spai.spai.processing.save_table import save_table
3
+ from spai.processing.save_table import save_table
4
4
 
5
5
  # Test save_table function
6
6
  def test_save_table():
@@ -0,0 +1 @@
1
+ ../../spai