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.
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/.gitignore +1 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/PKG-INFO +3 -4
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/makefile +1 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/pyproject.toml +8 -4
- spai-2026.8.6.dev0/spai/__init__.py +1 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/cli.py +0 -1
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/copernicus/download_copernicus.py +1 -1
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/models/StorageConfig.py +1 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/deploy_folder.py +2 -2
- {spai-2026.7.7.dev2/spai/data/sources → spai-2026.8.6.dev0/spai/project/project_template}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/run_local.py +5 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/repos/APIRepo.py +14 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/LocalStorage.py +3 -1
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/S3Storage.py +68 -35
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/Storage.py +6 -4
- spai-2026.8.6.dev0/spai/storage/presigned_object_response.py +28 -0
- spai-2026.8.6.dev0/spai/storage/s3_get_object_store.py +44 -0
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/config}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/tests/config/test_vars.py +1 -1
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/task_logging/test_task_logger.py +1 -1
- {spai-2026.7.7.dev2/tests/config → spai-2026.8.6.dev0/tests/deprecated/test_processing}/__init__.py +0 -0
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_autocategorize1D.py +1 -1
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_mask_raster.py +1 -1
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_normalised_difference.py +1 -1
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_px_count.py +1 -1
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_read_raster.py +1 -1
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_processing/test_save_table.py +1 -1
- spai-2026.8.6.dev0/tests/deprecated/test_pulses/spai +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/conftest.py +127 -0
- spai-2026.8.6.dev0/tests/unit/storage/factories.py +354 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/array.npy +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.nc +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/.zattrs +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/.zgroup +3 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/.zmetadata +87 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/temp/.zarray +22 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/temp/.zattrs +6 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/temp/0.0 +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/x/.zarray +20 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/x/.zattrs +5 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/x/0 +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/y/.zarray +20 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/y/.zattrs +5 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dataset.zarr/y/0 +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dict.geojson +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/dict.json +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/from_path.txt +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/hello.txt +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/image.png +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/table.csv +3 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/table.json +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/table.parquet +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.cpg +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.dbf +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.geojson +9 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.gml +24 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.parquet +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.prj +1 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.shp +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.shx +0 -0
- spai-2026.8.6.dev0/tests/unit/storage/output/vectors.xsd +53 -0
- spai-2026.8.6.dev0/tests/unit/storage/test_local_storage.py +351 -0
- spai-2026.8.6.dev0/tests/unit/storage/test_s3_storage.py +444 -0
- spai-2026.8.6.dev0/tests/unit/storage/test_s3_storage_managed.py +183 -0
- spai-2026.7.7.dev2/examples/test_era5.ipynb +0 -37
- spai-2026.7.7.dev2/spai/__init__.py +0 -1
- spai-2026.7.7.dev2/tests/test_pulses/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/README.md +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/00_setup.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/00bis_download_satellite_imagery.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/01_satellite_data_access.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/02_geospatial_data_access.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/03_storage.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/04_infra_management.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/05_mails.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/06_cogs.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/99_new_data_access.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/03_xyz_api.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/03_xyz_api.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/05_nominatim_example.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/06_colorize.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/10_select_one_gdf_from_map.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/11_forest_monitoring_flow.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/12_forest_monitoring.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/13_ms_buildings_download.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/13_water_quality_flow.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/14_download_dem.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/14_water_quality.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/development/15_perform_analysis.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/00_download_satellite_imagery.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/01_process_satellite_imagery.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/02_analytics.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/04_download_satellite_imagery_advanced.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/legacy/21_storage.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/llanca 2.geojson +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/llanca.geojson +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/map.geojson +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/sample.pdf +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/examples/samples/AOI_Vegetation_Growth.json +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/main.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/forest_monitoring.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/utils.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/analytics/water_quality.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/auth.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/is_logged.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/logout.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/auth/retrieve_credentials.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0/spai/commands}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/auth.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/delete.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/list.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/stop.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/commands/workspace.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/config.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/validate.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/config/vars.py +0 -0
- {spai-2026.7.7.dev2/spai/commands → spai-2026.8.6.dev0/spai/data}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/README.md +0 -0
- {spai-2026.7.7.dev2/spai/data → spai-2026.8.6.dev0/spai/data/climate}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/area_of_interest.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/earthdatahub/download_earthdatahub.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/ecmwf/download_ecmwf.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/era5.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/era5_vars.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/ecosystems/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/ecosystems/protected_areas.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/fire/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/fire/fwi/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/fire/fwi/fwi_nasa.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/hidrology/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/hidrology/waterbodies.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/hidrology/waterways.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/download.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/explore.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/load.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PCH4Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PCODownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PHCHODownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PNO2Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PO3Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/S5P/SHS5PSO2Downloader.py +0 -0
- {spai-2026.7.7.dev2/spai/data/climate → spai-2026.8.6.dev0/spai/data/satellite/sentinelhub/S5P}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHCloudMaskDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHDEM30Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHExplorer.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS1Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS2Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS2L1CDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS2L2ADownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS3OLCIDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/SHS3SLSTRDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/sentinelhub/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/STACDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDEM30Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDEM90Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDEMDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/AWSS2L2ADownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/aws/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/decorators.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCAlosPalsarAnnualDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCESAWCDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCLandsat8Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCModisBurnedDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCModisSnow8Downloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCModisSnowDayDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCS1GRDDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/PCS1RTCDownloader.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/stac/pc/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/satellite/utils.py +0 -0
- {spai-2026.7.7.dev2/spai/data/satellite/sentinelhub/S5P → spai-2026.8.6.dev0/spai/data/sources}/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/sources/osm.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/buildings.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/pipelines.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/power.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/railroads.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/utilities/roads.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/errors/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/errors/auth.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/utils.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/cache.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/errors.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/get_image_tile.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/image/xyz/image_utils.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/logging/log.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/models/Config.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/models/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/autocategorize1D.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/colorize_raster.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/convert_array_to_vector.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/mask_raster.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/normalised_difference.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/px_count.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/rasterio_mask.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/read_raster.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/save_table.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/processing/utils.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/delete_project.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/deploy_template.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/download_template.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_logs.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_project_by_name.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_projects.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_service_by_name_type_project.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/get_services.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/init_project.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/install_requirements.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/README.md +0 -0
- {spai-2026.7.7.dev2/spai/project/project_template → spai-2026.8.6.dev0/spai/project/project_template/apis}/__init__.py +0 -0
- {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
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/analytics/main.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/analytics/requirements.txt +0 -0
- {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
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/xyz/main.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/apis/xyz/requirements.txt +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/notebooks/analytics/main.ipynb +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/notebooks/analytics/requirements.txt +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/analytics/main.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/analytics/requirements.txt +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/downloader/main.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/scripts/downloader/requirements.txt +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/spai.config.local.yaml +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/spai.config.yaml +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/spai.vars.json +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/uis/map/main.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/project_template/uis/map/requirements.txt +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/project/stop_service.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/repos/AuthRepo.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/repos/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/BaseStorage.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/CloudStorage.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/__init__.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/create_s3.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/decorators.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/storage/minio.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/task_logging/task_logger.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/add_user_to_workspace.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/change_user_role.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/create_workspace.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/get_workspaces.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/remove_user_from_workspace.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/remove_workspace.py +0 -0
- {spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/workspaces/update_workspace.py +0 -0
- {spai-2026.7.7.dev2/spai/project/project_template/apis/xyz → spai-2026.8.6.dev0/tests}/__init__.py +0 -0
- {spai-2026.7.7.dev2/tests/test_processing → spai-2026.8.6.dev0/tests/deprecated/test_pulses}/__init__.py +0 -0
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_pulses/results_forest_monitoring.ipynb +0 -0
- {spai-2026.7.7.dev2/tests → spai-2026.8.6.dev0/tests/deprecated}/test_pulses/results_water_quality.ipynb +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spai
|
|
3
|
-
Version: 2026.
|
|
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
|
|
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
|
|
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
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spai"
|
|
3
|
-
version = "2026.
|
|
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
|
|
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
|
|
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"
|
{spai-2026.7.7.dev2 → spai-2026.8.6.dev0}/spai/data/climate/copernicus/download_copernicus.py
RENAMED
|
@@ -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)
|
|
@@ -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"
|
|
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"
|
|
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")
|
|
File without changes
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
398
|
-
|
|
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
|
-
|
|
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,
|
|
419
|
-
#
|
|
420
|
-
|
|
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
|
|
470
|
+
return self.read_object(name)
|
|
438
471
|
|
|
439
472
|
async def data_stream(self, name):
|
|
440
|
-
with self.
|
|
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
|
-
|
|
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
|
|
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[
|
|
38
|
+
self.storages[name] = LocalStorage(**envs, name=name)
|
|
37
39
|
|
|
38
40
|
def initialize_s3(self, name, envs):
|
|
39
|
-
self.storages[
|
|
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)
|
|
File without changes
|
{spai-2026.7.7.dev2/tests/config → spai-2026.8.6.dev0/tests/deprecated/test_processing}/__init__.py
RENAMED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../../spai
|