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