pysits 2.0.0.dev7__tar.gz → 2.0.0.dev9__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.
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/PKG-INFO +2 -1
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pyproject.toml +2 -1
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/__init__.py +4 -0
- pysits-2.0.0.dev9/pysits/docs/content/sits_from_parquet.md +26 -0
- pysits-2.0.0.dev9/pysits/docs/content/sits_to_parquet.md +23 -0
- pysits-2.0.0.dev9/pysits/network.py +100 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/__init__.py +9 -1
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/files.py +30 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/ml.py +3 -1
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/utils.py +31 -8
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/conftest.py +51 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_exporters.py +51 -1
- pysits-2.0.0.dev9/tests/test_network.py +160 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_utils.py +20 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/uv.lock +74 -1
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.gitattributes +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.github/actions/setup-pysits/action.yml +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.github/workflows/ruff.yaml +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.github/workflows/tests.yaml +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.gitignore +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.pre-commit-config.yaml +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/LICENSE +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/README.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/conda/meta.yaml +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/arrow.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/functions.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/loaders.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/pkgs.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/rinterface.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/clojure.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/common.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/decorators.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/base.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/mask.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/tuning.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/stac.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/tibble.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/vector.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/xarray.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_linear.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_mean.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_mean_window.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_median.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/plot.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_accuracy.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_accuracy_summary.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_add_base_cube.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_apply.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_as_geopandas.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_bands.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_barlow_twins.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_bbox.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_classify.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_clean.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cluster_clean.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cluster_dendro.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cluster_frequency.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_qgis.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_reset.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_set.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_show.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_combine_predictions.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_confidence_sampling.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_config.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_config_show.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_config_user_file.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_contrastive_learning.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cube.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cube_copy.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_encode.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_formula_linear.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_formula_logref.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_geo_dist.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_get_class.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_get_data.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_get_probs.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_kfold_validate.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_label_classification.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_labels.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_labels_summary.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_lightgbm.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_lighttae.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_list_collections.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_lstm_fcn.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_merge.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_mixture_model.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_mlp.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_model_export.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_mosaic.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_parallel.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_patterns.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pre_train.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_features.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_normalize.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_references.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_sample.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_predictors.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_random_sampling.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_reclassify.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_reduce.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_reduce_imbalance.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_regularize.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_resnet.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_rfor.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_roi_to_tiles.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_sample.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_sampling_design.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_sankey.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_segment.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_select.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_show_prediction.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_slic.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_smooth.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_snic.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_clean_samples.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_evaluate_cluster.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_map.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_remove_samples.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_ssl_lejepa.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_ssl_mae.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_ssl_vicreg.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_stats.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_stratified_sampling.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_svm.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tae.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tempcnn.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_texture.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tiles_to_roi.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_timeline.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_timeseries_to_csv.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_to_csv.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_to_xlsx.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_train.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tuning.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tuning_hparams.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_uncertainty.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_uncertainty_sampling.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_validate.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_variance.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_view.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_xgboost.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/summary.md +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/decorators.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/extras/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/extras/earthdatalogin.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/extras/torch.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/jinja.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/base.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/accuracy.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/base.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/cube.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/frame.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/frame_accessor.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/matrix.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/table.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/ts.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/tuning.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/vector.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/frame.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/ml.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/resolver.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/visual.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/settings.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/colors.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/config.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/context.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/cube.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/data.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/sf.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/xarray.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/impute.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/segment.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/tiles.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/ts.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/tuning.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/visualization.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/templates/cube.html +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/templates/plot.html +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/templates/tuning.html +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/__init__.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/base.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/device.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/host.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/leaflet.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/options.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_backend.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_classification.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_config.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_conversions.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_cube.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_data.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_dsl.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_embedding.py +1 -1
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_exporters_geopandas.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_exporters_xarray.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_extras.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_indexing.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_ml_models.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_resolver.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_rl_models.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_som.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_tiles.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_tuning.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_validation.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_visual_models.py +0 -0
- {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_visualization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pysits
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev9
|
|
4
4
|
Summary: Python wrapper for the sits R package
|
|
5
5
|
Project-URL: Homepage, https://github.com/e-sensing/pysits
|
|
6
6
|
Project-URL: Issue Tracker, https://github.com/e-sensing/pysits/issues
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
18
18
|
Requires-Python: <4,>=3.10
|
|
19
19
|
Requires-Dist: geopandas>=1.1.0
|
|
20
|
+
Requires-Dist: httpx2>=2.13.0
|
|
20
21
|
Requires-Dist: jinja2>=3.1.6
|
|
21
22
|
Requires-Dist: matplotlib>=3.10.1
|
|
22
23
|
Requires-Dist: pandas>=2.2.3
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
[project]
|
|
19
19
|
name = "pysits"
|
|
20
|
-
version = "2.0.0.
|
|
20
|
+
version = "2.0.0.dev9"
|
|
21
21
|
description = "Python wrapper for the sits R package"
|
|
22
22
|
readme = "README.md"
|
|
23
23
|
requires-python = ">=3.10,<4"
|
|
@@ -71,6 +71,7 @@ dependencies = [
|
|
|
71
71
|
"shapely>=2.1.1",
|
|
72
72
|
"pyarrow>=20.0.0",
|
|
73
73
|
"jinja2>=3.1.6",
|
|
74
|
+
"httpx2>=2.13.0",
|
|
74
75
|
]
|
|
75
76
|
|
|
76
77
|
[project.urls]
|
|
@@ -87,8 +87,10 @@ from .sits.data import sits_summary as summary
|
|
|
87
87
|
from .sits.exporters import (
|
|
88
88
|
sits_as_geopandas,
|
|
89
89
|
sits_as_xarray,
|
|
90
|
+
sits_from_parquet,
|
|
90
91
|
sits_timeseries_to_csv,
|
|
91
92
|
sits_to_csv,
|
|
93
|
+
sits_to_parquet,
|
|
92
94
|
sits_to_xlsx,
|
|
93
95
|
)
|
|
94
96
|
from .sits.impute import (
|
|
@@ -276,6 +278,8 @@ __all__ = (
|
|
|
276
278
|
"sits_as_geopandas",
|
|
277
279
|
"sits_to_xlsx",
|
|
278
280
|
"sits_timeseries_to_csv",
|
|
281
|
+
"sits_to_parquet",
|
|
282
|
+
"sits_from_parquet",
|
|
279
283
|
# Tuning
|
|
280
284
|
"sits_tuning_hparams",
|
|
281
285
|
"sits_tuning",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Read sits time series from the Parquet format
|
|
2
|
+
|
|
3
|
+
Reads a file written by `sits_to_parquet` and rebuilds the time series,
|
|
4
|
+
with its class, column order and nested series.
|
|
5
|
+
|
|
6
|
+
Files written by other programs are also read. Their class is then
|
|
7
|
+
inferred from the columns, which must include "longitude", "latitude",
|
|
8
|
+
"start_date", "end_date" and "label".
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
file (str | pathlib.Path): Full path or HTTP(S) URL of the file to
|
|
12
|
+
read (valid file name with extension ".parquet").
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
SITSTimeSeriesModel: Time series.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
httpx2.HTTPError: If a remote file cannot be downloaded.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
import tempfile
|
|
22
|
+
from pysits import *
|
|
23
|
+
|
|
24
|
+
parquet_file = tempfile.gettempdir() + "/samples.parquet"
|
|
25
|
+
sits_to_parquet(samples_modis_ndvi, file=parquet_file)
|
|
26
|
+
samples = sits_from_parquet(parquet_file)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Export sits time series to the Parquet format
|
|
2
|
+
|
|
3
|
+
Writes a set of time series to a Parquet file. The layout is long: one
|
|
4
|
+
row per sample and date, with the time series as plain columns. The
|
|
5
|
+
file metadata keeps what is needed to rebuild the data with
|
|
6
|
+
`sits_from_parquet` (class, column order and bands). It also follows
|
|
7
|
+
GeoParquet 1.1, so tools like GDAL and QGIS read the file as points.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
data (SITSTimeSeriesModel): Time series.
|
|
11
|
+
|
|
12
|
+
file (str | pathlib.Path): Full path of the exported file (valid
|
|
13
|
+
file name with extension ".parquet").
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
None
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
import tempfile
|
|
20
|
+
from pysits import *
|
|
21
|
+
|
|
22
|
+
parquet_file = tempfile.gettempdir() + "/samples.parquet"
|
|
23
|
+
sits_to_parquet(samples_modis_ndvi, file=parquet_file)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""Network operations."""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import tempfile
|
|
22
|
+
from collections.abc import Iterator
|
|
23
|
+
from contextlib import contextmanager
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from urllib.parse import urlparse
|
|
26
|
+
|
|
27
|
+
import httpx2
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Supported URL schemes
|
|
31
|
+
#
|
|
32
|
+
REMOTE_SCHEMES = ("http", "https")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# Auxiliary functions
|
|
37
|
+
#
|
|
38
|
+
def is_remote(location: str | Path) -> bool:
|
|
39
|
+
"""Check if a location points to a remote resource.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
location (str | Path): File location (local path or URL).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool: ``True`` when ``location`` is an URL with a supported scheme.
|
|
46
|
+
|
|
47
|
+
Notes:
|
|
48
|
+
- Supported schemes are ``http`` and ``https``.
|
|
49
|
+
"""
|
|
50
|
+
return urlparse(str(location)).scheme in REMOTE_SCHEMES
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# File management
|
|
55
|
+
#
|
|
56
|
+
@contextmanager
|
|
57
|
+
def fetch_to_tempfile(url: str, *, suffix: str = "") -> Iterator[Path]:
|
|
58
|
+
"""Download a remote file to a temporary file.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
url (str): File URL.
|
|
62
|
+
|
|
63
|
+
suffix (str): Suffix for the temporary file name.
|
|
64
|
+
|
|
65
|
+
Yields:
|
|
66
|
+
Path: Path of the downloaded file.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
httpx2.HTTPError: If the file cannot be downloaded.
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
- The temporary file is removed when the context exits, including
|
|
73
|
+
when the download fails or the caller raises.
|
|
74
|
+
"""
|
|
75
|
+
# Create a temporary file
|
|
76
|
+
descriptor, name = tempfile.mkstemp(suffix=suffix)
|
|
77
|
+
|
|
78
|
+
# Name as path
|
|
79
|
+
local_file = Path(name)
|
|
80
|
+
|
|
81
|
+
# Try to download the file
|
|
82
|
+
try:
|
|
83
|
+
# Open the stream in binary mode
|
|
84
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
85
|
+
# Enable redirects to facilitate operations on remote files
|
|
86
|
+
# stored in systems like huggingface and similar platforms.
|
|
87
|
+
with httpx2.stream("GET", url, follow_redirects=True) as response:
|
|
88
|
+
# Raise an exception if the response is not successful
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
|
|
91
|
+
# Write the content to the temporary file
|
|
92
|
+
for chunk in response.iter_bytes():
|
|
93
|
+
stream.write(chunk)
|
|
94
|
+
|
|
95
|
+
# Yield the path to the temporary file
|
|
96
|
+
yield local_file
|
|
97
|
+
|
|
98
|
+
finally:
|
|
99
|
+
# Remove the temporary file
|
|
100
|
+
local_file.unlink(missing_ok=True)
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
"""Exporters module."""
|
|
19
19
|
|
|
20
|
-
from .files import
|
|
20
|
+
from .files import (
|
|
21
|
+
sits_from_parquet,
|
|
22
|
+
sits_timeseries_to_csv,
|
|
23
|
+
sits_to_csv,
|
|
24
|
+
sits_to_parquet,
|
|
25
|
+
sits_to_xlsx,
|
|
26
|
+
)
|
|
21
27
|
from .sf import sits_as_geopandas
|
|
22
28
|
from .xarray import sits_as_xarray
|
|
23
29
|
|
|
@@ -27,4 +33,6 @@ __all__ = (
|
|
|
27
33
|
"sits_to_csv",
|
|
28
34
|
"sits_to_xlsx",
|
|
29
35
|
"sits_timeseries_to_csv",
|
|
36
|
+
"sits_to_parquet",
|
|
37
|
+
"sits_from_parquet",
|
|
30
38
|
)
|
|
@@ -17,10 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
"""Files exporter."""
|
|
19
19
|
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
20
22
|
from pysits.backend.pkgs import r_pkg_sits
|
|
21
23
|
from pysits.conversions.decorators import function_call
|
|
22
24
|
from pysits.docs import attach_doc
|
|
23
25
|
from pysits.models.data.frame import SITSFrame
|
|
26
|
+
from pysits.models.data.ts import SITSTimeSeriesModel
|
|
27
|
+
from pysits.models.resolver import resolve_and_invoke_content_class
|
|
28
|
+
from pysits.network import fetch_to_tempfile, is_remote
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
@function_call(r_pkg_sits.sits_to_csv, SITSFrame)
|
|
@@ -41,3 +46,28 @@ def sits_to_xlsx(*args, **kwargs) -> None:
|
|
|
41
46
|
@attach_doc("sits_timeseries_to_csv")
|
|
42
47
|
def sits_timeseries_to_csv(*args, **kwargs) -> None:
|
|
43
48
|
"""Export a full sits timeseries to CSV format."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@function_call(r_pkg_sits.sits_to_parquet, lambda x: None)
|
|
52
|
+
@attach_doc("sits_to_parquet")
|
|
53
|
+
def sits_to_parquet(*args, **kwargs) -> None:
|
|
54
|
+
"""Export sits time series to the Parquet format."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@function_call(r_pkg_sits.sits_from_parquet, resolve_and_invoke_content_class)
|
|
58
|
+
def _sits_from_parquet(*args, **kwargs) -> SITSTimeSeriesModel:
|
|
59
|
+
"""Read sits time series from a local Parquet file."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@attach_doc("sits_from_parquet")
|
|
63
|
+
def sits_from_parquet(file: str | Path) -> SITSTimeSeriesModel:
|
|
64
|
+
"""Read sits time series from the Parquet format."""
|
|
65
|
+
# If file is remote
|
|
66
|
+
if is_remote(file):
|
|
67
|
+
# Download it to a temporary file
|
|
68
|
+
with fetch_to_tempfile(str(file), suffix=".parquet") as local_file:
|
|
69
|
+
# Read it
|
|
70
|
+
return _sits_from_parquet(local_file)
|
|
71
|
+
|
|
72
|
+
# Otherwise, read it directly
|
|
73
|
+
return _sits_from_parquet(file)
|
|
@@ -84,7 +84,9 @@ sits_lightgbm = closure_factory("sits_lightgbm")
|
|
|
84
84
|
sits_ssl_mae = closure_factory("sits_ssl_mae", converters=dl_converters)
|
|
85
85
|
sits_ssl_lejepa = closure_factory("sits_ssl_lejepa", converters=dl_converters)
|
|
86
86
|
sits_ssl_vicreg = closure_factory("sits_ssl_vicreg", converters=dl_converters)
|
|
87
|
-
sits_ssl_barlow_twins = closure_factory(
|
|
87
|
+
sits_ssl_barlow_twins = closure_factory(
|
|
88
|
+
"sits_ssl_barlow_twins", converters=dl_converters
|
|
89
|
+
)
|
|
88
90
|
|
|
89
91
|
sits_barlow_twins = closure_factory("sits_barlow_twins", converters=dl_converters)
|
|
90
92
|
sits_contrastive_learning = closure_factory(
|
|
@@ -27,6 +27,7 @@ from pysits.models.resolver import (
|
|
|
27
27
|
resolve_and_invoke_accuracy_class,
|
|
28
28
|
resolve_and_invoke_content_class,
|
|
29
29
|
)
|
|
30
|
+
from pysits.network import fetch_to_tempfile, is_remote
|
|
30
31
|
|
|
31
32
|
#
|
|
32
33
|
# Data class resolver
|
|
@@ -56,19 +57,41 @@ def read_rds(file: str | Path) -> SITSFrame:
|
|
|
56
57
|
"""Read RDS file compatible with SITS.
|
|
57
58
|
|
|
58
59
|
Args:
|
|
59
|
-
file (str | Path): RDS file.
|
|
60
|
+
file (str | Path): RDS file. It can be a local path or an HTTP(S) URL.
|
|
60
61
|
|
|
61
62
|
Returns:
|
|
62
63
|
SITSFrame: SITS Data instance (can be a ``cube`` or a ``time-series`` object).
|
|
63
|
-
"""
|
|
64
|
-
file = Path(file)
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
Raises:
|
|
66
|
+
FileNotFoundError: If a local RDS file does not exist. A missing
|
|
67
|
+
remote file is reported by ``httpx2`` instead,
|
|
68
|
+
as an ``HTTPStatusError``.
|
|
69
|
+
|
|
70
|
+
httpx2.HTTPError: If a remote RDS file cannot be downloaded.
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
ValueError: If the RDS file has an object not supported by sits.
|
|
73
|
+
"""
|
|
74
|
+
# If is remote file, we download it
|
|
75
|
+
if is_remote(file):
|
|
76
|
+
# Remote files are downloaded before being read. In R, it is possible
|
|
77
|
+
# to read RDS straight from an ``url`` connection, but that only works
|
|
78
|
+
# when the content is gzipped: the other compressions ``saveRDS`` produces
|
|
79
|
+
# (``xz`` and ``bzip2``) are rejected when read this way.
|
|
80
|
+
# So we download the file to a temporary file and read it from there to avoid
|
|
81
|
+
# this issue.
|
|
82
|
+
with fetch_to_tempfile(str(file), suffix=".rds") as local_file:
|
|
83
|
+
# Download and read content!
|
|
84
|
+
rds_content = r_fnc_read_rds(local_file.as_posix())
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
file = Path(file)
|
|
88
|
+
|
|
89
|
+
# Check if file exists
|
|
90
|
+
if not file.exists():
|
|
91
|
+
raise FileNotFoundError("Failed to read RDS: File does not exist.")
|
|
92
|
+
|
|
93
|
+
# Read RDS
|
|
94
|
+
rds_content = r_fnc_read_rds(file.as_posix())
|
|
72
95
|
|
|
73
96
|
# Resolve and invoke data class
|
|
74
97
|
for resolver in RDS_RESOLVERS:
|
|
@@ -25,6 +25,10 @@ import os
|
|
|
25
25
|
os.environ.setdefault("MPLBACKEND", "Agg")
|
|
26
26
|
|
|
27
27
|
import webbrowser
|
|
28
|
+
from functools import partial
|
|
29
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from threading import Thread
|
|
28
32
|
from typing import Any
|
|
29
33
|
|
|
30
34
|
import matplotlib
|
|
@@ -36,6 +40,11 @@ from pysits.sits.cube import sits_cube
|
|
|
36
40
|
from pysits.sits.utils import r_package_dir
|
|
37
41
|
from pysits.visualization.options import reset_plot_options
|
|
38
42
|
|
|
43
|
+
#
|
|
44
|
+
# Path prefix the test server answers with a redirect
|
|
45
|
+
#
|
|
46
|
+
REDIRECT_PREFIX = "/redirect/"
|
|
47
|
+
|
|
39
48
|
|
|
40
49
|
#
|
|
41
50
|
# Helpers
|
|
@@ -107,3 +116,45 @@ def no_browser(monkeypatch):
|
|
|
107
116
|
monkeypatch.setattr(webbrowser, "open_new_tab", lambda x: None)
|
|
108
117
|
|
|
109
118
|
yield
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _RedirectingRequestHandler(SimpleHTTPRequestHandler):
|
|
122
|
+
"""Serve a directory with a redirect."""
|
|
123
|
+
|
|
124
|
+
def do_GET(self) -> None: # noqa: N802 (name defined by the base class)
|
|
125
|
+
# If path starts with a given prefix, simulate a redirect
|
|
126
|
+
if self.path.startswith(REDIRECT_PREFIX):
|
|
127
|
+
# Simulate a redirect
|
|
128
|
+
self.send_response(302)
|
|
129
|
+
self.send_header("Location", self.path[len(REDIRECT_PREFIX) - 1 :])
|
|
130
|
+
self.end_headers()
|
|
131
|
+
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
super().do_GET()
|
|
135
|
+
|
|
136
|
+
def log_message(self, *args, **kwargs) -> None:
|
|
137
|
+
"""Keep the per-request log out of the test output."""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.fixture
|
|
141
|
+
def http_server(tmp_path: Path):
|
|
142
|
+
"""HTTP server serving a directory with a redirect."""
|
|
143
|
+
handler = partial(
|
|
144
|
+
_RedirectingRequestHandler,
|
|
145
|
+
directory=str(tmp_path),
|
|
146
|
+
)
|
|
147
|
+
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
148
|
+
|
|
149
|
+
# Start the server in a daemon thread
|
|
150
|
+
thread = Thread(target=server.serve_forever, daemon=True)
|
|
151
|
+
thread.start()
|
|
152
|
+
|
|
153
|
+
# Yield the directory and its base URL
|
|
154
|
+
try:
|
|
155
|
+
yield tmp_path, f"http://127.0.0.1:{server.server_port}"
|
|
156
|
+
|
|
157
|
+
# Shutdown the server
|
|
158
|
+
finally:
|
|
159
|
+
server.shutdown()
|
|
160
|
+
server.server_close()
|
|
@@ -25,8 +25,15 @@ import pytest
|
|
|
25
25
|
from pysits.backend.pkgs import r_pkg_sits
|
|
26
26
|
from pysits.models.data.frame import SITSFrame
|
|
27
27
|
from pysits.models.data.matrix import SITSConfusionMatrix
|
|
28
|
+
from pysits.models.data.ts import SITSTimeSeriesModel
|
|
28
29
|
from pysits.sits.context import cerrado_2classes
|
|
29
|
-
from pysits.sits.exporters import
|
|
30
|
+
from pysits.sits.exporters import (
|
|
31
|
+
sits_from_parquet,
|
|
32
|
+
sits_timeseries_to_csv,
|
|
33
|
+
sits_to_csv,
|
|
34
|
+
sits_to_parquet,
|
|
35
|
+
sits_to_xlsx,
|
|
36
|
+
)
|
|
30
37
|
from pysits.sits.ml import sits_rfor
|
|
31
38
|
from pysits.sits.ts import sits_sample, sits_validate
|
|
32
39
|
|
|
@@ -94,3 +101,46 @@ def test_sits_to_xlsx(tmp_path: Path, accuracy):
|
|
|
94
101
|
r_pkg_sits.sits_to_xlsx(accuracy._instance, file=str(r_file))
|
|
95
102
|
|
|
96
103
|
assert read_xlsx(py_file) == read_xlsx(r_file)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_sits_parquet(tmp_path: Path):
|
|
107
|
+
"""Test time series export / import using parquet."""
|
|
108
|
+
# local file
|
|
109
|
+
file = tmp_path / "samples.parquet"
|
|
110
|
+
|
|
111
|
+
# export samples as parquet
|
|
112
|
+
sits_to_parquet(cerrado_2classes, file=file)
|
|
113
|
+
|
|
114
|
+
# import samples from parquet
|
|
115
|
+
samples = sits_from_parquet(file)
|
|
116
|
+
|
|
117
|
+
# samples must be a time series model
|
|
118
|
+
assert isinstance(samples, SITSTimeSeriesModel)
|
|
119
|
+
|
|
120
|
+
# metadata and nested series must be equal to the original samples
|
|
121
|
+
metadata = samples.drop(columns="time_series")
|
|
122
|
+
expected = cerrado_2classes.drop(columns="time_series")
|
|
123
|
+
|
|
124
|
+
assert metadata.equals(expected)
|
|
125
|
+
assert all(
|
|
126
|
+
ts.equals(expected_ts)
|
|
127
|
+
for ts, expected_ts in zip(
|
|
128
|
+
samples["time_series"], cerrado_2classes["time_series"], strict=True
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_sits_from_parquet_remote(http_server):
|
|
134
|
+
"""Test time series import using remote parquet file."""
|
|
135
|
+
# remote file
|
|
136
|
+
directory, base_url = http_server
|
|
137
|
+
|
|
138
|
+
# export samples as parquet
|
|
139
|
+
sits_to_parquet(cerrado_2classes, file=directory / "samples.parquet")
|
|
140
|
+
|
|
141
|
+
# import samples from parquet
|
|
142
|
+
samples = sits_from_parquet(f"{base_url}/samples.parquet")
|
|
143
|
+
|
|
144
|
+
# samples must be a time series model
|
|
145
|
+
assert isinstance(samples, SITSTimeSeriesModel)
|
|
146
|
+
assert len(samples) == len(cerrado_2classes)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 sits developers.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify it
|
|
5
|
+
# under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
"""Unit tests for network operations."""
|
|
19
|
+
|
|
20
|
+
import socket
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from threading import Thread
|
|
24
|
+
|
|
25
|
+
import httpx2
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
from pysits.network import fetch_to_tempfile, is_remote
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Auxiliary functions
|
|
33
|
+
#
|
|
34
|
+
def temporary_files() -> set[Path]:
|
|
35
|
+
"""List the files currently in the temporary directory."""
|
|
36
|
+
return set(Path(tempfile.gettempdir()).iterdir())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Fixtures
|
|
41
|
+
#
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def raw_server():
|
|
44
|
+
"""HTTP Server."""
|
|
45
|
+
server_sockets = []
|
|
46
|
+
|
|
47
|
+
def start(response: bytes) -> str:
|
|
48
|
+
"""Start the HTTP server."""
|
|
49
|
+
# Create a socket and bind it to a random port
|
|
50
|
+
server_socket = socket.socket()
|
|
51
|
+
server_socket.bind(("127.0.0.1", 0))
|
|
52
|
+
server_socket.listen(1)
|
|
53
|
+
server_sockets.append(server_socket)
|
|
54
|
+
|
|
55
|
+
# Start a thread to serve the response
|
|
56
|
+
def serve() -> None:
|
|
57
|
+
try:
|
|
58
|
+
connection, _ = server_socket.accept()
|
|
59
|
+
connection.recv(4096)
|
|
60
|
+
connection.sendall(response)
|
|
61
|
+
connection.close()
|
|
62
|
+
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Start the thread
|
|
67
|
+
Thread(target=serve, daemon=True).start()
|
|
68
|
+
|
|
69
|
+
# Return the URL of the server
|
|
70
|
+
return f"http://127.0.0.1:{server_socket.getsockname()[1]}/samples.rds"
|
|
71
|
+
|
|
72
|
+
yield start
|
|
73
|
+
|
|
74
|
+
for server_socket in server_sockets:
|
|
75
|
+
server_socket.close()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Tests
|
|
80
|
+
#
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
("location", "expected"),
|
|
83
|
+
[
|
|
84
|
+
("https://example.org/samples.rds", True),
|
|
85
|
+
("http://example.org/samples.rds", True),
|
|
86
|
+
("/data/samples.rds", False),
|
|
87
|
+
("/home/user/samples.rds", False),
|
|
88
|
+
# Note: ``urlparse`` reads the drive letter as the scheme,
|
|
89
|
+
# so a Windows path must not be mistaken for a remote location
|
|
90
|
+
(r"C:\data\samples.rds", False),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
def test_is_remote(location: str, expected: bool):
|
|
94
|
+
"""Test remote location identification."""
|
|
95
|
+
assert is_remote(location) is expected
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_fetch_to_tempfile_downloads_and_cleans_up(http_server):
|
|
99
|
+
"""Test remote content download."""
|
|
100
|
+
directory, base_url = http_server
|
|
101
|
+
|
|
102
|
+
# Save payload
|
|
103
|
+
samples_file_local = directory / "samples.rds"
|
|
104
|
+
samples_file_local.write_bytes(b"sits-payload")
|
|
105
|
+
|
|
106
|
+
# Fetch the file
|
|
107
|
+
samples_file_remote = f"{base_url}/samples.rds"
|
|
108
|
+
|
|
109
|
+
with fetch_to_tempfile(samples_file_remote, suffix=".rds") as local_file:
|
|
110
|
+
assert local_file.read_bytes() == b"sits-payload"
|
|
111
|
+
|
|
112
|
+
# Check the file is deleted
|
|
113
|
+
assert not local_file.exists()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_fetch_to_tempfile_follows_redirects(http_server):
|
|
117
|
+
"""Test redirect following."""
|
|
118
|
+
directory, base_url = http_server
|
|
119
|
+
|
|
120
|
+
# Save payload
|
|
121
|
+
samples_file_local = directory / "samples.rds"
|
|
122
|
+
samples_file_local.write_bytes(b"sits-payload")
|
|
123
|
+
|
|
124
|
+
# Fetch the file
|
|
125
|
+
samples_file_remote = f"{base_url}/redirect/samples.rds"
|
|
126
|
+
|
|
127
|
+
with fetch_to_tempfile(samples_file_remote, suffix=".rds") as local_file:
|
|
128
|
+
assert local_file.read_bytes() == b"sits-payload"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_fetch_to_tempfile_rejects_error_status(http_server):
|
|
132
|
+
"""Test error response handling."""
|
|
133
|
+
_, base_url = http_server
|
|
134
|
+
|
|
135
|
+
# Save payload
|
|
136
|
+
before = temporary_files()
|
|
137
|
+
|
|
138
|
+
# Fetch file
|
|
139
|
+
with pytest.raises(httpx2.HTTPStatusError):
|
|
140
|
+
with fetch_to_tempfile(f"{base_url}/non-existing-file.rds"):
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
assert temporary_files() == before
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_fetch_to_tempfile_rejects_truncated_response(raw_server):
|
|
147
|
+
"""Test truncated response handling."""
|
|
148
|
+
# Create a server that returns a truncated response
|
|
149
|
+
url = raw_server(b"HTTP/1.1 200 OK\r\nContent-Length: 5000\r\n\r\n" + b"x" * 100)
|
|
150
|
+
|
|
151
|
+
# Save payload
|
|
152
|
+
before = temporary_files()
|
|
153
|
+
|
|
154
|
+
# Fetch file
|
|
155
|
+
with pytest.raises(httpx2.RemoteProtocolError):
|
|
156
|
+
with fetch_to_tempfile(url):
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
# Check the file is deleted
|
|
160
|
+
assert temporary_files() == before
|
|
@@ -53,6 +53,26 @@ def test_read_rds(tmp_path: Path):
|
|
|
53
53
|
assert isinstance(rds_content, SITSTimeSeriesModel)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
@pytest.mark.parametrize("compress", ["gzip", "xz", "bzip2"])
|
|
57
|
+
def test_read_rds_remote(http_server, compress: str):
|
|
58
|
+
"""Test read RDS from a remote file."""
|
|
59
|
+
directory, base_url = http_server
|
|
60
|
+
|
|
61
|
+
# Save RDS
|
|
62
|
+
r_fnc_save_rds = load_function_from_package("base::saveRDS")
|
|
63
|
+
r_fnc_save_rds(
|
|
64
|
+
samples_modis_ndvi._instance,
|
|
65
|
+
(directory / "samples.rds").as_posix(),
|
|
66
|
+
compress=compress,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Read remote RDS
|
|
70
|
+
rds_content = read_rds(f"{base_url}/samples.rds")
|
|
71
|
+
|
|
72
|
+
# Check the content
|
|
73
|
+
assert isinstance(rds_content, SITSTimeSeriesModel)
|
|
74
|
+
|
|
75
|
+
|
|
56
76
|
def test_read_rds_file_not_found():
|
|
57
77
|
"""Test read RDS file not found."""
|
|
58
78
|
with pytest.raises(FileNotFoundError):
|