aereo 1.1.0__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.
- aereo-1.1.0/PKG-INFO +48 -0
- aereo-1.1.0/README.md +14 -0
- aereo-1.1.0/aereo/asset_downloader/__init__.py +17 -0
- aereo-1.1.0/aereo/asset_downloader/core.py +194 -0
- aereo-1.1.0/aereo/cli/__init__.py +0 -0
- aereo-1.1.0/aereo/cli/main.py +491 -0
- aereo-1.1.0/aereo/client/__init__.py +9 -0
- aereo-1.1.0/aereo/client/core.py +484 -0
- aereo-1.1.0/aereo/eoids/__init__.py +15 -0
- aereo-1.1.0/aereo/eoids/core.py +432 -0
- aereo-1.1.0/aereo/execution/__init__.py +20 -0
- aereo-1.1.0/aereo/execution/backends.py +195 -0
- aereo-1.1.0/aereo/execution/core.py +227 -0
- aereo-1.1.0/aereo/grid/__init__.py +16 -0
- aereo-1.1.0/aereo/grid/core.py +447 -0
- aereo-1.1.0/aereo/interfaces/__init__.py +33 -0
- aereo-1.1.0/aereo/interfaces/core.py +637 -0
- aereo-1.1.0/aereo/registry/__init__.py +7 -0
- aereo-1.1.0/aereo/registry/core.py +278 -0
- aereo-1.1.0/aereo/schemas/__init__.py +8 -0
- aereo-1.1.0/aereo/schemas/core.py +101 -0
- aereo-1.1.0/aereo/serialization/__init__.py +3 -0
- aereo-1.1.0/aereo/serialization/core.py +117 -0
- aereo-1.1.0/aereo/spatial/__init__.py +15 -0
- aereo-1.1.0/aereo/spatial/core.py +0 -0
- aereo-1.1.0/aereo/spatial/utils.py +73 -0
- aereo-1.1.0/aereo/viz/__init__.py +7 -0
- aereo-1.1.0/aereo/viz/core.py +113 -0
- aereo-1.1.0/pyproject.toml +67 -0
aereo-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aereo
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A modular, high-performance capability-graph framework for processing Earth Observation sensor data.
|
|
5
|
+
Project-URL: Documentation, https://frandorr.github.io/aereo
|
|
6
|
+
Project-URL: Homepage, https://github.com/frandorr/aereo
|
|
7
|
+
Project-URL: Issues, https://github.com/frandorr/aereo/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/frandorr/aereo
|
|
9
|
+
Author-email: Francisco Dorr <fran.dorr@gmail.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: earth-observation,gis,polylith,remote-sensing,satellite
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: attrs>=25.4
|
|
22
|
+
Requires-Dist: cattrs>=24.1
|
|
23
|
+
Requires-Dist: geopandas>=1.1.2
|
|
24
|
+
Requires-Dist: majortom-eg
|
|
25
|
+
Requires-Dist: pandas>=3.0.1
|
|
26
|
+
Requires-Dist: pandera[geopandas]>=0.24
|
|
27
|
+
Requires-Dist: pyproj>=3.7.1
|
|
28
|
+
Requires-Dist: rich>=13
|
|
29
|
+
Requires-Dist: shapely>=2.1.2
|
|
30
|
+
Requires-Dist: structlog>=25.5
|
|
31
|
+
Requires-Dist: typer>=0.12
|
|
32
|
+
Requires-Dist: utm>=0.8.1
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# aereoeo
|
|
36
|
+
|
|
37
|
+
`aereo` is a modular, high-performance capability-graph framework for downloading, parsing, and transforming Earth Observation sensor data.
|
|
38
|
+
|
|
39
|
+
This `aereo` package includes the base registries, spatial transforms, dependency grids, and foundational typing rules meant to be extended by third-party Python plugin components.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Strict Validations:** Uses `pandera` and `geopandas` for 100% rigorous parsing of geo-features.
|
|
44
|
+
- **Capability Graph Engine:** Automatically infers plugin IO shapes dynamically.
|
|
45
|
+
- **Lazy Discovery Engine:** Finds and eagerly caches dynamic instrument components.
|
|
46
|
+
- **SatPy/PyResample Wrappers:** Seamless grid abstractions across `utm`, `proj`.
|
|
47
|
+
|
|
48
|
+
For building capabilities and plugins on top of `aereo`, see our official Plugin Developer Guide on the repository docs!
|
aereo-1.1.0/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# aereoeo
|
|
2
|
+
|
|
3
|
+
`aereo` is a modular, high-performance capability-graph framework for downloading, parsing, and transforming Earth Observation sensor data.
|
|
4
|
+
|
|
5
|
+
This `aereo` package includes the base registries, spatial transforms, dependency grids, and foundational typing rules meant to be extended by third-party Python plugin components.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Strict Validations:** Uses `pandera` and `geopandas` for 100% rigorous parsing of geo-features.
|
|
10
|
+
- **Capability Graph Engine:** Automatically infers plugin IO shapes dynamically.
|
|
11
|
+
- **Lazy Discovery Engine:** Finds and eagerly caches dynamic instrument components.
|
|
12
|
+
- **SatPy/PyResample Wrappers:** Seamless grid abstractions across `utm`, `proj`.
|
|
13
|
+
|
|
14
|
+
For building capabilities and plugins on top of `aereo`, see our official Plugin Developer Guide on the repository docs!
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe asset downloading and cleanup routines
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aereo.asset_downloader.core import (
|
|
6
|
+
cleanup_asset_safely,
|
|
7
|
+
download_asset_safely,
|
|
8
|
+
download_assets_safely,
|
|
9
|
+
extract_asset_safely,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"cleanup_asset_safely",
|
|
14
|
+
"download_asset_safely",
|
|
15
|
+
"download_assets_safely",
|
|
16
|
+
"extract_asset_safely",
|
|
17
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from aereo.interfaces import Downloader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def download_asset_safely(
|
|
11
|
+
href: str,
|
|
12
|
+
local_path: Path,
|
|
13
|
+
s3_client: Optional[Any] = None,
|
|
14
|
+
http_session: Optional[Any] = None,
|
|
15
|
+
downloader: Optional["Downloader"] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Download asset with a filelock to avoid corruption in multi-processing.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
href: URL or local path to the asset.
|
|
21
|
+
local_path: Destination path for the downloaded file.
|
|
22
|
+
s3_client: Optional authenticated S3FileSystem for S3 access.
|
|
23
|
+
Note: Only works when running in AWS us-west-2 region.
|
|
24
|
+
http_session: Optional authenticated requests.Session for HTTPS downloads.
|
|
25
|
+
Use earthaccess.get_requests_https_session() to create.
|
|
26
|
+
Works from anywhere (no AWS region requirement).
|
|
27
|
+
downloader: Optional callable that handles the download itself.
|
|
28
|
+
If provided, it is called unconditionally inside the file lock
|
|
29
|
+
and all built-in logic is skipped.
|
|
30
|
+
"""
|
|
31
|
+
import filelock
|
|
32
|
+
import s3fs
|
|
33
|
+
import requests
|
|
34
|
+
|
|
35
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
lock_path = local_path.with_suffix(".lock")
|
|
37
|
+
|
|
38
|
+
with filelock.FileLock(str(lock_path)):
|
|
39
|
+
if not local_path.exists():
|
|
40
|
+
if downloader is not None:
|
|
41
|
+
downloader(href, local_path)
|
|
42
|
+
elif href.startswith("s3://"):
|
|
43
|
+
fs = (
|
|
44
|
+
s3_client if s3_client is not None else s3fs.S3FileSystem(anon=True)
|
|
45
|
+
)
|
|
46
|
+
fs.get(href.replace("s3://", ""), str(local_path))
|
|
47
|
+
elif href.startswith("http://") or href.startswith("https://"):
|
|
48
|
+
http = http_session if http_session is not None else requests
|
|
49
|
+
response = http.get(href, stream=True)
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
with open(local_path, "wb") as f:
|
|
52
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
53
|
+
f.write(chunk)
|
|
54
|
+
elif Path(href).exists():
|
|
55
|
+
if Path(href).absolute() != local_path.absolute():
|
|
56
|
+
shutil.copy(href, local_path)
|
|
57
|
+
else:
|
|
58
|
+
raise FileNotFoundError(f"Source file not found at {href}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def download_assets_safely(
|
|
62
|
+
hrefs: list[str],
|
|
63
|
+
local_paths: list[Path],
|
|
64
|
+
s3_client: Optional[Any] = None,
|
|
65
|
+
http_session: Optional[Any] = None,
|
|
66
|
+
downloader: Optional["Downloader"] = None,
|
|
67
|
+
max_workers: Optional[int] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Download multiple assets concurrently using a thread pool.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
hrefs: List of URLs or local paths to the assets.
|
|
73
|
+
local_paths: List of destination paths for the downloaded files.
|
|
74
|
+
s3_client: Optional authenticated S3FileSystem for S3 access.
|
|
75
|
+
http_session: Optional authenticated requests.Session for HTTPS downloads.
|
|
76
|
+
downloader: Optional callable that handles the download itself.
|
|
77
|
+
max_workers: The maximum number of threads to use. Defaults to None.
|
|
78
|
+
"""
|
|
79
|
+
if len(hrefs) != len(local_paths):
|
|
80
|
+
raise ValueError("hrefs and local_paths must have the same length")
|
|
81
|
+
|
|
82
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
83
|
+
futures = [
|
|
84
|
+
executor.submit(
|
|
85
|
+
download_asset_safely,
|
|
86
|
+
href=href,
|
|
87
|
+
local_path=local_path,
|
|
88
|
+
s3_client=s3_client,
|
|
89
|
+
http_session=http_session,
|
|
90
|
+
downloader=downloader,
|
|
91
|
+
)
|
|
92
|
+
for href, local_path in zip(hrefs, local_paths, strict=True)
|
|
93
|
+
]
|
|
94
|
+
for future in futures:
|
|
95
|
+
future.result()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_asset_safely(
|
|
99
|
+
archive_path: Path,
|
|
100
|
+
extract_dir: Optional[Path] = None,
|
|
101
|
+
lock_path: Optional[Path] = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Extract a zip archive safely with file locking.
|
|
104
|
+
|
|
105
|
+
Uses atomic extraction (temp directory + rename) so that other
|
|
106
|
+
processes never see a partially extracted directory. An
|
|
107
|
+
``.extracted`` marker file is written on success; if the marker
|
|
108
|
+
exists the function returns immediately.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
archive_path: Path to the zip archive.
|
|
112
|
+
extract_dir: Destination directory. Defaults to
|
|
113
|
+
``archive_path.with_suffix("")``.
|
|
114
|
+
lock_path: Path to the lock file. Defaults to
|
|
115
|
+
``extract_dir.with_suffix(".lock")``.
|
|
116
|
+
"""
|
|
117
|
+
import filelock
|
|
118
|
+
import shutil
|
|
119
|
+
import tempfile
|
|
120
|
+
import zipfile
|
|
121
|
+
|
|
122
|
+
archive_path = Path(archive_path)
|
|
123
|
+
if extract_dir is None:
|
|
124
|
+
extract_dir = archive_path.with_suffix("")
|
|
125
|
+
extract_dir = Path(extract_dir)
|
|
126
|
+
|
|
127
|
+
if lock_path is None:
|
|
128
|
+
lock_path = extract_dir.with_suffix(".lock")
|
|
129
|
+
lock_path = Path(lock_path)
|
|
130
|
+
|
|
131
|
+
marker_path = extract_dir.with_suffix(".extracted")
|
|
132
|
+
|
|
133
|
+
with filelock.FileLock(str(lock_path)):
|
|
134
|
+
# Already extracted and marked complete
|
|
135
|
+
if marker_path.exists() and extract_dir.exists():
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Remove any stale partial extraction
|
|
139
|
+
if extract_dir.exists():
|
|
140
|
+
shutil.rmtree(extract_dir, ignore_errors=True)
|
|
141
|
+
|
|
142
|
+
extract_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
|
|
144
|
+
# Extract to a temporary directory so other processes never
|
|
145
|
+
# see an incomplete destination.
|
|
146
|
+
temp_dir = tempfile.mkdtemp(
|
|
147
|
+
prefix=extract_dir.name + "_tmp_",
|
|
148
|
+
dir=extract_dir.parent,
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
with zipfile.ZipFile(archive_path, "r") as zf:
|
|
152
|
+
zf.extractall(temp_dir)
|
|
153
|
+
|
|
154
|
+
Path(temp_dir).rename(extract_dir)
|
|
155
|
+
except Exception:
|
|
156
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
marker_path.touch()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cleanup_asset_safely(
|
|
163
|
+
local_path: Path, chunk_id: Optional[int] = None, total_chunks: int = 1
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Safely clean up the downloaded asset after all chunks are processed."""
|
|
166
|
+
import filelock
|
|
167
|
+
|
|
168
|
+
lock_path = local_path.with_suffix(".lock")
|
|
169
|
+
if total_chunks > 1 and chunk_id is not None:
|
|
170
|
+
done_file = local_path.with_suffix(f".chunk_{chunk_id}.done")
|
|
171
|
+
done_file.touch()
|
|
172
|
+
with filelock.FileLock(str(lock_path)):
|
|
173
|
+
done_files = list(local_path.parent.glob(f"{local_path.stem}.chunk_*.done"))
|
|
174
|
+
if len(done_files) >= total_chunks:
|
|
175
|
+
if local_path.exists():
|
|
176
|
+
try:
|
|
177
|
+
local_path.unlink()
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
for df in done_files:
|
|
181
|
+
try:
|
|
182
|
+
df.unlink()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
try:
|
|
186
|
+
lock_path.unlink()
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
else:
|
|
190
|
+
if local_path.exists():
|
|
191
|
+
try:
|
|
192
|
+
local_path.unlink()
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
File without changes
|