cubexpress 0.1.18__tar.gz → 0.1.32__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 (46) hide show
  1. {cubexpress-0.1.18 → cubexpress-0.1.32}/PKG-INFO +8 -6
  2. {cubexpress-0.1.18 → cubexpress-0.1.32}/README.md +2 -0
  3. cubexpress-0.1.32/cubexpress/__init__.py +84 -0
  4. cubexpress-0.1.32/cubexpress/animation.py +177 -0
  5. cubexpress-0.1.32/cubexpress/cache.py +73 -0
  6. cubexpress-0.1.32/cubexpress/cloud_utils.py +1361 -0
  7. cubexpress-0.1.32/cubexpress/config.py +45 -0
  8. cubexpress-0.1.32/cubexpress/conversion.py +153 -0
  9. cubexpress-0.1.32/cubexpress/cube.py +506 -0
  10. cubexpress-0.1.32/cubexpress/downloader.py +169 -0
  11. cubexpress-0.1.32/cubexpress/exceptions.py +33 -0
  12. cubexpress-0.1.32/cubexpress/formats.py +309 -0
  13. cubexpress-0.1.32/cubexpress/geospatial.py +283 -0
  14. cubexpress-0.1.32/cubexpress/geotyping.py +297 -0
  15. cubexpress-0.1.32/cubexpress/logging_config.py +41 -0
  16. cubexpress-0.1.32/cubexpress/mss_table.py +251 -0
  17. cubexpress-0.1.32/cubexpress/request.py +450 -0
  18. cubexpress-0.1.32/cubexpress/scene_geometry.py +123 -0
  19. cubexpress-0.1.32/cubexpress/test/__init__.py +1 -0
  20. cubexpress-0.1.32/cubexpress/test/conftest.py +119 -0
  21. cubexpress-0.1.32/cubexpress/test/test_animation.py +78 -0
  22. cubexpress-0.1.32/cubexpress/test/test_cache.py +106 -0
  23. cubexpress-0.1.32/cubexpress/test/test_cloud_utils.py +101 -0
  24. cubexpress-0.1.32/cubexpress/test/test_config.py +51 -0
  25. cubexpress-0.1.32/cubexpress/test/test_conversion.py +102 -0
  26. cubexpress-0.1.32/cubexpress/test/test_downloader.py +57 -0
  27. cubexpress-0.1.32/cubexpress/test/test_exceptions.py +46 -0
  28. cubexpress-0.1.32/cubexpress/test/test_formats.py +150 -0
  29. cubexpress-0.1.32/cubexpress/test/test_geospatial.py +273 -0
  30. cubexpress-0.1.32/cubexpress/test/test_geotyping.py +185 -0
  31. cubexpress-0.1.32/cubexpress/test/test_request.py +29 -0
  32. cubexpress-0.1.32/cubexpress/test/test_scene_geometry.py +51 -0
  33. cubexpress-0.1.32/cubexpress/test/test_tiling.py +223 -0
  34. cubexpress-0.1.32/cubexpress/tiling.py +160 -0
  35. cubexpress-0.1.32/pyproject.toml +170 -0
  36. cubexpress-0.1.18/cubexpress/__init__.py +0 -19
  37. cubexpress-0.1.18/cubexpress/cache.py +0 -52
  38. cubexpress-0.1.18/cubexpress/cloud_utils.py +0 -268
  39. cubexpress-0.1.18/cubexpress/conversion.py +0 -156
  40. cubexpress-0.1.18/cubexpress/cube.py +0 -240
  41. cubexpress-0.1.18/cubexpress/downloader.py +0 -97
  42. cubexpress-0.1.18/cubexpress/geospatial.py +0 -195
  43. cubexpress-0.1.18/cubexpress/geotyping.py +0 -398
  44. cubexpress-0.1.18/cubexpress/request.py +0 -120
  45. cubexpress-0.1.18/pyproject.toml +0 -100
  46. {cubexpress-0.1.18 → cubexpress-0.1.32}/LICENSE +0 -0
@@ -1,28 +1,28 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cubexpress
3
- Version: 0.1.18
3
+ Version: 0.1.32
4
4
  Summary: Efficient processing of cubic Earth-observation (EO) data.
5
5
  Home-page: https://github.com/andesdatacube/cubexpress
6
6
  Keywords: earth-engine,sentinel-2,geospatial,eo,cube
7
7
  Author: Julio Contreras
8
8
  Author-email: contrerasnetk@gmail.com
9
- Requires-Python: >=3.9
9
+ Requires-Python: >=3.10,<4.0
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3 :: Only
17
16
  Classifier: Topic :: Scientific/Engineering :: GIS
18
17
  Requires-Dist: earthengine-api (>=1.5.12)
