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