ee-client 3.1.2__tar.gz → 3.2.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.
- {ee_client-3.1.2 → ee_client-3.2.1}/PKG-INFO +1 -1
- {ee_client-3.1.2 → ee_client-3.2.1}/ee_client.egg-info/PKG-INFO +1 -1
- {ee_client-3.1.2 → ee_client-3.2.1}/ee_client.egg-info/SOURCES.txt +1 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/__init__.py +1 -1
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/cache.py +37 -33
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/export/image.py +31 -1
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/interfaces/export.py +9 -2
- {ee_client-3.1.2 → ee_client-3.2.1}/pyproject.toml +2 -2
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_cache.py +73 -0
- ee_client-3.2.1/tests/test_export_image_pyramiding.py +96 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/LICENSE +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/README.rst +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/ee_client.egg-info/dependency_links.txt +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/ee_client.egg-info/requires.txt +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/ee_client.egg-info/top_level.txt +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/client.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/credential_mixin.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/data.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/exceptions.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/export/__init__.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/export/table.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/helpers.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/interfaces/__init__.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/interfaces/operations.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/interfaces/tasks.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/loopstate.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/models.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/providers.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/sepal_credential_mixin.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/eeclient/tasks.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/setup.cfg +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_agnostic_auth_integration.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_client.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_credential_mixin_close.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_data.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_export_image.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_export_table.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_factories.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_integration_get_assets.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_loop_scoping.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_models.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_providers.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_resolution.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_tasks.py +0 -0
- {ee_client-3.1.2 → ee_client-3.2.1}/tests/test_tls_verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ee-client
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: extends the capabilities of the earthengine-api by providing custom session management and client interactions
|
|
5
5
|
Author-email: Daniel Guerrero <dfgm2006@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dfguerrerom/ee-client
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ee-client
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: extends the capabilities of the earthengine-api by providing custom session management and client interactions
|
|
5
5
|
Author-email: Daniel Guerrero <dfgm2006@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dfguerrerom/ee-client
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import time
|
|
@@ -19,6 +20,7 @@ class CacheEntry:
|
|
|
19
20
|
self.error = error
|
|
20
21
|
self.task = task
|
|
21
22
|
self.timestamp = time.time()
|
|
23
|
+
self.waiters = 0
|
|
22
24
|
|
|
23
25
|
def is_expired(self, ttl: float) -> bool:
|
|
24
26
|
"""Check if the cache entry has exceeded its TTL."""
|
|
@@ -65,49 +67,51 @@ class ResponseCache:
|
|
|
65
67
|
Returns:
|
|
66
68
|
The cached or freshly fetched result
|
|
67
69
|
"""
|
|
68
|
-
# Check cache and get task reference if needed
|
|
69
70
|
async with self._lock:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# Valid cached result - return it
|
|
71
|
+
entry = self._cache.get(key)
|
|
72
|
+
if entry is not None and entry.task is not None and entry.task.done():
|
|
73
|
+
# Its starter was cancelled before it could settle the entry.
|
|
74
|
+
self._settle(key, entry)
|
|
75
|
+
entry = self._cache.get(key)
|
|
76
|
+
if entry is not None and entry.task is None:
|
|
77
|
+
if not entry.is_expired(self.ttl):
|
|
78
78
|
self._cache.move_to_end(key)
|
|
79
79
|
return entry.get_result()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
in_flight_task = None
|
|
84
|
-
else:
|
|
85
|
-
in_flight_task = None
|
|
86
|
-
|
|
87
|
-
# No in-flight task - create a new one
|
|
88
|
-
if in_flight_task is None:
|
|
80
|
+
del self._cache[key]
|
|
81
|
+
entry = None
|
|
82
|
+
if entry is None:
|
|
89
83
|
task = asyncio.create_task(fetch_func(*args, **kwargs))
|
|
90
|
-
|
|
84
|
+
entry = CacheEntry(task=task)
|
|
85
|
+
self._cache[key] = entry
|
|
86
|
+
task.add_done_callback(lambda _: self._settle(key, entry))
|
|
91
87
|
|
|
92
88
|
# LRU eviction
|
|
93
89
|
while len(self._cache) > self.max_size:
|
|
94
90
|
self._cache.popitem(last=False)
|
|
95
|
-
|
|
96
|
-
# Use the existing in-flight task
|
|
97
|
-
task = in_flight_task
|
|
91
|
+
entry.waiters += 1
|
|
98
92
|
|
|
99
|
-
#
|
|
93
|
+
# Shielded, so one cancelled caller does not cancel the fetch the others
|
|
94
|
+
# (and the cache) wait on. The last caller to leave cancels it instead: a
|
|
95
|
+
# fetch must not outlive its callers, nor its loop.
|
|
100
96
|
try:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
return await asyncio.shield(entry.task)
|
|
98
|
+
finally:
|
|
99
|
+
entry.waiters -= 1
|
|
100
|
+
if entry.waiters == 0 and not entry.task.done():
|
|
101
|
+
entry.task.cancel()
|
|
102
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
103
|
+
await asyncio.wait({entry.task})
|
|
104
|
+
|
|
105
|
+
def _settle(self, key: str, entry: CacheEntry) -> None:
|
|
106
|
+
"""Keep what a finished fetch produced, or drop it when it was cancelled."""
|
|
107
|
+
if self._cache.get(key) is not entry or not entry.task.done():
|
|
108
|
+
return
|
|
109
|
+
if entry.task.cancelled():
|
|
110
|
+
del self._cache[key]
|
|
111
|
+
elif entry.task.exception() is not None:
|
|
112
|
+
self._cache[key] = CacheEntry(error=entry.task.exception())
|
|
113
|
+
else:
|
|
114
|
+
self._cache[key] = CacheEntry(value=entry.task.result())
|
|
111
115
|
|
|
112
116
|
def clear(self):
|
|
113
117
|
"""Clear all cached entries."""
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
2
|
+
from typing import TYPE_CHECKING, Dict, Optional, Union
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
4
4
|
from pydantic.alias_generators import to_camel
|
|
5
5
|
|
|
@@ -66,8 +66,34 @@ class DriveOptions(BaseExportModel):
|
|
|
66
66
|
# See the api: https://developers.google.com/earth-engine/reference/rest/v1alpha/ImageFileExportOptions
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
class PyramidingPolicy(str, Enum):
|
|
70
|
+
"""Reducer that builds an asset's overview levels.
|
|
71
|
+
|
|
72
|
+
The server default is ``MEAN``, which averages class codes across every
|
|
73
|
+
overview of a categorical image and renders it wrong below native zoom.
|
|
74
|
+
Such images want ``MODE`` or ``SAMPLE``.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
MEAN = "MEAN"
|
|
78
|
+
SAMPLE = "SAMPLE"
|
|
79
|
+
MIN = "MIN"
|
|
80
|
+
MAX = "MAX"
|
|
81
|
+
MODE = "MODE"
|
|
82
|
+
MEDIAN = "MEDIAN"
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def _missing_(cls, value):
|
|
86
|
+
# ``ee.batch`` upper-cases whatever the caller passes, so callers are
|
|
87
|
+
# used to writing "mode".
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
return cls.__members__.get(value.upper())
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
69
93
|
class AssetOptions(BaseExportModel):
|
|
70
94
|
earth_engine_destination: EarthEngineDestination
|
|
95
|
+
pyramiding_policy: Optional[PyramidingPolicy] = None
|
|
96
|
+
pyramiding_policy_overrides: Optional[Dict[str, PyramidingPolicy]] = None
|
|
71
97
|
|
|
72
98
|
|
|
73
99
|
class GridDimensions(BaseModel):
|
|
@@ -267,6 +293,8 @@ async def image_to_asset_async(
|
|
|
267
293
|
scale: Optional[float] = None,
|
|
268
294
|
crs: Optional[str] = None,
|
|
269
295
|
crs_transform: Optional[AffineTransform] = None,
|
|
296
|
+
pyramiding_policy: Optional[PyramidingPolicy] = None,
|
|
297
|
+
pyramiding_policy_overrides: Optional[Dict[str, PyramidingPolicy]] = None,
|
|
270
298
|
) -> Task:
|
|
271
299
|
"""Abstracts the export of an image to Earth Engine Asset.
|
|
272
300
|
|
|
@@ -275,6 +303,8 @@ async def image_to_asset_async(
|
|
|
275
303
|
"""
|
|
276
304
|
asset_options = AssetOptions(
|
|
277
305
|
earth_engine_destination=EarthEngineDestination(name=asset_id),
|
|
306
|
+
pyramiding_policy=pyramiding_policy,
|
|
307
|
+
pyramiding_policy_overrides=pyramiding_policy_overrides,
|
|
278
308
|
)
|
|
279
309
|
|
|
280
310
|
return await _export_image(
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
from typing import Optional, Protocol, Union
|
|
2
|
-
from eeclient.export.image import
|
|
1
|
+
from typing import Any, Dict, Optional, Protocol, Union
|
|
2
|
+
from eeclient.export.image import (
|
|
3
|
+
AffineTransform,
|
|
4
|
+
ImageFileFormat,
|
|
5
|
+
PixelGrid,
|
|
6
|
+
PyramidingPolicy,
|
|
7
|
+
)
|
|
3
8
|
from eeclient.export.table import TableFileFormat
|
|
4
9
|
|
|
5
10
|
|
|
@@ -18,6 +23,8 @@ class ExportProtocol(Protocol):
|
|
|
18
23
|
scale: Optional[float] = ...,
|
|
19
24
|
crs: Optional[str] = ...,
|
|
20
25
|
crs_transform: Optional[AffineTransform] = ...,
|
|
26
|
+
pyramiding_policy: Optional[PyramidingPolicy] = ...,
|
|
27
|
+
pyramiding_policy_overrides: Optional[Dict[str, PyramidingPolicy]] = ...,
|
|
21
28
|
) -> dict: ...
|
|
22
29
|
|
|
23
30
|
async def image_to_drive_async(
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ee-client"
|
|
7
|
-
version = "3.1
|
|
7
|
+
version = "3.2.1"
|
|
8
8
|
description = "extends the capabilities of the earthengine-api by providing custom session management and client interactions"
|
|
9
9
|
readme = { file = "README.rst", content-type = "text/x-rst" }
|
|
10
10
|
authors = [
|
|
@@ -66,7 +66,7 @@ branch = true
|
|
|
66
66
|
[tool.commitizen]
|
|
67
67
|
tag_format = "v$major.$minor.$patch$prerelease"
|
|
68
68
|
update_changelog_on_bump = false
|
|
69
|
-
version = "3.1
|
|
69
|
+
version = "3.2.1"
|
|
70
70
|
version_files = [
|
|
71
71
|
"pyproject.toml:version",
|
|
72
72
|
"eeclient/__init__.py:__version__",
|
|
@@ -250,3 +250,76 @@ async def test_get_assets_concurrent_deduplication():
|
|
|
250
250
|
assert all(len(r) == 1 for r in results)
|
|
251
251
|
assert all(r[0]["id"] == "test_id" for r in results)
|
|
252
252
|
assert call_count == 1
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# Cancellation
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _cancel(task):
|
|
259
|
+
task.cancel()
|
|
260
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@pytest.mark.asyncio
|
|
264
|
+
async def test_cache_cancelled_caller_leaves_no_stale_none():
|
|
265
|
+
"""A second call within the TTL must fetch again, not read a cancelled entry."""
|
|
266
|
+
cache = ResponseCache(ttl=10.0, max_size=100)
|
|
267
|
+
call_count = 0
|
|
268
|
+
|
|
269
|
+
async def fetch_data():
|
|
270
|
+
nonlocal call_count
|
|
271
|
+
call_count += 1
|
|
272
|
+
await asyncio.sleep(0.05)
|
|
273
|
+
return ["asset"]
|
|
274
|
+
|
|
275
|
+
key = cache.make_cache_key("folder")
|
|
276
|
+
first = asyncio.create_task(cache.get_or_fetch(key, fetch_data))
|
|
277
|
+
await asyncio.sleep(0.01)
|
|
278
|
+
await _cancel(first)
|
|
279
|
+
|
|
280
|
+
assert await cache.get_or_fetch(key, fetch_data) == ["asset"]
|
|
281
|
+
assert call_count == 2
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.mark.asyncio
|
|
285
|
+
async def test_cache_last_caller_to_leave_cancels_the_fetch():
|
|
286
|
+
"""A fetch nobody waits for must not run on, nor stay in the cache."""
|
|
287
|
+
cache = ResponseCache(ttl=10.0, max_size=100)
|
|
288
|
+
cancelled = asyncio.Event()
|
|
289
|
+
|
|
290
|
+
async def fetch_data():
|
|
291
|
+
try:
|
|
292
|
+
await asyncio.sleep(300)
|
|
293
|
+
except asyncio.CancelledError:
|
|
294
|
+
cancelled.set()
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
key = cache.make_cache_key("folder")
|
|
298
|
+
caller = asyncio.create_task(cache.get_or_fetch(key, fetch_data))
|
|
299
|
+
await asyncio.sleep(0.01)
|
|
300
|
+
await _cancel(caller)
|
|
301
|
+
|
|
302
|
+
assert cancelled.is_set()
|
|
303
|
+
assert key not in cache._cache
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.asyncio
|
|
307
|
+
async def test_cache_cancelled_caller_does_not_cancel_the_others():
|
|
308
|
+
"""Callers deduplicated onto one fetch must not lose it when one of them leaves."""
|
|
309
|
+
cache = ResponseCache(ttl=10.0, max_size=100)
|
|
310
|
+
call_count = 0
|
|
311
|
+
|
|
312
|
+
async def fetch_data():
|
|
313
|
+
nonlocal call_count
|
|
314
|
+
call_count += 1
|
|
315
|
+
await asyncio.sleep(0.05)
|
|
316
|
+
return ["asset"]
|
|
317
|
+
|
|
318
|
+
key = cache.make_cache_key("folder")
|
|
319
|
+
leaver = asyncio.create_task(cache.get_or_fetch(key, fetch_data))
|
|
320
|
+
stayer = asyncio.create_task(cache.get_or_fetch(key, fetch_data))
|
|
321
|
+
await asyncio.sleep(0.01)
|
|
322
|
+
await _cancel(leaver)
|
|
323
|
+
|
|
324
|
+
assert await asyncio.wait_for(stayer, timeout=5.0) == ["asset"]
|
|
325
|
+
assert call_count == 1
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from eeclient.export.image import (
|
|
7
|
+
AssetOptions,
|
|
8
|
+
EarthEngineDestination,
|
|
9
|
+
PyramidingPolicy,
|
|
10
|
+
image_to_asset_async,
|
|
11
|
+
)
|
|
12
|
+
from eeclient.interfaces.export import ExportProtocol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _asset_options(**kwargs):
|
|
16
|
+
return AssetOptions(
|
|
17
|
+
earth_engine_destination=EarthEngineDestination(name="projects/p/assets/x"),
|
|
18
|
+
**kwargs,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_asset_options_omit_pyramiding_by_default():
|
|
23
|
+
dumped = _asset_options().model_dump(by_alias=True, exclude_none=True)
|
|
24
|
+
assert "pyramidingPolicy" not in dumped
|
|
25
|
+
assert "pyramidingPolicyOverrides" not in dumped
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_asset_options_serialize_default_policy_as_camel_case():
|
|
29
|
+
dumped = _asset_options(pyramiding_policy="MODE").model_dump(
|
|
30
|
+
by_alias=True, exclude_none=True
|
|
31
|
+
)
|
|
32
|
+
assert dumped["pyramidingPolicy"] == "MODE"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_asset_options_serialize_per_band_overrides():
|
|
36
|
+
dumped = _asset_options(pyramiding_policy_overrides={"B1": "MIN"}).model_dump(
|
|
37
|
+
by_alias=True, exclude_none=True
|
|
38
|
+
)
|
|
39
|
+
assert dumped["pyramidingPolicyOverrides"] == {"B1": "MIN"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_asset_options_accept_lowercase_policy():
|
|
43
|
+
"""`ee.batch` upper-cases whatever the caller passes, so callers write "mode"."""
|
|
44
|
+
opts = _asset_options(
|
|
45
|
+
pyramiding_policy="mode", pyramiding_policy_overrides={"B1": "min"}
|
|
46
|
+
)
|
|
47
|
+
assert opts.pyramiding_policy is PyramidingPolicy.MODE
|
|
48
|
+
assert opts.pyramiding_policy_overrides == {"B1": PyramidingPolicy.MIN}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_asset_options_reject_unknown_policy():
|
|
52
|
+
with pytest.raises(ValueError):
|
|
53
|
+
_asset_options(pyramiding_policy="average")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_asset_options_reject_unknown_override_policy():
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
_asset_options(pyramiding_policy_overrides={"B1": "average"})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_image_to_asset_async_forwards_pyramiding_to_the_request():
|
|
63
|
+
image = MagicMock(name="image")
|
|
64
|
+
image._apply_crs_and_affine.return_value = (image, {}, False)
|
|
65
|
+
image._apply_selection_and_scale.return_value = (image, {})
|
|
66
|
+
|
|
67
|
+
client = MagicMock()
|
|
68
|
+
client.rest_call = AsyncMock(return_value={})
|
|
69
|
+
|
|
70
|
+
with (
|
|
71
|
+
patch("eeclient.export.image.serializer.encode", return_value={}),
|
|
72
|
+
patch("eeclient.export.image.Task.model_validate", return_value=MagicMock()),
|
|
73
|
+
):
|
|
74
|
+
await image_to_asset_async(
|
|
75
|
+
client=client,
|
|
76
|
+
image=image,
|
|
77
|
+
asset_id="projects/p/assets/x",
|
|
78
|
+
pyramiding_policy="mode",
|
|
79
|
+
pyramiding_policy_overrides={"B1": "min"},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Asserted through json, because `rest_call` hands `data` to httpx as
|
|
83
|
+
# `json=` -- a policy that stringified as "PyramidingPolicy.MODE" would
|
|
84
|
+
# still compare equal to "MODE" here as a str enum.
|
|
85
|
+
body = json.loads(json.dumps(client.rest_call.await_args.kwargs["data"]))
|
|
86
|
+
asset_options = body["assetExportOptions"]
|
|
87
|
+
assert asset_options["pyramidingPolicy"] == "MODE"
|
|
88
|
+
assert asset_options["pyramidingPolicyOverrides"] == {"B1": "MIN"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize("param", ["pyramiding_policy", "pyramiding_policy_overrides"])
|
|
92
|
+
def test_export_protocol_matches_the_implementation(param):
|
|
93
|
+
"""`session.export.*` dispatches through the Protocol, so it must agree."""
|
|
94
|
+
import inspect
|
|
95
|
+
|
|
96
|
+
assert param in inspect.signature(ExportProtocol.image_to_asset_async).parameters
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|