19
- Requires-Dist: numpy (>=2.0.2)
20
- Requires-Dist: pandas (>=2.2.2)
18
+ Requires-Dist: numpy (>=1.26.0,<2.1.0)
19
+ Requires-Dist: pandas (>=2.0.0)
21
20
  Requires-Dist: pyarrow (>=14.0.0)
22
- Requires-Dist: pydantic (>=2.11.4)
21
+ Requires-Dist: pydantic (>=2.0.0)
23
22
  Requires-Dist: pygeohash (>=1.2.0)
24
23
  Requires-Dist: pyproj (>=3.6.0)
25
24
  Requires-Dist: rasterio (>=1.3.9)
25
+ Requires-Dist: tqdm (>=4.65.0)
26
26
  Requires-Dist: utm (>=0.7.0)
27
27
  Project-URL: Documentation, https://andesdatacube.github.io/cubexpress
28
28
  Project-URL: Repository, https://github.com/andesdatacube/cubexpress
@@ -53,12 +53,14 @@ Description-Content-Type: text/markdown
53
53
  </a>
54
54
  </p>
55
55
 
56
+
56
57
  ---
57
58
 
58
59
  **GitHub**: [https://github.com/andesdatacube/cubexpress/](https://github.com/andesdatacube/cubexpress/) 🌐
59
60
 
60
61
  **PyPI**: [https://pypi.org/project/cubexpress/](https://pypi.org/project/cubexpress/) 🛠️
61
62
 
63
+ ![Tests](https://github.com/andesdatacube/cubexpress/actions/workflows/tests.yml/badge.svg)
62
64
  ---
63
65
 
64
66
  ## **Overview**
@@ -23,12 +23,14 @@
23
23
  </a>
24
24
  </p>
25
25
 
26
+
26
27
  ---
27
28
 
28
29
  **GitHub**: [https://github.com/andesdatacube/cubexpress/](https://github.com/andesdatacube/cubexpress/) 🌐
29
30
 
30
31
  **PyPI**: [https://pypi.org/project/cubexpress/](https://pypi.org/project/cubexpress/) 🛠️
31
32
 
33
+ ![Tests](https://github.com/andesdatacube/cubexpress/actions/workflows/tests.yml/badge.svg)
32
34
  ---
33
35
 
34
36
  ## **Overview**
@@ -0,0 +1,84 @@
1
+ """
2
+ CubExpress - Efficient Earth Engine data download and processing.
3
+
4
+ Main components:
5
+ - lonlat2rt: Convert coordinates to raster transforms
6
+ - s2_table: Query Sentinel-2 metadata with cloud scores
7
+ - sensor_table: Query any sensor metadata (Landsat, S2)
8
+ - table_to_requestset: Build request sets from metadata
9
+ - get_cube: Download Earth Engine data cubes
10
+
11
+ Format components:
12
+ - Formats: Pre-configured export format presets
13
+ - VisPresets: Visualization presets for common sensors
14
+ - EEFileFormat: Available Earth Engine file formats
15
+ - ExportFormat: Full format specification class
16
+ - VisualizationOptions: Visualization parameters class
17
+
18
+ Constants:
19
+ - LANDSAT_COMMON_OPTIONAL: Set of properties common to all Landsat sensors
20
+ - SENSORS: Dictionary of all supported sensor configurations
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from cubexpress.animation import create_gif, create_gif_from_requests
26
+ from cubexpress.cloud_utils import (
27
+ AGGREGATED_SENSORS,
28
+ LANDSAT_COMMON_OPTIONAL,
29
+ S2_BOA_BANDS,
30
+ S2_COMMON_OPTIONAL,
31
+ S2_TOA_BANDS,
32
+ SENSORS,
33
+ mss_table,
34
+ s2_table,
35
+ sensor_table,
36
+ )
37
+ from cubexpress.conversion import geo2utm, lonlat2rt
38
+ from cubexpress.cube import get_cube, get_geotiff, get_numpy_cube
39
+ from cubexpress.formats import EEFileFormat, ExportFormat, Formats, VisPresets, VisualizationOptions
40
+ from cubexpress.geotyping import RasterTransform, Request, RequestSet
41
+ from cubexpress.request import table_to_requestset
42
+ from cubexpress.scene_geometry import clear_scene_cache, get_batch_scene_info, get_scene_info
43
+
44
+ __all__ = [
45
+ # Core functions
46
+ "lonlat2rt",
47
+ "geo2utm",
48
+ "RasterTransform",
49
+ "Request",
50
+ "RequestSet",
51
+ "s2_table",
52
+ "mss_table",
53
+ "sensor_table",
54
+ "table_to_requestset",
55
+ "get_cube",
56
+ "get_geotiff",
57
+ "get_numpy_cube",
58
+ # Formats
59
+ "EEFileFormat",
60
+ "ExportFormat",
61
+ "Formats",
62
+ "VisualizationOptions",
63
+ "VisPresets",
64
+ # Animation
65
+ "create_gif",
66
+ "create_gif_from_requests",
67
+ # Constants
68
+ "AGGREGATED_SENSORS",
69
+ "LANDSAT_COMMON_OPTIONAL",
70
+ "S2_COMMON_OPTIONAL",
71
+ "S2_TOA_BANDS",
72
+ "S2_BOA_BANDS",
73
+ "SENSORS",
74
+ "get_batch_scene_info",
75
+ "get_scene_info",
76
+ "clear_scene_cache",
77
+ ]
78
+
79
+ try:
80
+ from importlib.metadata import version
81
+
82
+ __version__ = version("cubexpress")
83
+ except Exception:
84
+ __version__ = "0.0.0-dev"
@@ -0,0 +1,177 @@
1
+ """Animation utilities for creating GIFs from image sequences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+ from typing import Any
7
+
8
+
9
+ def create_gif(
10
+ image_paths: list[pathlib.Path],
11
+ output_path: pathlib.Path,
12
+ duration: int = 500,
13
+ loop: int = 0,
14
+ background_color: tuple[int, int, int] = (0, 0, 0),
15
+ ) -> pathlib.Path:
16
+ """Create animated GIF from image sequence.
17
+
18
+ Args:
19
+ image_paths: List of PNG/JPEG image paths in order
20
+ output_path: Output GIF path
21
+ duration: Frame duration in milliseconds
22
+ loop: Number of loops (0 = infinite)
23
+ background_color: RGB tuple for background (replaces transparency)
24
+
25
+ Returns:
26
+ Path to created GIF
27
+ """
28
+ try:
29
+ from PIL import Image
30
+ except ImportError:
31
+ raise ImportError("Pillow required for GIF creation: pip install Pillow") from None
32
+
33
+ if not image_paths:
34
+ raise ValueError("No images provided for GIF creation")
35
+
36
+ # Load all frames and convert to RGB (no transparency issues)
37
+ frames = []
38
+ for path in image_paths:
39
+ img = Image.open(path)
40
+
41
+ # Handle transparency by compositing onto background
42
+ if img.mode == "RGBA":
43
+ background = Image.new("RGB", img.size, background_color)
44
+ background.paste(img, mask=img.split()[3])
45
+ img = background
46
+ elif img.mode != "RGB":
47
+ img = img.convert("RGB")
48
+
49
+ frames.append(img)
50
+
51
+ # Save as animated GIF
52
+ output_path = pathlib.Path(output_path)
53
+ output_path.parent.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Convert to palette mode for GIF
56
+ frames_p = []
57
+ for frame in frames:
58
+ frame_p = frame.quantize(colors=256, method=Image.Quantize.MEDIANCUT)
59
+ frames_p.append(frame_p)
60
+
61
+ frames_p[0].save(
62
+ output_path,
63
+ save_all=True,
64
+ append_images=frames_p[1:],
65
+ duration=duration,
66
+ loop=loop,
67
+ optimize=False,
68
+ disposal=2,
69
+ )
70
+
71
+ return output_path
72
+
73
+
74
+ def create_gif_from_requests(
75
+ requests: Any, # RequestSet or DataFrame
76
+ visualization: Any | None = None,
77
+ output_path: pathlib.Path | str = "animation.gif",
78
+ duration: int = 500,
79
+ background_color: tuple[int, int, int] = (0, 0, 0),
80
+ keep_frames: pathlib.Path | str | None = None,
81
+ ) -> pathlib.Path:
82
+ """Create animated GIF from RequestSet with FIXED geometry.
83
+
84
+ Uses getPixels/computePixels directly to ensure correct geometry
85
+ without distortion.
86
+
87
+ Args:
88
+ requests: RequestSet or DataFrame from table_to_requestset
89
+ visualization: VisualizationOptions (e.g., VisPresets.s2_truecolor())
90
+ output_path: Output GIF path
91
+ duration: Frame duration in ms
92
+ background_color: RGB tuple for background (replaces transparency)
93
+ keep_frames: If provided, save PNG frames to this folder
94
+
95
+ Returns:
96
+ Path to created GIF
97
+
98
+ Example:
99
+ >>> df = sensor_table("S2_TOA", lon=-0.3763, lat=39.4699, edge_size=256,
100
+ ... start="2024-01-01", end="2024-03-01", max_cloud=20)
101
+ >>> requests = table_to_requestset(df, mosaic=True)
102
+ >>> create_gif_from_requests(
103
+ ... requests,
104
+ ... visualization=VisPresets.s2_truecolor(),
105
+ ... output_path="valencia_timelapse.gif",
106
+ ... duration=300,
107
+ ... )
108
+ """
109
+ from cubexpress.downloader import download_manifest, temp_workspace
110
+ from cubexpress.formats import EEFileFormat, ExportFormat
111
+
112
+ output_path = pathlib.Path(output_path)
113
+
114
+ # Get dataframe
115
+ if hasattr(requests, "_dataframe"):
116
+ dataframe = requests._dataframe
117
+ else:
118
+ dataframe = requests
119
+
120
+ if dataframe.empty:
121
+ raise ValueError("Request set is empty")
122
+
123
+ n_frames = len(dataframe)
124
+
125
+ if visualization is None:
126
+ raise ValueError("visualization is required for GIF creation. " "Use VisPresets.s2_truecolor() or similar.")
127
+
128
+ png_format = ExportFormat(
129
+ file_format=EEFileFormat.PNG,
130
+ visualization=visualization,
131
+ )
132
+
133
+ # Determine frame folder
134
+ if keep_frames:
135
+ frame_dir = pathlib.Path(keep_frames)
136
+ frame_dir.mkdir(parents=True, exist_ok=True)
137
+ use_temp = False
138
+ tmp_context = None
139
+ else:
140
+ use_temp = True
141
+ tmp_context = temp_workspace(prefix="cubexpress_gif_")
142
+
143
+ try:
144
+ if use_temp:
145
+ frame_dir = tmp_context.__enter__()
146
+
147
+ frame_paths = []
148
+
149
+ print(f"⏳ Rendering {n_frames} frames...", end="", flush=True)
150
+
151
+ for i, (_, row) in enumerate(dataframe.iterrows()):
152
+ frame_path = frame_dir / f"frame_{i:04d}_{row.id}.png"
153
+
154
+ download_manifest(
155
+ ulist=row.manifest,
156
+ full_outname=frame_path,
157
+ export_format=png_format,
158
+ )
159
+ frame_paths.append(frame_path)
160
+
161
+ print(f"\r✅ Rendered {n_frames} frames")
162
+ print("⏳ Creating GIF...", end="", flush=True)
163
+
164
+ result = create_gif(
165
+ image_paths=frame_paths,
166
+ output_path=output_path,
167
+ duration=duration,
168
+ background_color=background_color,
169
+ )
170
+
171
+ print(f"\r✅ Created GIF: {output_path} ({n_frames} frames)")
172
+
173
+ finally:
174
+ if use_temp and tmp_context:
175
+ tmp_context.__exit__(None, None, None)
176
+
177
+ return result
@@ -0,0 +1,73 @@
1
+ """Caching utilities for Earth Engine query results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import pathlib
8
+
9
+ from cubexpress.config import CACHE_DIR
10
+
11
+ CACHE_DIR.mkdir(exist_ok=True, parents=True)
12
+
13
+
14
+ def _cache_key(
15
+ lon: float,
16
+ lat: float,
17
+ edge_size: int | tuple[int, int],
18
+ scale: int,
19
+ collection: str,
20
+ ) -> pathlib.Path:
21
+ """
22
+ Generate a deterministic cache file path for query parameters.
23
+
24
+ Coordinates are rounded to 4 decimal places (~11m precision) to
25
+ ensure cache hits for equivalent locations.
26
+
27
+ Args:
28
+ lon: Longitude of center point
29
+ lat: Latitude of center point
30
+ edge_size: ROI size in pixels
31
+ scale: Pixel resolution in meters
32
+ collection: Earth Engine collection ID
33
+
34
+ Returns:
35
+ Path to hashed .parquet cache file
36
+ """
37
+ lon_r = round(lon, 4)
38
+ lat_r = round(lat, 4)
39
+
40
+ edge_tuple = (edge_size, edge_size) if isinstance(edge_size, int) else tuple(edge_size)
41
+
42
+ signature = [lon_r, lat_r, edge_tuple, scale, collection]
43
+
44
+ raw = json.dumps(signature, sort_keys=True).encode("utf-8")
45
+ digest = hashlib.md5(raw).hexdigest()
46
+
47
+ return CACHE_DIR / f"{digest}.parquet"
48
+
49
+
50
+ def clear_cache() -> int:
51
+ """
52
+ Remove all cached query results.
53
+
54
+ Returns:
55
+ Number of files deleted
56
+ """
57
+ count = 0
58
+ for cache_file in CACHE_DIR.glob("*.parquet"):
59
+ cache_file.unlink()
60
+ count += 1
61
+ return count
62
+
63
+
64
+ def get_cache_size() -> tuple[int, int]:
65
+ """
66
+ Calculate total cache size.
67
+
68
+ Returns:
69
+ Tuple of (file_count, total_bytes)
70
+ """
71
+ files = list(CACHE_DIR.glob("*.parquet"))
72
+ total_bytes = sum(f.stat().st_size for f in files)
73
+ return len(files), total_bytes