agrigee-lite-client 3.4.1__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.
- agrigee_lite_client-3.4.1/.gitignore +164 -0
- agrigee_lite_client-3.4.1/AGENTS.md +105 -0
- agrigee_lite_client-3.4.1/PKG-INFO +144 -0
- agrigee_lite_client-3.4.1/README.md +117 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/__init__.py +25 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_async_client.py +205 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_client.py +213 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_exceptions.py +57 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_geo_compat.py +47 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_geoparquet.py +66 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_get.py +111 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_satellite_dates.py +95 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_validation.py +91 -0
- agrigee_lite_client-3.4.1/agrigee_lite_client/_version.py +10 -0
- agrigee_lite_client-3.4.1/pyproject.toml +71 -0
- agrigee_lite_client-3.4.1/scripts/generate_satellite_dates.py +91 -0
- agrigee_lite_client-3.4.1/tests/__init__.py +0 -0
- agrigee_lite_client-3.4.1/tests/_fake_server.py +127 -0
- agrigee_lite_client-3.4.1/tests/test_async_client_mocked.py +188 -0
- agrigee_lite_client-3.4.1/tests/test_client_against_live_api.py +56 -0
- agrigee_lite_client-3.4.1/tests/test_client_mocked.py +186 -0
- agrigee_lite_client-3.4.1/tests/test_geo_compat.py +55 -0
- agrigee_lite_client-3.4.1/tests/test_geoparquet.py +70 -0
- agrigee_lite_client-3.4.1/tests/test_satellite_dates.py +30 -0
- agrigee_lite_client-3.4.1/tests/test_satellite_dates_match_server.py +50 -0
- agrigee_lite_client-3.4.1/tests/test_validation.py +100 -0
- agrigee_lite_client-3.4.1/tests/test_version_matches_server.py +41 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
docs/source
|
|
2
|
+
|
|
3
|
+
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
|
|
4
|
+
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
MANIFEST
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
# Usually these files are written by a python script from a template
|
|
35
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
36
|
+
*.manifest
|
|
37
|
+
*.spec
|
|
38
|
+
|
|
39
|
+
# Installer logs
|
|
40
|
+
pip-log.txt
|
|
41
|
+
pip-delete-this-directory.txt
|
|
42
|
+
|
|
43
|
+
# Unit test / coverage reports
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.nox/
|
|
47
|
+
.coverage
|
|
48
|
+
.coverage.*
|
|
49
|
+
.cache
|
|
50
|
+
nosetests.xml
|
|
51
|
+
coverage.xml
|
|
52
|
+
*.cover
|
|
53
|
+
*.py,cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
cover/
|
|
57
|
+
|
|
58
|
+
# Translations
|
|
59
|
+
*.mo
|
|
60
|
+
*.pot
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
local_settings.py
|
|
65
|
+
db.sqlite3
|
|
66
|
+
db.sqlite3-journal
|
|
67
|
+
|
|
68
|
+
# Flask stuff:
|
|
69
|
+
instance/
|
|
70
|
+
.webassets-cache
|
|
71
|
+
|
|
72
|
+
# Scrapy stuff:
|
|
73
|
+
.scrapy
|
|
74
|
+
|
|
75
|
+
# Sphinx documentation
|
|
76
|
+
docs/_build/
|
|
77
|
+
|
|
78
|
+
# PyBuilder
|
|
79
|
+
.pybuilder/
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
90
|
+
__pypackages__/
|
|
91
|
+
|
|
92
|
+
# Celery stuff
|
|
93
|
+
celerybeat-schedule
|
|
94
|
+
celerybeat.pid
|
|
95
|
+
|
|
96
|
+
# SageMath parsed files
|
|
97
|
+
*.sage.py
|
|
98
|
+
|
|
99
|
+
# Environments
|
|
100
|
+
.env
|
|
101
|
+
.venv
|
|
102
|
+
env/
|
|
103
|
+
venv/
|
|
104
|
+
ENV/
|
|
105
|
+
env.bak/
|
|
106
|
+
venv.bak/
|
|
107
|
+
|
|
108
|
+
# Spyder project settings
|
|
109
|
+
.spyderproject
|
|
110
|
+
.spyproject
|
|
111
|
+
|
|
112
|
+
# Rope project settings
|
|
113
|
+
.ropeproject
|
|
114
|
+
|
|
115
|
+
# mkdocs documentation
|
|
116
|
+
/site
|
|
117
|
+
|
|
118
|
+
# mypy
|
|
119
|
+
.mypy_cache/
|
|
120
|
+
.dmypy.json
|
|
121
|
+
dmypy.json
|
|
122
|
+
|
|
123
|
+
# Pyre type checker
|
|
124
|
+
.pyre/
|
|
125
|
+
|
|
126
|
+
# pytype static type analyzer
|
|
127
|
+
.pytype/
|
|
128
|
+
|
|
129
|
+
# Cython debug symbols
|
|
130
|
+
cython_debug/
|
|
131
|
+
|
|
132
|
+
# Vscode config files
|
|
133
|
+
.vscode/
|
|
134
|
+
|
|
135
|
+
# PyCharm
|
|
136
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
137
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
138
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
139
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
140
|
+
#.idea/
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Specific OS junk files
|
|
145
|
+
*.DS_Store
|
|
146
|
+
core.*
|
|
147
|
+
*:Zone.Identifier
|
|
148
|
+
|
|
149
|
+
*.parquet
|
|
150
|
+
*.npz
|
|
151
|
+
*.csv
|
|
152
|
+
|
|
153
|
+
data/
|
|
154
|
+
!data/sample.parquet
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
*.pdf
|
|
158
|
+
*.html
|
|
159
|
+
*.zip
|
|
160
|
+
*.npg
|
|
161
|
+
aria2.session
|
|
162
|
+
# pixi environments
|
|
163
|
+
.pixi/*
|
|
164
|
+
!.pixi/config.toml
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
## AGENTS (compact LLM-oriented) — agrigee_lite_client
|
|
2
|
+
|
|
3
|
+
Subproject inside the AgriGEE.lite monorepo. See `../AGENTS.md` for the
|
|
4
|
+
server; this file covers only the HTTP client. Read `SPECS.md` in full
|
|
5
|
+
before touching this code — the design decisions (naive on purpose, no
|
|
6
|
+
install extras, geopandas optional-only) are already made and justified
|
|
7
|
+
there; don't reopen them without a new reason.
|
|
8
|
+
|
|
9
|
+
Run checks:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd agrigee_lite_client
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
pytest
|
|
15
|
+
ruff check .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Key rules (short)
|
|
19
|
+
|
|
20
|
+
- `geopandas` is never a hard dependency, but `multiple_sits()` accepts it as
|
|
21
|
+
input via lazy detection (`_geo_compat.py`: `try: import geopandas` inside
|
|
22
|
+
the function, never at module top-level). A top-level `import geopandas`
|
|
23
|
+
anywhere in the package is a bug — callers who only use `sits()`/geopolars/
|
|
24
|
+
polars must never pay that import. `geopandas` as a **test-only**
|
|
25
|
+
dependency (`test_geoparquet.py`, `test_geo_compat.py`) is fine.
|
|
26
|
+
- Fixed dependencies, no extras: `geopolars`, `shapely`, `httpx`. Do not add
|
|
27
|
+
`[async]`, `[shapely]`, or any optional install variant — explicit user
|
|
28
|
+
decision, don't reintroduce.
|
|
29
|
+
- Version always matches the server's (`../pyproject.toml`). Every server
|
|
30
|
+
version bump gets the same bump here, same commit.
|
|
31
|
+
`tests/test_version_matches_server.py` breaks CI on drift.
|
|
32
|
+
- Sync and async are deliberately duplicated code (`_client.py` /
|
|
33
|
+
`_async_client.py`) — do not build a sync/async unification layer (e.g.
|
|
34
|
+
`unasync`) on top. `_get.py` already holds everything that's genuinely
|
|
35
|
+
shared (payload building, response parsing); only I/O (`httpx` calls,
|
|
36
|
+
`sleep`/`asyncio.sleep`) is duplicated.
|
|
37
|
+
- `get.sits()` is sugar over `get.multiple_sits()`, not a separate endpoint —
|
|
38
|
+
don't give it its own HTTP path without reviewing SPECS.md §6 and §11
|
|
39
|
+
first (a synchronous `/sits/single/file` server endpoint is explicitly
|
|
40
|
+
deferred future work, don't implement it preemptively).
|
|
41
|
+
- GeoParquet metadata is hand-built (`_geoparquet.py`) via
|
|
42
|
+
`pyarrow.Table.replace_schema_metadata`, because `geopolars` 0.1.0-alpha.4
|
|
43
|
+
doesn't write the `"geo"` key on its own (`GeoDataFrame.write_parquet` is
|
|
44
|
+
the plain `polars.DataFrame.write_parquet`). Reconfirm that behavior on
|
|
45
|
+
the installed `geopolars` version before changing this approach — it may
|
|
46
|
+
have changed.
|
|
47
|
+
- CRS is always `EPSG:4326`/`OGC:CRS84` — the client never reprojects. Don't
|
|
48
|
+
add `pyproj`/reprojection without discussing first (breaks the minimal-
|
|
49
|
+
dependency promise). A `geopandas.GeoDataFrame` with a different CRS is
|
|
50
|
+
rejected with `ValueError`, not reprojected.
|
|
51
|
+
- Don't add configurable retry/backoff, local caching, authentication, a
|
|
52
|
+
CLI, or image decoding (`numpy`/`tifffile`/`rasterio`) without updating
|
|
53
|
+
SPECS.md §10 first — explicit non-goals of this v1, not oversights.
|
|
54
|
+
- **No new server endpoint just to support this client.** One was proposed
|
|
55
|
+
(`POST /satellites/date_range`) and rejected — the user didn't want the
|
|
56
|
+
server growing surface area purely for the client's benefit. `GET
|
|
57
|
+
/version` is the one exception, added specifically because `/health`
|
|
58
|
+
already has consumers and must not change shape. Prefer extending an
|
|
59
|
+
existing endpoint over adding a new one; if that's not possible, ask
|
|
60
|
+
before adding one.
|
|
61
|
+
- `_satellite_dates.py` is generated, not hand-edited. Regenerate with:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python scripts/generate_satellite_dates.py > agrigee_lite_client/_satellite_dates.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
(needs `agrigee_lite` importable — use the `all-features` pixi env)
|
|
68
|
+
whenever a satellite is added/removed or its range changes server-side,
|
|
69
|
+
and bump both packages' version together (SPECS.md §2.1).
|
|
70
|
+
`test_satellite_dates_match_server.py` catches drift automatically
|
|
71
|
+
whenever both packages are installed side by side, but doesn't replace
|
|
72
|
+
regenerating after a real server-side change.
|
|
73
|
+
- `pl.col(...).cast(pl.Datetime, strict=False)` silently nulls a `Utf8`
|
|
74
|
+
column instead of parsing it, as of polars 1.43 — it used to parse; that
|
|
75
|
+
behavior is deprecated/gone, `str.to_datetime(strict=False)` is the
|
|
76
|
+
replacement. Bit this code once (every row got silently dropped).
|
|
77
|
+
`_validation.py::_as_datetime_expr` checks dtype and picks the right one —
|
|
78
|
+
don't copy the server's `sanitize_and_prepare_input_gdf` pattern
|
|
79
|
+
(`agrigee_lite/get/sits.py`) verbatim; it only works there because the
|
|
80
|
+
column already arrives converted via `pandas.to_datetime`.
|
|
81
|
+
|
|
82
|
+
## Tests
|
|
83
|
+
|
|
84
|
+
- `test_geoparquet.py` — no network, validates bytes against
|
|
85
|
+
`geopandas.read_parquet`.
|
|
86
|
+
- `test_validation.py`, `test_geo_compat.py`, `test_satellite_dates.py` — no
|
|
87
|
+
network, test `_validation.py`/`_geo_compat.py`/`_satellite_dates.py` in
|
|
88
|
+
isolation.
|
|
89
|
+
- `test_satellite_dates_match_server.py` — only runs when `agrigee_lite` is
|
|
90
|
+
importable (`pytest.importorskip`); this is what actually catches drift
|
|
91
|
+
between the static table and the server's `REGISTRY`.
|
|
92
|
+
- `test_client_mocked.py` / `test_async_client_mocked.py` — no network, use
|
|
93
|
+
`httpx.MockTransport` + `tests/_fake_server.py` (a stateful fake of the
|
|
94
|
+
real API: health, version, satellites, job lifecycle). Add new scenarios
|
|
95
|
+
here, not against a real server.
|
|
96
|
+
- `test_client_against_live_api.py` — only runs with `AGRIGEE_TEST_API_URL`
|
|
97
|
+
set; not part of the default test run (depends on network + the server's
|
|
98
|
+
Earth Engine credentials).
|
|
99
|
+
- `test_version_matches_server.py` — always runs, the version lock.
|
|
100
|
+
|
|
101
|
+
## Docker Hub
|
|
102
|
+
|
|
103
|
+
This subproject has no image of its own — it's consumed by whoever runs
|
|
104
|
+
`pip install agrigee_lite_client`. The relevant Docker image is the
|
|
105
|
+
server's (`mateuspinto/agrigee-lite`, see `../AGENTS.md`).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agrigee_lite_client
|
|
3
|
+
Version: 3.4.1
|
|
4
|
+
Summary: Thin HTTP client for the AgriGEE.lite REST API — no Earth Engine, no GDAL, no DuckDB.
|
|
5
|
+
Project-URL: Homepage, https://mateuspinto.github.io/agrigee_lite/
|
|
6
|
+
Project-URL: Repository, https://github.com/mateuspinto/agrigee_lite
|
|
7
|
+
Project-URL: Documentation, https://mateuspinto.github.io/agrigee_lite/
|
|
8
|
+
Author-email: Mateus Pinto da Silva <mateus.p.silva@ufv.br>
|
|
9
|
+
Keywords: python
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: <4.0,>=3.10
|
|
19
|
+
Requires-Dist: geopolars==0.1.0a4
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: shapely>=2.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: geopandas>=1.0.1; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.11.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# agrigee_lite_client
|
|
29
|
+
|
|
30
|
+
Thin HTTP client for the [`agrigee_lite[api]`](../README.md) server. Doesn't
|
|
31
|
+
install Earth Engine, GDAL/PROJ, or DuckDB — it only knows how to build a
|
|
32
|
+
request, upload/download bytes, and talk to `/jobs`. See
|
|
33
|
+
[`SPECS.md`](SPECS.md) for the full design and the reasoning behind it.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install agrigee_lite_client
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
One fixed dependency set, always — `geopolars`, `shapely`, and `httpx`.
|
|
42
|
+
There's no install variation (no `[async]`, `[shapely]`, etc. extras): the
|
|
43
|
+
same install already covers both sync and async use.
|
|
44
|
+
|
|
45
|
+
## Quickstart — sync
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from agrigee_lite_client import AgriGEEClient
|
|
49
|
+
from shapely.geometry import Polygon
|
|
50
|
+
|
|
51
|
+
geometry = Polygon([(-56.42, -11.20), (-56.41, -11.20), (-56.41, -11.19)])
|
|
52
|
+
|
|
53
|
+
with AgriGEEClient("http://192.168.3.204:8100") as client:
|
|
54
|
+
# a single geometry — naive: internally becomes a 1-row multiple_sits
|
|
55
|
+
df = client.get.sits(geometry, "2023-01-01", "2023-06-01", satellite="Sentinel2")
|
|
56
|
+
|
|
57
|
+
# multiple geometries — uploads a GeoParquet, polls the job, downloads the result parquet
|
|
58
|
+
import polars as pl
|
|
59
|
+
from agrigee_lite_client._geoparquet import encode_geometries
|
|
60
|
+
|
|
61
|
+
gdf = pl.DataFrame({
|
|
62
|
+
"start_date": ["2023-01-01", "2023-01-01"],
|
|
63
|
+
"end_date": ["2023-06-01", "2023-06-01"],
|
|
64
|
+
"geometry": encode_geometries([geometry, geometry]),
|
|
65
|
+
})
|
|
66
|
+
df = client.get.multiple_sits(gdf, satellite="Sentinel2", reducers=["mean", "std"])
|
|
67
|
+
|
|
68
|
+
# geopandas.GeoDataFrame also works (geopandas is optional — only
|
|
69
|
+
# imported if you actually pass one; it's not installed alongside the client)
|
|
70
|
+
import geopandas as gpd
|
|
71
|
+
|
|
72
|
+
gdf_geopandas = gpd.GeoDataFrame(
|
|
73
|
+
{"start_date": ["2023-01-01"], "end_date": ["2023-06-01"]},
|
|
74
|
+
geometry=[geometry],
|
|
75
|
+
crs="EPSG:4326",
|
|
76
|
+
)
|
|
77
|
+
df = client.get.multiple_sits(gdf_geopandas, satellite="Sentinel2")
|
|
78
|
+
|
|
79
|
+
# image — returns an in-memory zipfile.ZipFile, nothing decoded
|
|
80
|
+
zf = client.get.image(geometry, "2023-01-01", "2023-06-01", satellite="Sentinel2")
|
|
81
|
+
zf.extractall("out/")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quickstart — async
|
|
85
|
+
|
|
86
|
+
Same surface, `AsyncAgriGEEClient`:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from agrigee_lite_client import AsyncAgriGEEClient
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
async with AsyncAgriGEEClient("http://192.168.3.204:8100") as client:
|
|
94
|
+
df = await client.get.sits(geometry, "2023-01-01", "2023-06-01", satellite="Sentinel2")
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Validation before anything leaves your machine
|
|
100
|
+
|
|
101
|
+
`sits()`/`multiple_sits()` check that the requested dates actually intersect
|
|
102
|
+
the satellite's operating period — the same check the server does — and
|
|
103
|
+
drop (with a warning) rows that don't, raising `ValueError` if none are
|
|
104
|
+
left. All of this happens **before** building the GeoParquet or opening a
|
|
105
|
+
connection, using a static table embedded in the client (no network call).
|
|
106
|
+
An unknown satellite name is also caught here (`AgriGEEUnknownSatelliteError`).
|
|
107
|
+
|
|
108
|
+
Since that table is a copy of what the server knows, the client checks that
|
|
109
|
+
it's running against the same server version (via `GET /version`) before
|
|
110
|
+
trusting it — a mismatch raises `AgriGEEVersionMismatchError` instead of
|
|
111
|
+
validating against possibly stale data.
|
|
112
|
+
|
|
113
|
+
## What this client does NOT do (on purpose)
|
|
114
|
+
|
|
115
|
+
- No CRS reprojection — input geometries must be in WGS84 (EPSG:4326 /
|
|
116
|
+
OGC:CRS84). If you pass a `geopandas.GeoDataFrame` with a different CRS,
|
|
117
|
+
the client rejects it with a clear error instead of reprojecting.
|
|
118
|
+
- No local caching, no sophisticated retry/backoff — the cache already lives
|
|
119
|
+
on the server; the only "retry" here is `httpx`'s transport timeout and a
|
|
120
|
+
simple poll loop against `GET /jobs/{id}`.
|
|
121
|
+
- No image decoding — `get.image(...)` returns a raw `zipfile.ZipFile`, no
|
|
122
|
+
`numpy`/`tifffile`/`rasterio`.
|
|
123
|
+
- `get.sits(...)` isn't a separate fast path: it's sugar over
|
|
124
|
+
`get.multiple_sits(...)` with a single row, so it pays the job+poll+
|
|
125
|
+
download cost even for one geometry. See `SPECS.md §6`.
|
|
126
|
+
|
|
127
|
+
## Versioning
|
|
128
|
+
|
|
129
|
+
This package's version always matches `agrigee_lite`'s (the server), same
|
|
130
|
+
commit — they aren't versioned independently. A test
|
|
131
|
+
(`tests/test_version_matches_server.py`) enforces this in CI.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd agrigee_lite_client
|
|
137
|
+
pip install -e ".[dev]"
|
|
138
|
+
pytest
|
|
139
|
+
ruff check .
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`tests/test_client_against_live_api.py` only runs if `AGRIGEE_TEST_API_URL`
|
|
143
|
+
is set — the rest (`test_client_mocked.py`, `test_async_client_mocked.py`)
|
|
144
|
+
use `httpx.MockTransport` and need no network.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# agrigee_lite_client
|
|
2
|
+
|
|
3
|
+
Thin HTTP client for the [`agrigee_lite[api]`](../README.md) server. Doesn't
|
|
4
|
+
install Earth Engine, GDAL/PROJ, or DuckDB — it only knows how to build a
|
|
5
|
+
request, upload/download bytes, and talk to `/jobs`. See
|
|
6
|
+
[`SPECS.md`](SPECS.md) for the full design and the reasoning behind it.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install agrigee_lite_client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
One fixed dependency set, always — `geopolars`, `shapely`, and `httpx`.
|
|
15
|
+
There's no install variation (no `[async]`, `[shapely]`, etc. extras): the
|
|
16
|
+
same install already covers both sync and async use.
|
|
17
|
+
|
|
18
|
+
## Quickstart — sync
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from agrigee_lite_client import AgriGEEClient
|
|
22
|
+
from shapely.geometry import Polygon
|
|
23
|
+
|
|
24
|
+
geometry = Polygon([(-56.42, -11.20), (-56.41, -11.20), (-56.41, -11.19)])
|
|
25
|
+
|
|
26
|
+
with AgriGEEClient("http://192.168.3.204:8100") as client:
|
|
27
|
+
# a single geometry — naive: internally becomes a 1-row multiple_sits
|
|
28
|
+
df = client.get.sits(geometry, "2023-01-01", "2023-06-01", satellite="Sentinel2")
|
|
29
|
+
|
|
30
|
+
# multiple geometries — uploads a GeoParquet, polls the job, downloads the result parquet
|
|
31
|
+
import polars as pl
|
|
32
|
+
from agrigee_lite_client._geoparquet import encode_geometries
|
|
33
|
+
|
|
34
|
+
gdf = pl.DataFrame({
|
|
35
|
+
"start_date": ["2023-01-01", "2023-01-01"],
|
|
36
|
+
"end_date": ["2023-06-01", "2023-06-01"],
|
|
37
|
+
"geometry": encode_geometries([geometry, geometry]),
|
|
38
|
+
})
|
|
39
|
+
df = client.get.multiple_sits(gdf, satellite="Sentinel2", reducers=["mean", "std"])
|
|
40
|
+
|
|
41
|
+
# geopandas.GeoDataFrame also works (geopandas is optional — only
|
|
42
|
+
# imported if you actually pass one; it's not installed alongside the client)
|
|
43
|
+
import geopandas as gpd
|
|
44
|
+
|
|
45
|
+
gdf_geopandas = gpd.GeoDataFrame(
|
|
46
|
+
{"start_date": ["2023-01-01"], "end_date": ["2023-06-01"]},
|
|
47
|
+
geometry=[geometry],
|
|
48
|
+
crs="EPSG:4326",
|
|
49
|
+
)
|
|
50
|
+
df = client.get.multiple_sits(gdf_geopandas, satellite="Sentinel2")
|
|
51
|
+
|
|
52
|
+
# image — returns an in-memory zipfile.ZipFile, nothing decoded
|
|
53
|
+
zf = client.get.image(geometry, "2023-01-01", "2023-06-01", satellite="Sentinel2")
|
|
54
|
+
zf.extractall("out/")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart — async
|
|
58
|
+
|
|
59
|
+
Same surface, `AsyncAgriGEEClient`:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
from agrigee_lite_client import AsyncAgriGEEClient
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
async with AsyncAgriGEEClient("http://192.168.3.204:8100") as client:
|
|
67
|
+
df = await client.get.sits(geometry, "2023-01-01", "2023-06-01", satellite="Sentinel2")
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Validation before anything leaves your machine
|
|
73
|
+
|
|
74
|
+
`sits()`/`multiple_sits()` check that the requested dates actually intersect
|
|
75
|
+
the satellite's operating period — the same check the server does — and
|
|
76
|
+
drop (with a warning) rows that don't, raising `ValueError` if none are
|
|
77
|
+
left. All of this happens **before** building the GeoParquet or opening a
|
|
78
|
+
connection, using a static table embedded in the client (no network call).
|
|
79
|
+
An unknown satellite name is also caught here (`AgriGEEUnknownSatelliteError`).
|
|
80
|
+
|
|
81
|
+
Since that table is a copy of what the server knows, the client checks that
|
|
82
|
+
it's running against the same server version (via `GET /version`) before
|
|
83
|
+
trusting it — a mismatch raises `AgriGEEVersionMismatchError` instead of
|
|
84
|
+
validating against possibly stale data.
|
|
85
|
+
|
|
86
|
+
## What this client does NOT do (on purpose)
|
|
87
|
+
|
|
88
|
+
- No CRS reprojection — input geometries must be in WGS84 (EPSG:4326 /
|
|
89
|
+
OGC:CRS84). If you pass a `geopandas.GeoDataFrame` with a different CRS,
|
|
90
|
+
the client rejects it with a clear error instead of reprojecting.
|
|
91
|
+
- No local caching, no sophisticated retry/backoff — the cache already lives
|
|
92
|
+
on the server; the only "retry" here is `httpx`'s transport timeout and a
|
|
93
|
+
simple poll loop against `GET /jobs/{id}`.
|
|
94
|
+
- No image decoding — `get.image(...)` returns a raw `zipfile.ZipFile`, no
|
|
95
|
+
`numpy`/`tifffile`/`rasterio`.
|
|
96
|
+
- `get.sits(...)` isn't a separate fast path: it's sugar over
|
|
97
|
+
`get.multiple_sits(...)` with a single row, so it pays the job+poll+
|
|
98
|
+
download cost even for one geometry. See `SPECS.md §6`.
|
|
99
|
+
|
|
100
|
+
## Versioning
|
|
101
|
+
|
|
102
|
+
This package's version always matches `agrigee_lite`'s (the server), same
|
|
103
|
+
commit — they aren't versioned independently. A test
|
|
104
|
+
(`tests/test_version_matches_server.py`) enforces this in CI.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd agrigee_lite_client
|
|
110
|
+
pip install -e ".[dev]"
|
|
111
|
+
pytest
|
|
112
|
+
ruff check .
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`tests/test_client_against_live_api.py` only runs if `AGRIGEE_TEST_API_URL`
|
|
116
|
+
is set — the rest (`test_client_mocked.py`, `test_async_client_mocked.py`)
|
|
117
|
+
use `httpx.MockTransport` and need no network.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Thin HTTP client for the AgriGEE.lite REST API. See SPECS.md."""
|
|
2
|
+
|
|
3
|
+
from agrigee_lite_client._async_client import AsyncAgriGEEClient
|
|
4
|
+
from agrigee_lite_client._client import AgriGEEClient
|
|
5
|
+
from agrigee_lite_client._exceptions import (
|
|
6
|
+
AgriGEEClientError,
|
|
7
|
+
AgriGEEHTTPError,
|
|
8
|
+
AgriGEEJobError,
|
|
9
|
+
AgriGEEJobTimeoutError,
|
|
10
|
+
AgriGEEUnknownSatelliteError,
|
|
11
|
+
AgriGEEVersionMismatchError,
|
|
12
|
+
)
|
|
13
|
+
from agrigee_lite_client._version import __version__
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgriGEEClient",
|
|
17
|
+
"AgriGEEClientError",
|
|
18
|
+
"AgriGEEHTTPError",
|
|
19
|
+
"AgriGEEJobError",
|
|
20
|
+
"AgriGEEJobTimeoutError",
|
|
21
|
+
"AgriGEEUnknownSatelliteError",
|
|
22
|
+
"AgriGEEVersionMismatchError",
|
|
23
|
+
"AsyncAgriGEEClient",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|