spai 2026.7.7.dev2__tar.gz → 2026.9.8.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.9.8.dev0}/.gitignore +1 -0
  2. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/PKG-INFO +3 -4
  3. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/makefile +1 -0
  4. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/pyproject.toml +8 -4
  5. spai-2026.9.8.dev0/spai/__init__.py +1 -0
  6. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/cli.py +2 -1
  7. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/copernicus/download_copernicus.py +1 -1
  8. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/models/StorageConfig.py +2 -3
  9. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/deploy_folder.py +2 -3
  10. {spai-2026.7.7.dev2/spai/data/sources → spai-2026.9.8.dev0/spai/project/project_template}/__init__.py +0 -0
  11. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/run_local.py +9 -1
  12. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/repos/APIRepo.py +14 -0
  13. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/LocalStorage.py +3 -1
  14. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/S3Storage.py +69 -35
  15. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/Storage.py +6 -4
  16. spai-2026.9.8.dev0/spai/storage/presigned_object_response.py +28 -0
  17. spai-2026.9.8.dev0/spai/storage/s3_get_object_store.py +44 -0
  18. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/config}/__init__.py +0 -0
  19. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/tests/config/test_vars.py +1 -1
  20. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/task_logging/test_task_logger.py +1 -1
  21. {spai-2026.7.7.dev2/tests/config → spai-2026.9.8.dev0/tests/deprecated/test_processing}/__init__.py +0 -0
  22. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_processing/test_autocategorize1D.py +1 -1
  23. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_processing/test_mask_raster.py +1 -1
  24. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_processing/test_normalised_difference.py +1 -1
  25. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_processing/test_px_count.py +1 -1
  26. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_processing/test_read_raster.py +1 -1
  27. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_processing/test_save_table.py +1 -1
  28. spai-2026.9.8.dev0/tests/deprecated/test_pulses/spai +1 -0
  29. spai-2026.9.8.dev0/tests/unit/storage/conftest.py +127 -0
  30. spai-2026.9.8.dev0/tests/unit/storage/factories.py +354 -0
  31. spai-2026.9.8.dev0/tests/unit/storage/output/array.npy +0 -0
  32. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.nc +0 -0
  33. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/.zattrs +1 -0
  34. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/.zgroup +3 -0
  35. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/.zmetadata +87 -0
  36. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/temp/.zarray +22 -0
  37. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/temp/.zattrs +6 -0
  38. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/temp/0.0 +0 -0
  39. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/x/.zarray +20 -0
  40. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/x/.zattrs +5 -0
  41. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/x/0 +0 -0
  42. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/y/.zarray +20 -0
  43. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/y/.zattrs +5 -0
  44. spai-2026.9.8.dev0/tests/unit/storage/output/dataset.zarr/y/0 +0 -0
  45. spai-2026.9.8.dev0/tests/unit/storage/output/dict.geojson +1 -0
  46. spai-2026.9.8.dev0/tests/unit/storage/output/dict.json +1 -0
  47. spai-2026.9.8.dev0/tests/unit/storage/output/from_path.txt +1 -0
  48. spai-2026.9.8.dev0/tests/unit/storage/output/hello.txt +1 -0
  49. spai-2026.9.8.dev0/tests/unit/storage/output/image.png +0 -0
  50. spai-2026.9.8.dev0/tests/unit/storage/output/table.csv +3 -0
  51. spai-2026.9.8.dev0/tests/unit/storage/output/table.json +1 -0
  52. spai-2026.9.8.dev0/tests/unit/storage/output/table.parquet +0 -0
  53. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.cpg +1 -0
  54. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.dbf +0 -0
  55. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.geojson +9 -0
  56. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.gml +24 -0
  57. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.parquet +0 -0
  58. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.prj +1 -0
  59. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.shp +0 -0
  60. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.shx +0 -0
  61. spai-2026.9.8.dev0/tests/unit/storage/output/vectors.xsd +53 -0
  62. spai-2026.9.8.dev0/tests/unit/storage/test_local_storage.py +351 -0
  63. spai-2026.9.8.dev0/tests/unit/storage/test_s3_storage.py +444 -0
  64. spai-2026.9.8.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.9.8.dev0}/README.md +0 -0
  69. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/00_setup.ipynb +0 -0
  70. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/00bis_download_satellite_imagery.ipynb +0 -0
  71. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/01_satellite_data_access.ipynb +0 -0
  72. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/02_geospatial_data_access.ipynb +0 -0
  73. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/03_storage.ipynb +0 -0
  74. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/04_infra_management.ipynb +0 -0
  75. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/05_mails.ipynb +0 -0
  76. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/06_cogs.ipynb +0 -0
  77. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/99_new_data_access.ipynb +0 -0
  78. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/03_xyz_api.ipynb +0 -0
  79. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/03_xyz_api.py +0 -0
  80. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/05_nominatim_example.ipynb +0 -0
  81. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/06_colorize.ipynb +0 -0
  82. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/10_select_one_gdf_from_map.ipynb +0 -0
  83. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/11_forest_monitoring_flow.ipynb +0 -0
  84. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/12_forest_monitoring.ipynb +0 -0
  85. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/13_ms_buildings_download.ipynb +0 -0
  86. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/13_water_quality_flow.ipynb +0 -0
  87. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/14_download_dem.ipynb +0 -0
  88. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/14_water_quality.ipynb +0 -0
  89. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/development/15_perform_analysis.ipynb +0 -0
  90. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/legacy/00_download_satellite_imagery.ipynb +0 -0
  91. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/legacy/01_process_satellite_imagery.ipynb +0 -0
  92. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/legacy/02_analytics.ipynb +0 -0
  93. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/legacy/04_download_satellite_imagery_advanced.ipynb +0 -0
  94. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/legacy/21_storage.ipynb +0 -0
  95. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/llanca 2.geojson +0 -0
  96. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/llanca.geojson +0 -0
  97. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/map.geojson +0 -0
  98. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/sample.pdf +0 -0
  99. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/examples/samples/AOI_Vegetation_Growth.json +0 -0
  100. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/main.py +0 -0
  101. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/analytics/__init__.py +0 -0
  102. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/analytics/forest_monitoring.py +0 -0
  103. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/analytics/utils.py +0 -0
  104. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/analytics/water_quality.py +0 -0
  105. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/auth/__init__.py +0 -0
  106. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/auth/auth.py +0 -0
  107. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/auth/is_logged.py +0 -0
  108. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/auth/logout.py +0 -0
  109. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/auth/retrieve_credentials.py +0 -0
  110. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0/spai/commands}/__init__.py +0 -0
  111. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/commands/auth.py +0 -0
  112. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/commands/delete.py +0 -0
  113. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/commands/list.py +0 -0
  114. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/commands/stop.py +0 -0
  115. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/commands/workspace.py +0 -0
  116. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/config/__init__.py +0 -0
  117. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/config/config.py +0 -0
  118. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/config/validate.py +0 -0
  119. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/config/vars.py +0 -0
  120. {spai-2026.7.7.dev2/spai/commands → spai-2026.9.8.dev0/spai/data}/__init__.py +0 -0
  121. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/README.md +0 -0
  122. {spai-2026.7.7.dev2/spai/data → spai-2026.9.8.dev0/spai/data/climate}/__init__.py +0 -0
  123. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/area_of_interest.py +0 -0
  124. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/earthdatahub/download_earthdatahub.py +0 -0
  125. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/ecmwf/download_ecmwf.py +0 -0
  126. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/era5.py +0 -0
  127. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/climate/era5_vars.py +0 -0
  128. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/ecosystems/__init__.py +0 -0
  129. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/ecosystems/protected_areas.py +0 -0
  130. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/fire/__init__.py +0 -0
  131. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/fire/fwi/__init__.py +0 -0
  132. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/fire/fwi/fwi_nasa.py +0 -0
  133. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/hidrology/__init__.py +0 -0
  134. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/hidrology/waterbodies.py +0 -0
  135. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/hidrology/waterways.py +0 -0
  136. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/__init__.py +0 -0
  137. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/download.py +0 -0
  138. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/explore.py +0 -0
  139. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/load.py +0 -0
  140. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PCH4Downloader.py +0 -0
  141. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PCODownloader.py +0 -0
  142. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PDownloader.py +0 -0
  143. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PHCHODownloader.py +0 -0
  144. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PNO2Downloader.py +0 -0
  145. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PO3Downloader.py +0 -0
  146. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PSO2Downloader.py +0 -0
  147. {spai-2026.7.7.dev2/spai/data/climate → spai-2026.9.8.dev0/spai/data/satellite/sentinelhub/S5P}/__init__.py +0 -0
  148. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHCloudMaskDownloader.py +0 -0
  149. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHDEM30Downloader.py +0 -0
  150. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHDownloader.py +0 -0
  151. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHExplorer.py +0 -0
  152. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHS1Downloader.py +0 -0
  153. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHS2Downloader.py +0 -0
  154. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHS2L1CDownloader.py +0 -0
  155. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHS2L2ADownloader.py +0 -0
  156. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHS3OLCIDownloader.py +0 -0
  157. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/SHS3SLSTRDownloader.py +0 -0
  158. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/sentinelhub/__init__.py +0 -0
  159. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/STACDownloader.py +0 -0
  160. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/__init__.py +0 -0
  161. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/aws/AWSDEM30Downloader.py +0 -0
  162. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/aws/AWSDEM90Downloader.py +0 -0
  163. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/aws/AWSDEMDownloader.py +0 -0
  164. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/aws/AWSDownloader.py +0 -0
  165. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/aws/AWSS2L2ADownloader.py +0 -0
  166. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/aws/__init__.py +0 -0
  167. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/decorators.py +0 -0
  168. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCAlosPalsarAnnualDownloader.py +0 -0
  169. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCDownloader.py +0 -0
  170. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCESAWCDownloader.py +0 -0
  171. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCLandsat8Downloader.py +0 -0
  172. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCModisBurnedDownloader.py +0 -0
  173. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCModisSnow8Downloader.py +0 -0
  174. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCModisSnowDayDownloader.py +0 -0
  175. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCS1GRDDownloader.py +0 -0
  176. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/PCS1RTCDownloader.py +0 -0
  177. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/stac/pc/__init__.py +0 -0
  178. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/satellite/utils.py +0 -0
  179. {spai-2026.7.7.dev2/spai/data/satellite/sentinelhub/S5P → spai-2026.9.8.dev0/spai/data/sources}/__init__.py +0 -0
  180. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/sources/osm.py +0 -0
  181. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/utilities/__init__.py +0 -0
  182. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/utilities/buildings.py +0 -0
  183. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/utilities/pipelines.py +0 -0
  184. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/utilities/power.py +0 -0
  185. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/utilities/railroads.py +0 -0
  186. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/data/utilities/roads.py +0 -0
  187. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/errors/__init__.py +0 -0
  188. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/errors/auth.py +0 -0
  189. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/__init__.py +0 -0
  190. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/utils.py +0 -0
  191. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/xyz/__init__.py +0 -0
  192. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/xyz/cache.py +0 -0
  193. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/xyz/errors.py +0 -0
  194. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/xyz/get_image_tile.py +0 -0
  195. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/image/xyz/image_utils.py +0 -0
  196. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/logging/log.py +0 -0
  197. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/models/Config.py +0 -0
  198. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/models/__init__.py +0 -0
  199. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/__init__.py +0 -0
  200. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/autocategorize1D.py +0 -0
  201. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/colorize_raster.py +0 -0
  202. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/convert_array_to_vector.py +0 -0
  203. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/mask_raster.py +0 -0
  204. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/normalised_difference.py +0 -0
  205. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/px_count.py +0 -0
  206. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/rasterio_mask.py +0 -0
  207. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/read_raster.py +0 -0
  208. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/save_table.py +0 -0
  209. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/processing/utils.py +0 -0
  210. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/__init__.py +0 -0
  211. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/delete_project.py +0 -0
  212. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/deploy_template.py +0 -0
  213. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/download_template.py +0 -0
  214. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/get_logs.py +0 -0
  215. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/get_project_by_name.py +0 -0
  216. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/get_projects.py +0 -0
  217. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/get_service_by_name_type_project.py +0 -0
  218. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/get_services.py +0 -0
  219. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/init_project.py +0 -0
  220. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/install_requirements.py +0 -0
  221. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/README.md +0 -0
  222. {spai-2026.7.7.dev2/spai/project/project_template → spai-2026.9.8.dev0/spai/project/project_template/apis}/__init__.py +0 -0
  223. {spai-2026.7.7.dev2/spai/project/project_template/apis → spai-2026.9.8.dev0/spai/project/project_template/apis/analytics}/__init__.py +0 -0
  224. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/apis/analytics/main.py +0 -0
  225. {spai-2026.7.7.dev2 → spai-2026.9.8.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.9.8.dev0/spai/project/project_template/apis/xyz}/__init__.py +0 -0
  227. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/apis/xyz/main.py +0 -0
  228. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/apis/xyz/requirements.txt +0 -0
  229. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/notebooks/analytics/main.ipynb +0 -0
  230. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/notebooks/analytics/requirements.txt +0 -0
  231. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/scripts/analytics/main.py +0 -0
  232. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/scripts/analytics/requirements.txt +0 -0
  233. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/scripts/downloader/main.py +0 -0
  234. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/scripts/downloader/requirements.txt +0 -0
  235. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/spai.config.local.yaml +0 -0
  236. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/spai.config.yaml +0 -0
  237. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/spai.vars.json +0 -0
  238. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/uis/map/main.py +0 -0
  239. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/project_template/uis/map/requirements.txt +0 -0
  240. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/project/stop_service.py +0 -0
  241. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/repos/AuthRepo.py +0 -0
  242. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/repos/__init__.py +0 -0
  243. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/BaseStorage.py +0 -0
  244. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/CloudStorage.py +0 -0
  245. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/__init__.py +0 -0
  246. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/create_s3.py +0 -0
  247. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/decorators.py +0 -0
  248. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/storage/minio.py +0 -0
  249. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/task_logging/task_logger.py +0 -0
  250. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/add_user_to_workspace.py +0 -0
  251. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/change_user_role.py +0 -0
  252. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/create_workspace.py +0 -0
  253. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/get_workspaces.py +0 -0
  254. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/remove_user_from_workspace.py +0 -0
  255. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/remove_workspace.py +0 -0
  256. {spai-2026.7.7.dev2 → spai-2026.9.8.dev0}/spai/workspaces/update_workspace.py +0 -0
  257. {spai-2026.7.7.dev2/spai/project/project_template/apis/xyz → spai-2026.9.8.dev0/tests}/__init__.py +0 -0
  258. {spai-2026.7.7.dev2/tests/test_processing → spai-2026.9.8.dev0/tests/deprecated/test_pulses}/__init__.py +0 -0
  259. {spai-2026.7.7.dev2/tests → spai-2026.9.8.dev0/tests/deprecated}/test_pulses/results_forest_monitoring.ipynb +0 -0
  260. {spai-2026.7.7.dev2/tests → spai-2026.9.8.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.9.8.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.09.08.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.09.08.dev0"
@@ -1,6 +1,7 @@
1
1
  import typer
2
2
  from pathlib import Path
3
3
  from typing import List
4
+ import traceback
4
5
 
5
6
 
6
7
  # Add the cli directory to the Python path
@@ -93,6 +94,7 @@ def run(
93
94
  install_requirements(path, typer, False)
94
95
  return run_local(path, variables, typer)
95
96
  except Exception as e:
97
+ traceback.print_exc()
96
98
  typer.echo(f"Error: {e}")
97
99
 
98
100
 
@@ -157,7 +159,6 @@ def clone(
157
159
  typer.echo(f"Error: {e}")
158
160
 
159
161
 
160
-
161
162
  @app.command(help="get owned project")
162
163
  def get_owned_project(
163
164
  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)
@@ -34,13 +34,12 @@ class S3StorageCredentials(BaseModel):
34
34
  access_key: str
35
35
  secret_key: str
36
36
  bucket: str
37
- region: Optional[str] = (
38
- None # en local no hace falta, además afecta a la conexión segura o no
39
- )
37
+ region: str | None = None
40
38
 
41
39
 
42
40
  class S3StorageConfig(StorageConfig):
43
41
  credentials: Optional[S3StorageCredentials] = None
42
+ managed: bool = False # whether the bucket is managed by SPAI or the user
44
43
 
45
44
  @field_validator("type")
46
45
  @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,8 +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/"))
77
- for host in ("localhost", "0.0.0.0")
76
+ host in (os.getenv("SPAI_API_URL")) for host in ("localhost", "0.0.0.0")
78
77
  ):
79
78
  typer.echo("Deployed to development")
80
79
  else:
@@ -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 = []
@@ -126,8 +127,15 @@ def run_local(dir, variables, typer):
126
127
  os.environ[f"{prefix}_S3_{name}_SECRET"] = (
127
128
  storage.credentials.secret_key
128
129
  )
129
- os.environ[f"{prefix}_S3_{name}_REGION"] = storage.credentials.region
130
+ if storage.credentials.region:
131
+ os.environ[f"{prefix}_S3_{name}_REGION"] = (
132
+ storage.credentials.region
133
+ )
130
134
  os.environ[f"{prefix}_S3_{name}_BUCKET"] = storage.credentials.bucket
135
+ if storage.managed:
136
+ os.environ[f"{prefix}_S3_{name}_MANAGED"] = "True"
137
+ else:
138
+ os.environ[f"{prefix}_S3_{name}_MANAGED"] = "False"
131
139
  os.environ[f"{prefix}_NAMES"] = ",".join(names)
132
140
  if config.scripts:
133
141
  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,38 @@ 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 if managed is "True" else False
38
+ # self.managed = False # TODO: remove this once we have a way to manage S3 storages in the cloud
39
+
40
+ self.config = SPAIConfig()
41
+ self.workspace = self.config["workspace"]
42
+ self.api_repo = None
43
+ if managed:
44
+ print("This is a SPAI managed S3 storage")
45
+ self.api_repo = APIRepo()
46
+ else:
47
+ print("This is a user managed S3 storage")
36
48
 
37
49
  if self.access and self.secret:
38
50
  # Create a client
@@ -70,14 +82,32 @@ class S3Storage(BaseStorage):
70
82
  self.client.remove_object(self.bucket, name)
71
83
 
72
84
  def get_url(self, name):
73
- return self.client.presigned_get_object(self.bucket, name)
85
+ if self.managed:
86
+ return self.api_repo.generate_presigned_url(
87
+ self.config["project"],
88
+ self.name,
89
+ name,
90
+ self.workspace if self.workspace else "default",
91
+ )
92
+ else:
93
+ return self.client.presigned_get_object(self.bucket, name)
94
+
95
+ def get_object(self, name):
96
+ if self.managed:
97
+ url = self.get_url(name)
98
+ if isinstance(url, tuple):
99
+ url, error = url
100
+ if error:
101
+ raise RuntimeError(error)
102
+ response = requests.get(url, stream=True)
103
+ response.raise_for_status()
104
+ return PresignedObjectResponse(response)
105
+ return self.client.get_object(self.bucket, name)
74
106
 
75
107
  def exists(self, name):
76
108
  try:
77
109
  self.client.stat_object(self.bucket, name)
78
110
  return True
79
- # except error.S3Error: # this is not working in soil moisture template
80
- # return False
81
111
  except Exception:
82
112
  return False
83
113
 
@@ -325,10 +355,9 @@ class S3Storage(BaseStorage):
325
355
  parquet_name = name.rsplit(".", 1)[0] + ".parquet"
326
356
  return self.create_from_parquet(data, parquet_name)
327
357
 
328
- @with_s3fs
329
358
  @with_zarr
330
359
  @with_xarray
331
- def create_from_zarr(self, xr, s3fs, data, name):
360
+ def create_from_zarr(self, xr, data, name):
332
361
  # Create a temporary directory to store the zarr
333
362
  with tempfile.TemporaryDirectory() as temp_dir:
334
363
  # First save the zarr locally
@@ -364,7 +393,14 @@ class S3Storage(BaseStorage):
364
393
  return self.get_url(name)
365
394
 
366
395
  def read_object(self, name):
367
- return BytesIO(self.client.get_object(self.bucket, name).read())
396
+ response = None
397
+ try:
398
+ response = self.get_object(name)
399
+ return BytesIO(response.read())
400
+ finally:
401
+ if response:
402
+ response.close()
403
+ response.release_conn()
368
404
 
369
405
  def read_from_json(self, name):
370
406
  return pd.read_json(self.read_object(name))
@@ -376,7 +412,7 @@ class S3Storage(BaseStorage):
376
412
  def read_from_geojson(self, gpd, name):
377
413
  response = None
378
414
  try:
379
- response = self.client.get_object(self.bucket, name)
415
+ response = self.get_object(name)
380
416
  data = json.load(response)
381
417
  finally:
382
418
  if response:
@@ -394,8 +430,14 @@ class S3Storage(BaseStorage):
394
430
  @with_geopandas
395
431
  @with_pyarrow
396
432
  def read_from_parquet(self, pa, gpd, name):
397
- response = self.client.get_object(self.bucket, name)
398
- data = response.read()
433
+ response = None
434
+ try:
435
+ response = self.get_object(name)
436
+ data = response.read()
437
+ finally:
438
+ if response:
439
+ response.close()
440
+ response.release_conn()
399
441
  df = pd.read_parquet(BytesIO(data))
400
442
  if "geometry" in df.columns or "geom" in df.columns:
401
443
  from shapely import wkb
@@ -410,34 +452,26 @@ class S3Storage(BaseStorage):
410
452
 
411
453
  @with_xarray
412
454
  def read_from_netcdf(self, xr, name):
413
- return xr.open_dataset(self.read_object(name), decode_timedelta=True)
455
+ # netCDF backends generally need a real filesystem path, not a BytesIO
456
+ with tempfile.NamedTemporaryFile(suffix=".nc") as temp_file:
457
+ temp_file.write(self.read_object(name).read())
458
+ temp_file.flush()
459
+ with xr.open_dataset(temp_file.name, decode_timedelta=True) as ds:
460
+ return ds.load()
414
461
 
415
- @with_s3fs
416
462
  @with_zarr
417
463
  @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
464
+ def read_from_zarr(self, xr, name):
465
+ # Zarr is a multi-object store; each key is fetched via get_object
466
+ # so managed (presigned) and unmanaged (MinIO) both work.
467
+ return xr.open_zarr(S3GetObjectStore(self, name), decode_timedelta=True)
434
468
 
435
469
  def read_file(self, name):
436
470
  # Using BytesIO to be equivalent to LocalStorage
437
- return io.BytesIO(self.client.get_object(self.bucket, name).read())
471
+ return self.read_object(name)
438
472
 
439
473
  async def data_stream(self, name):
440
- with self.client.get_object(self.bucket, name) as stream:
474
+ with self.get_object(name) as stream:
441
475
  for chunk in stream.stream(1024 * 1024): # Stream in chunks of 1MB
442
476
  yield chunk
443
477
 
@@ -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