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.
- {cubexpress-0.1.18 → cubexpress-0.1.32}/PKG-INFO +8 -6
- {cubexpress-0.1.18 → cubexpress-0.1.32}/README.md +2 -0
- cubexpress-0.1.32/cubexpress/__init__.py +84 -0
- cubexpress-0.1.32/cubexpress/animation.py +177 -0
- cubexpress-0.1.32/cubexpress/cache.py +73 -0
- cubexpress-0.1.32/cubexpress/cloud_utils.py +1361 -0
- cubexpress-0.1.32/cubexpress/config.py +45 -0
- cubexpress-0.1.32/cubexpress/conversion.py +153 -0
- cubexpress-0.1.32/cubexpress/cube.py +506 -0
- cubexpress-0.1.32/cubexpress/downloader.py +169 -0
- cubexpress-0.1.32/cubexpress/exceptions.py +33 -0
- cubexpress-0.1.32/cubexpress/formats.py +309 -0
- cubexpress-0.1.32/cubexpress/geospatial.py +283 -0
- cubexpress-0.1.32/cubexpress/geotyping.py +297 -0
- cubexpress-0.1.32/cubexpress/logging_config.py +41 -0
- cubexpress-0.1.32/cubexpress/mss_table.py +251 -0
- cubexpress-0.1.32/cubexpress/request.py +450 -0
- cubexpress-0.1.32/cubexpress/scene_geometry.py +123 -0
- cubexpress-0.1.32/cubexpress/test/__init__.py +1 -0
- cubexpress-0.1.32/cubexpress/test/conftest.py +119 -0
- cubexpress-0.1.32/cubexpress/test/test_animation.py +78 -0
- cubexpress-0.1.32/cubexpress/test/test_cache.py +106 -0
- cubexpress-0.1.32/cubexpress/test/test_cloud_utils.py +101 -0
- cubexpress-0.1.32/cubexpress/test/test_config.py +51 -0
- cubexpress-0.1.32/cubexpress/test/test_conversion.py +102 -0
- cubexpress-0.1.32/cubexpress/test/test_downloader.py +57 -0
- cubexpress-0.1.32/cubexpress/test/test_exceptions.py +46 -0
- cubexpress-0.1.32/cubexpress/test/test_formats.py +150 -0
- cubexpress-0.1.32/cubexpress/test/test_geospatial.py +273 -0
- cubexpress-0.1.32/cubexpress/test/test_geotyping.py +185 -0
- cubexpress-0.1.32/cubexpress/test/test_request.py +29 -0
- cubexpress-0.1.32/cubexpress/test/test_scene_geometry.py +51 -0
- cubexpress-0.1.32/cubexpress/test/test_tiling.py +223 -0
- cubexpress-0.1.32/cubexpress/tiling.py +160 -0
- cubexpress-0.1.32/pyproject.toml +170 -0
- cubexpress-0.1.18/cubexpress/__init__.py +0 -19
- cubexpress-0.1.18/cubexpress/cache.py +0 -52
- cubexpress-0.1.18/cubexpress/cloud_utils.py +0 -268
- cubexpress-0.1.18/cubexpress/conversion.py +0 -156
- cubexpress-0.1.18/cubexpress/cube.py +0 -240
- cubexpress-0.1.18/cubexpress/downloader.py +0 -97
- cubexpress-0.1.18/cubexpress/geospatial.py +0 -195
- cubexpress-0.1.18/cubexpress/geotyping.py +0 -398
- cubexpress-0.1.18/cubexpress/request.py +0 -120
- cubexpress-0.1.18/pyproject.toml +0 -100
- {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.
|
|
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
|
+
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
|
|
20
|
-
Requires-Dist: pandas (>=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.
|
|
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
|
+

|
|
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
|
+

|
|
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
|