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.
Files changed (214) hide show
  1. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/PKG-INFO +2 -1
  2. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pyproject.toml +2 -1
  3. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/__init__.py +4 -0
  4. pysits-2.0.0.dev9/pysits/docs/content/sits_from_parquet.md +26 -0
  5. pysits-2.0.0.dev9/pysits/docs/content/sits_to_parquet.md +23 -0
  6. pysits-2.0.0.dev9/pysits/network.py +100 -0
  7. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/__init__.py +9 -1
  8. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/files.py +30 -0
  9. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/ml.py +3 -1
  10. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/utils.py +31 -8
  11. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/conftest.py +51 -0
  12. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_exporters.py +51 -1
  13. pysits-2.0.0.dev9/tests/test_network.py +160 -0
  14. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_utils.py +20 -0
  15. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/uv.lock +74 -1
  16. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.gitattributes +0 -0
  17. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.github/actions/setup-pysits/action.yml +0 -0
  18. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.github/workflows/ruff.yaml +0 -0
  19. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.github/workflows/tests.yaml +0 -0
  20. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.gitignore +0 -0
  21. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/.pre-commit-config.yaml +0 -0
  22. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/LICENSE +0 -0
  23. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/README.md +0 -0
  24. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/conda/meta.yaml +0 -0
  25. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/__init__.py +0 -0
  26. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/arrow.py +0 -0
  27. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/functions.py +0 -0
  28. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/loaders.py +0 -0
  29. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/pkgs.py +0 -0
  30. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/backend/rinterface.py +0 -0
  31. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/__init__.py +0 -0
  32. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/clojure.py +0 -0
  33. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/common.py +0 -0
  34. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/decorators.py +0 -0
  35. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/__init__.py +0 -0
  36. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/base.py +0 -0
  37. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/mask.py +0 -0
  38. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/dsl/tuning.py +0 -0
  39. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/stac.py +0 -0
  40. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/tibble.py +0 -0
  41. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/vector.py +0 -0
  42. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/conversions/xarray.py +0 -0
  43. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/__init__.py +0 -0
  44. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_linear.md +0 -0
  45. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_mean.md +0 -0
  46. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_mean_window.md +0 -0
  47. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/impute_median.md +0 -0
  48. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/plot.md +0 -0
  49. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_accuracy.md +0 -0
  50. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_accuracy_summary.md +0 -0
  51. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_add_base_cube.md +0 -0
  52. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_apply.md +0 -0
  53. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_as_geopandas.md +0 -0
  54. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_bands.md +0 -0
  55. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_barlow_twins.md +0 -0
  56. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_bbox.md +0 -0
  57. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_classify.md +0 -0
  58. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_clean.md +0 -0
  59. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cluster_clean.md +0 -0
  60. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cluster_dendro.md +0 -0
  61. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cluster_frequency.md +0 -0
  62. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors.md +0 -0
  63. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_qgis.md +0 -0
  64. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_reset.md +0 -0
  65. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_set.md +0 -0
  66. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_colors_show.md +0 -0
  67. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_combine_predictions.md +0 -0
  68. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_confidence_sampling.md +0 -0
  69. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_config.md +0 -0
  70. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_config_show.md +0 -0
  71. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_config_user_file.md +0 -0
  72. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_contrastive_learning.md +0 -0
  73. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cube.md +0 -0
  74. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_cube_copy.md +0 -0
  75. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_encode.md +0 -0
  76. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_formula_linear.md +0 -0
  77. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_formula_logref.md +0 -0
  78. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_geo_dist.md +0 -0
  79. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_get_class.md +0 -0
  80. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_get_data.md +0 -0
  81. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_get_probs.md +0 -0
  82. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_kfold_validate.md +0 -0
  83. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_label_classification.md +0 -0
  84. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_labels.md +0 -0
  85. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_labels_summary.md +0 -0
  86. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_lightgbm.md +0 -0
  87. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_lighttae.md +0 -0
  88. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_list_collections.md +0 -0
  89. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_lstm_fcn.md +0 -0
  90. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_merge.md +0 -0
  91. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_mixture_model.md +0 -0
  92. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_mlp.md +0 -0
  93. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_model_export.md +0 -0
  94. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_mosaic.md +0 -0
  95. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_parallel.md +0 -0
  96. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_patterns.md +0 -0
  97. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pre_train.md +0 -0
  98. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_features.md +0 -0
  99. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_normalize.md +0 -0
  100. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_references.md +0 -0
  101. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_pred_sample.md +0 -0
  102. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_predictors.md +0 -0
  103. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_random_sampling.md +0 -0
  104. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_reclassify.md +0 -0
  105. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_reduce.md +0 -0
  106. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_reduce_imbalance.md +0 -0
  107. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_regularize.md +0 -0
  108. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_resnet.md +0 -0
  109. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_rfor.md +0 -0
  110. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_roi_to_tiles.md +0 -0
  111. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_sample.md +0 -0
  112. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_sampling_design.md +0 -0
  113. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_sankey.md +0 -0
  114. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_segment.md +0 -0
  115. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_select.md +0 -0
  116. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_show_prediction.md +0 -0
  117. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_slic.md +0 -0
  118. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_smooth.md +0 -0
  119. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_snic.md +0 -0
  120. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_clean_samples.md +0 -0
  121. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_evaluate_cluster.md +0 -0
  122. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_map.md +0 -0
  123. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_som_remove_samples.md +0 -0
  124. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_ssl_lejepa.md +0 -0
  125. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_ssl_mae.md +0 -0
  126. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_ssl_vicreg.md +0 -0
  127. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_stats.md +0 -0
  128. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_stratified_sampling.md +0 -0
  129. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_svm.md +0 -0
  130. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tae.md +0 -0
  131. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tempcnn.md +0 -0
  132. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_texture.md +0 -0
  133. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tiles_to_roi.md +0 -0
  134. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_timeline.md +0 -0
  135. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_timeseries_to_csv.md +0 -0
  136. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_to_csv.md +0 -0
  137. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_to_xlsx.md +0 -0
  138. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_train.md +0 -0
  139. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tuning.md +0 -0
  140. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_tuning_hparams.md +0 -0
  141. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_uncertainty.md +0 -0
  142. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_uncertainty_sampling.md +0 -0
  143. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_validate.md +0 -0
  144. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_variance.md +0 -0
  145. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_view.md +0 -0
  146. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/sits_xgboost.md +0 -0
  147. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/content/summary.md +0 -0
  148. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/docs/decorators.py +0 -0
  149. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/extras/__init__.py +0 -0
  150. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/extras/earthdatalogin.py +0 -0
  151. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/extras/torch.py +0 -0
  152. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/jinja.py +0 -0
  153. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/__init__.py +0 -0
  154. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/base.py +0 -0
  155. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/__init__.py +0 -0
  156. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/accuracy.py +0 -0
  157. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/base.py +0 -0
  158. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/cube.py +0 -0
  159. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/frame.py +0 -0
  160. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/frame_accessor.py +0 -0
  161. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/matrix.py +0 -0
  162. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/table.py +0 -0
  163. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/ts.py +0 -0
  164. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/tuning.py +0 -0
  165. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/data/vector.py +0 -0
  166. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/frame.py +0 -0
  167. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/ml.py +0 -0
  168. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/resolver.py +0 -0
  169. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/models/visual.py +0 -0
  170. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/settings.py +0 -0
  171. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/__init__.py +0 -0
  172. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/colors.py +0 -0
  173. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/config.py +0 -0
  174. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/context.py +0 -0
  175. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/cube.py +0 -0
  176. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/data.py +0 -0
  177. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/sf.py +0 -0
  178. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/exporters/xarray.py +0 -0
  179. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/impute.py +0 -0
  180. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/segment.py +0 -0
  181. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/tiles.py +0 -0
  182. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/ts.py +0 -0
  183. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/tuning.py +0 -0
  184. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/sits/visualization.py +0 -0
  185. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/templates/cube.html +0 -0
  186. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/templates/plot.html +0 -0
  187. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/templates/tuning.html +0 -0
  188. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/__init__.py +0 -0
  189. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/base.py +0 -0
  190. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/device.py +0 -0
  191. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/host.py +0 -0
  192. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/leaflet.py +0 -0
  193. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/pysits/visualization/options.py +0 -0
  194. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_backend.py +0 -0
  195. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_classification.py +0 -0
  196. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_config.py +0 -0
  197. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_conversions.py +0 -0
  198. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_cube.py +0 -0
  199. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_data.py +0 -0
  200. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_dsl.py +0 -0
  201. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_embedding.py +1 -1
  202. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_exporters_geopandas.py +0 -0
  203. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_exporters_xarray.py +0 -0
  204. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_extras.py +0 -0
  205. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_indexing.py +0 -0
  206. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_ml_models.py +0 -0
  207. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_resolver.py +0 -0
  208. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_rl_models.py +0 -0
  209. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_som.py +0 -0
  210. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_tiles.py +0 -0
  211. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_tuning.py +0 -0
  212. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_validation.py +0 -0
  213. {pysits-2.0.0.dev7 → pysits-2.0.0.dev9}/tests/test_visual_models.py +0 -0
  214. {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.dev7
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.dev7"
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 sits_timeseries_to_csv, sits_to_csv, sits_to_xlsx
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("sits_ssl_barlow_twins", converters=dl_converters)
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
- # Check if file exists
67
- if not file.exists():
68
- raise FileNotFoundError("Failed to read RDS: File does not exist.")
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
- # Read RDS
71
- rds_content = r_fnc_read_rds(file.as_posix())
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 sits_timeseries_to_csv, sits_to_csv, sits_to_xlsx
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):