earthengine-api 1.6.11__py3-none-any.whl → 1.6.12__py3-none-any.whl
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.
- {earthengine_api-1.6.11.dist-info → earthengine_api-1.6.12.dist-info}/METADATA +1 -1
- {earthengine_api-1.6.11.dist-info → earthengine_api-1.6.12.dist-info}/RECORD +48 -46
- ee/__init__.py +5 -5
- ee/_cloud_api_utils.py +33 -10
- ee/_state.py +105 -0
- ee/apifunction.py +1 -1
- ee/apitestcase.py +15 -21
- ee/batch.py +1 -1
- ee/cli/commands.py +153 -63
- ee/cli/eecli.py +1 -1
- ee/cli/utils.py +25 -15
- ee/collection.py +27 -18
- ee/computedobject.py +5 -5
- ee/customfunction.py +3 -3
- ee/data.py +104 -210
- ee/ee_array.py +4 -2
- ee/ee_number.py +1 -1
- ee/ee_string.py +18 -26
- ee/ee_types.py +2 -2
- ee/element.py +1 -1
- ee/featurecollection.py +10 -7
- ee/filter.py +2 -2
- ee/geometry.py +20 -21
- ee/image.py +7 -12
- ee/imagecollection.py +3 -3
- ee/mapclient.py +9 -9
- ee/oauth.py +13 -6
- ee/tests/_cloud_api_utils_test.py +16 -0
- ee/tests/_helpers_test.py +9 -9
- ee/tests/_state_test.py +49 -0
- ee/tests/apifunction_test.py +5 -5
- ee/tests/batch_test.py +61 -50
- ee/tests/collection_test.py +13 -13
- ee/tests/data_test.py +65 -60
- ee/tests/dictionary_test.py +9 -9
- ee/tests/ee_number_test.py +32 -26
- ee/tests/ee_string_test.py +8 -0
- ee/tests/ee_test.py +37 -19
- ee/tests/element_test.py +2 -2
- ee/tests/feature_test.py +6 -6
- ee/tests/function_test.py +5 -5
- ee/tests/geometry_test.py +73 -51
- ee/tests/oauth_test.py +21 -2
- ee/tests/serializer_test.py +8 -8
- {earthengine_api-1.6.11.dist-info → earthengine_api-1.6.12.dist-info}/WHEEL +0 -0
- {earthengine_api-1.6.11.dist-info → earthengine_api-1.6.12.dist-info}/entry_points.txt +0 -0
- {earthengine_api-1.6.11.dist-info → earthengine_api-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {earthengine_api-1.6.11.dist-info → earthengine_api-1.6.12.dist-info}/top_level.txt +0 -0
ee/cli/commands.py
CHANGED
|
@@ -69,7 +69,7 @@ TYPE_STRING = 'string'
|
|
|
69
69
|
SYSTEM_TIME_START = 'system:time_start'
|
|
70
70
|
SYSTEM_TIME_END = 'system:time_end'
|
|
71
71
|
|
|
72
|
-
# A regex that parses properties of the form "[(type)]name=value".
|
|
72
|
+
# A regex that parses properties of the form "[(type)]name=value". The
|
|
73
73
|
# second, third, and fourth group are type, name, and number, respectively.
|
|
74
74
|
PROPERTY_RE = re.compile(r'(\(([^\)]*)\))?([^=]+)=(.*)')
|
|
75
75
|
|
|
@@ -193,6 +193,39 @@ def _cloud_timestamp_for_timestamp_ms(timestamp_ms: float) -> str:
|
|
|
193
193
|
return timestamp.replace(tzinfo=None).isoformat() + 'Z'
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def _datetime_from_cloud_timestamp(
|
|
197
|
+
cloud_timestamp: str | None,
|
|
198
|
+
) -> datetime.datetime:
|
|
199
|
+
"""Returns a datetime object for the given Cloud-formatted timestamp.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
cloud_timestamp: A timestamp string in the format 'YYYY-MM-DDTHH:MM:SS.mmmZ'
|
|
203
|
+
or 'YYYY-MM-DDTHH:MM:SSZ'. If None, returns the epoch.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A datetime object for the given Cloud-formatted timestamp. If the timestamp
|
|
207
|
+
is None, returns the epoch.
|
|
208
|
+
"""
|
|
209
|
+
if not cloud_timestamp:
|
|
210
|
+
return datetime.datetime.fromtimestamp(0)
|
|
211
|
+
# Replace 'Z' with the UTC offset +00:00 for fromisoformat compatibility.
|
|
212
|
+
return datetime.datetime.fromisoformat(cloud_timestamp.replace('Z', '+00:00'))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _format_cloud_timestamp(timestamp: str | None) -> str:
|
|
216
|
+
"""Returns a formatted datetime for the given Cloud-formatted timestamp.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
timestamp: A timestamp string in the format 'YYYY-MM-DDTHH:MM:SS.mmmZ'
|
|
220
|
+
or 'YYYY-MM-DDTHH:MM:SSZ'.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A formatted datetime string in the format 'YYYY-MM-DD HH:MM:SS'. If the
|
|
224
|
+
timestamp is None, returns the formatted epoch.
|
|
225
|
+
"""
|
|
226
|
+
return _datetime_from_cloud_timestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
|
227
|
+
|
|
228
|
+
|
|
196
229
|
def _parse_millis(millis: float) -> datetime.datetime:
|
|
197
230
|
return datetime.datetime.fromtimestamp(millis / 1000)
|
|
198
231
|
|
|
@@ -230,13 +263,34 @@ def _decode_date(string: str) -> Union[float, str]:
|
|
|
230
263
|
'Invalid value for property of type "date": "%s".' % string)
|
|
231
264
|
|
|
232
265
|
|
|
266
|
+
def _task_id_to_operation_name(task_id: str) -> str:
|
|
267
|
+
"""Converts a task ID to an operation name."""
|
|
268
|
+
# pylint: disable=protected-access
|
|
269
|
+
return ee._cloud_api_utils.convert_task_id_to_operation_name(
|
|
270
|
+
ee.data._get_state().cloud_api_user_project, task_id
|
|
271
|
+
)
|
|
272
|
+
# pylint: enable=protected-access
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _operation_name_to_task_id(operation_name: str) -> str:
|
|
276
|
+
"""Converts an operation name to a task ID."""
|
|
277
|
+
# pylint: disable-next=protected-access
|
|
278
|
+
return ee._cloud_api_utils.convert_operation_name_to_task_id(operation_name)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _convert_to_operation_state(state: str) -> str:
|
|
282
|
+
"""Converts a task or operation state to an operation state."""
|
|
283
|
+
# pylint: disable-next=protected-access
|
|
284
|
+
return ee._cloud_api_utils.convert_to_operation_state(state)
|
|
285
|
+
|
|
286
|
+
|
|
233
287
|
def _decode_property(string: str) -> tuple[str, Any]:
|
|
234
288
|
"""Decodes a general key-value property from a command-line argument.
|
|
235
289
|
|
|
236
290
|
Args:
|
|
237
291
|
string: The string must have the form name=value or (type)name=value, where
|
|
238
292
|
type is one of 'number', 'string', or 'date'. The value format for dates
|
|
239
|
-
is YYYY-MM-DD[THH:MM:SS[.MS]].
|
|
293
|
+
is YYYY-MM-DD[THH:MM:SS[.MS]]. The value 'null' is special: it evaluates
|
|
240
294
|
to None unless it is cast to a string of 'null'.
|
|
241
295
|
|
|
242
296
|
Returns:
|
|
@@ -400,7 +454,7 @@ class AuthenticateCommand:
|
|
|
400
454
|
args_auth['auth_mode'] = 'notebook'
|
|
401
455
|
|
|
402
456
|
if ee.Authenticate(**args_auth):
|
|
403
|
-
print('Authenticate: Credentials already exist.
|
|
457
|
+
print('Authenticate: Credentials already exist. Use --force to refresh.')
|
|
404
458
|
|
|
405
459
|
|
|
406
460
|
class SetProjectCommand:
|
|
@@ -1111,19 +1165,21 @@ class TaskCancelCommand:
|
|
|
1111
1165
|
config.ee_init()
|
|
1112
1166
|
cancel_all = args.task_ids == ['all']
|
|
1113
1167
|
if cancel_all:
|
|
1114
|
-
|
|
1168
|
+
operations = ee.data.listOperations()
|
|
1115
1169
|
else:
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1170
|
+
operation_names = map(_task_id_to_operation_name, args.task_ids)
|
|
1171
|
+
operations = map(ee.data.getOperation, operation_names)
|
|
1172
|
+
for operation in operations:
|
|
1173
|
+
name = operation['name']
|
|
1174
|
+
state = operation['metadata']['state']
|
|
1175
|
+
task_id = _operation_name_to_task_id(name)
|
|
1120
1176
|
if state == 'UNKNOWN':
|
|
1121
|
-
raise ee.EEException('Unknown task id "
|
|
1122
|
-
elif state == '
|
|
1123
|
-
print('Canceling task "
|
|
1124
|
-
ee.data.
|
|
1177
|
+
raise ee.EEException(f'Unknown task id "{task_id}"')
|
|
1178
|
+
elif state == 'PENDING' or state == 'RUNNING':
|
|
1179
|
+
print(f'Canceling task "{task_id}"')
|
|
1180
|
+
ee.data.cancelOperation(name)
|
|
1125
1181
|
elif not cancel_all:
|
|
1126
|
-
print('Task "{}" already in state "{}".'
|
|
1182
|
+
print(f'Task "{task_id}" already in state "{state}".')
|
|
1127
1183
|
|
|
1128
1184
|
|
|
1129
1185
|
class TaskInfoCommand:
|
|
@@ -1139,26 +1195,30 @@ class TaskInfoCommand:
|
|
|
1139
1195
|
) -> None:
|
|
1140
1196
|
"""Runs the TaskInfo command."""
|
|
1141
1197
|
config.ee_init()
|
|
1142
|
-
for i,
|
|
1198
|
+
for i, task_id in enumerate(args.task_id):
|
|
1199
|
+
operation = ee.data.getOperation(_task_id_to_operation_name(task_id))
|
|
1143
1200
|
if i:
|
|
1144
1201
|
print()
|
|
1145
|
-
print('
|
|
1146
|
-
|
|
1147
|
-
|
|
1202
|
+
print(f'{task_id}:')
|
|
1203
|
+
metadata = operation['metadata']
|
|
1204
|
+
state = metadata['state']
|
|
1205
|
+
print(f' State: {state}')
|
|
1206
|
+
if state == 'UNKNOWN':
|
|
1148
1207
|
continue
|
|
1149
|
-
print(' Type:
|
|
1150
|
-
print(' Description:
|
|
1151
|
-
print(' Created:
|
|
1152
|
-
if '
|
|
1153
|
-
print(' Started:
|
|
1154
|
-
if '
|
|
1155
|
-
print(' Updated:
|
|
1156
|
-
if '
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1208
|
+
print(f' Type: {TASK_TYPES.get(metadata.get("type"), "Unknown")}')
|
|
1209
|
+
print(f' Description: {metadata.get("description")}')
|
|
1210
|
+
print(f' Created: {_format_cloud_timestamp(metadata["createTime"])}')
|
|
1211
|
+
if start_time := metadata.get('startTime'):
|
|
1212
|
+
print(f' Started: {_format_cloud_timestamp(start_time)}')
|
|
1213
|
+
if update_time := metadata.get('updateTime'):
|
|
1214
|
+
print(f' Updated: {_format_cloud_timestamp(update_time)}')
|
|
1215
|
+
if error := operation.get('error'):
|
|
1216
|
+
if error_message := error.get('message'):
|
|
1217
|
+
print(f' Error: {error_message}')
|
|
1218
|
+
if destination_uris := metadata.get('destinationUris'):
|
|
1219
|
+
print(f' Destination URIs: {destination_uris}')
|
|
1220
|
+
if priority := metadata.get('priority'):
|
|
1221
|
+
print(f' Priority: {priority}')
|
|
1162
1222
|
|
|
1163
1223
|
|
|
1164
1224
|
class TaskListCommand:
|
|
@@ -1168,10 +1228,27 @@ class TaskListCommand:
|
|
|
1168
1228
|
|
|
1169
1229
|
def __init__(self, parser: argparse.ArgumentParser):
|
|
1170
1230
|
parser.add_argument(
|
|
1171
|
-
'--status',
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1231
|
+
'--status',
|
|
1232
|
+
'-s',
|
|
1233
|
+
required=False,
|
|
1234
|
+
nargs='*',
|
|
1235
|
+
choices=[
|
|
1236
|
+
'CANCELLED',
|
|
1237
|
+
'CANCELLING',
|
|
1238
|
+
'COMPLETED', # Kept for backward compatibility.
|
|
1239
|
+
'FAILED',
|
|
1240
|
+
'PENDING',
|
|
1241
|
+
'READY', # Kept for backward compatibility.
|
|
1242
|
+
'RUNNING',
|
|
1243
|
+
'SUCCEEDED',
|
|
1244
|
+
'UNKNOWN',
|
|
1245
|
+
],
|
|
1246
|
+
help=(
|
|
1247
|
+
'List tasks only with a given status. Note: for backward'
|
|
1248
|
+
' compatibility, "READY" is an alias for "PENDING" and "COMPLETED"'
|
|
1249
|
+
' is an alias for "SUCCEEDED".'
|
|
1250
|
+
),
|
|
1251
|
+
)
|
|
1175
1252
|
parser.add_argument(
|
|
1176
1253
|
'--long_format',
|
|
1177
1254
|
'-l',
|
|
@@ -1185,34 +1262,47 @@ class TaskListCommand:
|
|
|
1185
1262
|
) -> None:
|
|
1186
1263
|
"""Lists tasks present for a user, maybe filtering by state."""
|
|
1187
1264
|
config.ee_init()
|
|
1188
|
-
status =
|
|
1189
|
-
|
|
1190
|
-
|
|
1265
|
+
status = list(
|
|
1266
|
+
map(_convert_to_operation_state, args.status) if args.status else []
|
|
1267
|
+
)
|
|
1268
|
+
operations = ee.data.listOperations()
|
|
1269
|
+
descs = [
|
|
1270
|
+
utils.truncate(op.get('metadata', {}).get('description', ''), 40)
|
|
1271
|
+
for op in operations
|
|
1272
|
+
]
|
|
1191
1273
|
desc_length = max((len(word) for word in descs), default=0)
|
|
1192
|
-
format_str = '{:25s} {:13s} {
|
|
1193
|
-
for
|
|
1194
|
-
|
|
1274
|
+
format_str = f'{{:25s}} {{:13s}} {{:{desc_length + 1}s}} {{:10s}} {{:s}}'
|
|
1275
|
+
for operation in operations:
|
|
1276
|
+
metadata = operation['metadata']
|
|
1277
|
+
if status and metadata['state'] not in status:
|
|
1195
1278
|
continue
|
|
1196
|
-
truncated_desc = utils.truncate(
|
|
1197
|
-
task_type = TASK_TYPES.get(
|
|
1279
|
+
truncated_desc = utils.truncate(metadata.get('description', ''), 40)
|
|
1280
|
+
task_type = TASK_TYPES.get(metadata['type'], 'Unknown')
|
|
1198
1281
|
extra = ''
|
|
1199
1282
|
if args.long_format:
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
trailing_extras =
|
|
1205
|
-
trailing_extras.append(
|
|
1283
|
+
if eecu := metadata.get('batchEecuUsageSeconds'):
|
|
1284
|
+
eecu = f'{eecu:.4f}'
|
|
1285
|
+
else:
|
|
1286
|
+
eecu = '-'
|
|
1287
|
+
trailing_extras = metadata.get('destination_uris', [])
|
|
1288
|
+
trailing_extras.append(metadata.get('priority', '-'))
|
|
1206
1289
|
extra = ' {:20s} {:20s} {:20s} {:11s} {}'.format(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1290
|
+
_format_cloud_timestamp(operation.get('createTime')),
|
|
1291
|
+
_format_cloud_timestamp(operation.get('startTime')),
|
|
1292
|
+
_format_cloud_timestamp(operation.get('updateTime')),
|
|
1210
1293
|
eecu,
|
|
1211
1294
|
' '.join(map(str, trailing_extras)),
|
|
1212
1295
|
)
|
|
1213
|
-
print(
|
|
1214
|
-
|
|
1215
|
-
|
|
1296
|
+
print(
|
|
1297
|
+
format_str.format(
|
|
1298
|
+
_operation_name_to_task_id(operation['name']),
|
|
1299
|
+
task_type,
|
|
1300
|
+
truncated_desc,
|
|
1301
|
+
metadata['state'],
|
|
1302
|
+
operation.get('error', {}).get('message', '---'),
|
|
1303
|
+
)
|
|
1304
|
+
+ extra
|
|
1305
|
+
)
|
|
1216
1306
|
|
|
1217
1307
|
|
|
1218
1308
|
class TaskWaitCommand:
|
|
@@ -1241,17 +1331,17 @@ class TaskWaitCommand:
|
|
|
1241
1331
|
config.ee_init()
|
|
1242
1332
|
task_ids = []
|
|
1243
1333
|
if args.task_ids == ['all']:
|
|
1244
|
-
|
|
1245
|
-
for
|
|
1246
|
-
if
|
|
1247
|
-
task_ids.append(
|
|
1334
|
+
operations = ee.data.listOperations()
|
|
1335
|
+
for operation in operations:
|
|
1336
|
+
if not operation.get('done', False):
|
|
1337
|
+
task_ids.append(_operation_name_to_task_id(operation['name']))
|
|
1248
1338
|
else:
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
state =
|
|
1252
|
-
task_id =
|
|
1339
|
+
for task_id in args.task_ids:
|
|
1340
|
+
operation = ee.data.getOperation(_task_id_to_operation_name(task_id))
|
|
1341
|
+
state = operation['metadata']['state']
|
|
1342
|
+
task_id = _operation_name_to_task_id(operation['name'])
|
|
1253
1343
|
if state == 'UNKNOWN':
|
|
1254
|
-
raise ee.EEException('Unknown task id "
|
|
1344
|
+
raise ee.EEException(f'Unknown task id "{task_id}"')
|
|
1255
1345
|
else:
|
|
1256
1346
|
task_ids.append(task_id)
|
|
1257
1347
|
|
ee/cli/eecli.py
CHANGED
|
@@ -35,7 +35,7 @@ def _run_command(*argv):
|
|
|
35
35
|
'Defaults to "~/%s".' % utils.DEFAULT_EE_CONFIG_FILE_RELATIVE)
|
|
36
36
|
parser.add_argument(
|
|
37
37
|
'--service_account_file', help='Path to a service account credentials'
|
|
38
|
-
'file.
|
|
38
|
+
'file. Overrides any ee_config if specified.')
|
|
39
39
|
parser.add_argument(
|
|
40
40
|
'--project',
|
|
41
41
|
help='Specifies a Google Cloud Platform Project id to override the call.',
|
ee/cli/utils.py
CHANGED
|
@@ -40,12 +40,6 @@ CONFIG_PARAMS: dict[str, Union[str, list[str], None]] = {
|
|
|
40
40
|
'url': 'https://earthengine.googleapis.com',
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
TASK_FINISHED_STATES: tuple[str, str, str] = (
|
|
44
|
-
ee.batch.Task.State.COMPLETED,
|
|
45
|
-
ee.batch.Task.State.FAILED,
|
|
46
|
-
ee.batch.Task.State.CANCELLED,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
43
|
|
|
50
44
|
class CommandLineConfig:
|
|
51
45
|
"""Holds the configuration parameters used by the EE command line interface.
|
|
@@ -248,6 +242,15 @@ def truncate(string: str, length: int) -> str:
|
|
|
248
242
|
return string
|
|
249
243
|
|
|
250
244
|
|
|
245
|
+
def _task_id_to_operation_name(task_id: str) -> str:
|
|
246
|
+
"""Converts a task ID to an operation name."""
|
|
247
|
+
# pylint: disable=protected-access
|
|
248
|
+
return ee._cloud_api_utils.convert_task_id_to_operation_name(
|
|
249
|
+
ee.data._get_state().cloud_api_user_project, task_id
|
|
250
|
+
)
|
|
251
|
+
# pylint: enable=protected-access
|
|
252
|
+
|
|
253
|
+
|
|
251
254
|
def wait_for_task(
|
|
252
255
|
task_id: str, timeout: float, log_progress: bool = True
|
|
253
256
|
) -> None:
|
|
@@ -257,10 +260,10 @@ def wait_for_task(
|
|
|
257
260
|
last_check = 0
|
|
258
261
|
while True:
|
|
259
262
|
elapsed = time.time() - start
|
|
260
|
-
status = ee.data.
|
|
261
|
-
state = status['state']
|
|
262
|
-
if
|
|
263
|
-
error_message = status.get('
|
|
263
|
+
status = ee.data.getOperation(_task_id_to_operation_name(task_id))
|
|
264
|
+
state = status['metadata']['state']
|
|
265
|
+
if status.get('done', False):
|
|
266
|
+
error_message = status.get('error', {}).get('message')
|
|
264
267
|
print('Task %s ended at state: %s after %.2f seconds'
|
|
265
268
|
% (task_id, state, elapsed))
|
|
266
269
|
if error_message:
|
|
@@ -299,14 +302,21 @@ def wait_for_tasks(
|
|
|
299
302
|
for thread in threads:
|
|
300
303
|
thread.join()
|
|
301
304
|
|
|
302
|
-
|
|
305
|
+
get_state = lambda task_id: ee.data.getOperation(
|
|
306
|
+
_task_id_to_operation_name(task_id)
|
|
307
|
+
)['metadata']['state']
|
|
308
|
+
status_list = [get_state(task_id) for task_id in task_id_list]
|
|
303
309
|
status_counts = collections.defaultdict(int)
|
|
304
310
|
for status in status_list:
|
|
305
|
-
status_counts[status
|
|
306
|
-
num_incomplete = (
|
|
307
|
-
|
|
311
|
+
status_counts[status] += 1
|
|
312
|
+
num_incomplete = (
|
|
313
|
+
len(status_list)
|
|
314
|
+
- status_counts['SUCCEEDED']
|
|
315
|
+
- status_counts['FAILED']
|
|
316
|
+
- status_counts['CANCELLED']
|
|
317
|
+
)
|
|
308
318
|
print('Finished waiting for tasks.\n Status summary:')
|
|
309
|
-
print(' %d tasks completed successfully.' % status_counts['
|
|
319
|
+
print(' %d tasks completed successfully.' % status_counts['SUCCEEDED'])
|
|
310
320
|
print(' %d tasks failed.' % status_counts['FAILED'])
|
|
311
321
|
print(' %d tasks cancelled.' % status_counts['CANCELLED'])
|
|
312
322
|
print(' %d tasks are still incomplete (timed-out)' % num_incomplete)
|
ee/collection.py
CHANGED
|
@@ -6,7 +6,7 @@ This class is never intended to be instantiated by the user.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import datetime
|
|
9
|
-
from typing import Any, Callable
|
|
9
|
+
from typing import Any, Callable, Generic, TypeVar
|
|
10
10
|
|
|
11
11
|
from ee import _arg_types
|
|
12
12
|
from ee import _utils
|
|
@@ -26,8 +26,14 @@ from ee import function
|
|
|
26
26
|
from ee import geometry as ee_geometry
|
|
27
27
|
from ee import image
|
|
28
28
|
|
|
29
|
+
# TODO: Remove this TypeVar and replace with typing.Self once
|
|
30
|
+
# Python 3.11 is the minimum version.
|
|
31
|
+
T = TypeVar('T', bound='Collection')
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
ElementType = TypeVar('ElementType')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Collection(Generic[ElementType], element.Element):
|
|
31
37
|
"""Base class for ImageCollection and FeatureCollection."""
|
|
32
38
|
|
|
33
39
|
_initialized = False
|
|
@@ -442,7 +448,7 @@ class Collection(element.Element):
|
|
|
442
448
|
'Collection.errorMatrix', self, actual, predicted, order
|
|
443
449
|
)
|
|
444
450
|
|
|
445
|
-
def filter(self, new_filter: str | ee_filter.Filter) ->
|
|
451
|
+
def filter(self: T, new_filter: str | ee_filter.Filter) -> T:
|
|
446
452
|
"""Apply a filter to this collection.
|
|
447
453
|
|
|
448
454
|
Args:
|
|
@@ -459,8 +465,8 @@ class Collection(element.Element):
|
|
|
459
465
|
|
|
460
466
|
@deprecation.CanUseDeprecated
|
|
461
467
|
def filterMetadata(
|
|
462
|
-
self, name: str, operator: str, value: int | str
|
|
463
|
-
) ->
|
|
468
|
+
self: T, name: str, operator: str, value: int | str
|
|
469
|
+
) -> T:
|
|
464
470
|
"""Shortcut to add a metadata filter to a collection.
|
|
465
471
|
|
|
466
472
|
This is equivalent to self.filter(Filter().metadata(...)).
|
|
@@ -468,7 +474,7 @@ class Collection(element.Element):
|
|
|
468
474
|
Args:
|
|
469
475
|
name: Name of a property to filter.
|
|
470
476
|
operator: Name of a comparison operator as defined
|
|
471
|
-
by FilterCollection.
|
|
477
|
+
by FilterCollection. Possible values are: "equals", "less_than",
|
|
472
478
|
"greater_than", "not_equals", "not_less_than", "not_greater_than",
|
|
473
479
|
"starts_with", "ends_with", "not_starts_with", "not_ends_with",
|
|
474
480
|
"contains", "not_contains".
|
|
@@ -480,8 +486,13 @@ class Collection(element.Element):
|
|
|
480
486
|
return self.filter(ee_filter.Filter.metadata_(name, operator, value))
|
|
481
487
|
|
|
482
488
|
def filterBounds(
|
|
483
|
-
self
|
|
484
|
-
|
|
489
|
+
self: T,
|
|
490
|
+
geometry: (
|
|
491
|
+
dict[str, Any]
|
|
492
|
+
| ee_geometry.Geometry
|
|
493
|
+
| featurecollection.FeatureCollection
|
|
494
|
+
),
|
|
495
|
+
) -> T:
|
|
485
496
|
"""Shortcut to add a geometry filter to a collection.
|
|
486
497
|
|
|
487
498
|
Items in the collection with a footprint that fails to intersect
|
|
@@ -505,12 +516,10 @@ class Collection(element.Element):
|
|
|
505
516
|
# TODO(user): Any --> DateRange
|
|
506
517
|
@_utils.accept_opt_prefix('opt_end')
|
|
507
518
|
def filterDate(
|
|
508
|
-
self,
|
|
519
|
+
self: T,
|
|
509
520
|
start: datetime.datetime | ee_date.Date | int | str | Any,
|
|
510
|
-
end: None | (
|
|
511
|
-
|
|
512
|
-
) = None,
|
|
513
|
-
) -> Any:
|
|
521
|
+
end: None | (datetime.datetime | ee_date.Date | int | str | Any) = None,
|
|
522
|
+
) -> T:
|
|
514
523
|
"""Shortcut to filter a collection with a date range.
|
|
515
524
|
|
|
516
525
|
Items in the collection with a system:time_start property that doesn't
|
|
@@ -529,7 +538,7 @@ class Collection(element.Element):
|
|
|
529
538
|
"""
|
|
530
539
|
return self.filter(ee_filter.Filter.date(start, end))
|
|
531
540
|
|
|
532
|
-
def first(self) ->
|
|
541
|
+
def first(self) -> ElementType:
|
|
533
542
|
"""Returns the first entry from a given collection."""
|
|
534
543
|
|
|
535
544
|
return apifunction.ApiFunction.call_('Collection.first', self)
|
|
@@ -660,10 +669,10 @@ class Collection(element.Element):
|
|
|
660
669
|
# TODO(user): Can dropNulls default to False?
|
|
661
670
|
@_utils.accept_opt_prefix('opt_dropNulls')
|
|
662
671
|
def map(
|
|
663
|
-
self,
|
|
672
|
+
self: T,
|
|
664
673
|
algorithm: Callable[[Any], Any],
|
|
665
674
|
dropNulls: bool | None = None, # pylint: disable=invalid-name
|
|
666
|
-
) ->
|
|
675
|
+
) -> T:
|
|
667
676
|
"""Maps an algorithm over a collection.
|
|
668
677
|
|
|
669
678
|
Args:
|
|
@@ -822,12 +831,12 @@ class Collection(element.Element):
|
|
|
822
831
|
|
|
823
832
|
# TODO(user): Make ascending default to True
|
|
824
833
|
@_utils.accept_opt_prefix('opt_ascending')
|
|
825
|
-
def sort(self, prop: str, ascending: bool | None = None) ->
|
|
834
|
+
def sort(self: T, prop: str, ascending: bool | None = None) -> T:
|
|
826
835
|
"""Sort a collection by the specified property.
|
|
827
836
|
|
|
828
837
|
Args:
|
|
829
838
|
prop: The property to sort by.
|
|
830
|
-
ascending: Whether to sort in ascending or descending order.
|
|
839
|
+
ascending: Whether to sort in ascending or descending order. The default
|
|
831
840
|
is true (ascending).
|
|
832
841
|
|
|
833
842
|
Returns:
|
ee/computedobject.py
CHANGED
|
@@ -38,11 +38,11 @@ class ComputedObject(encodable.Encodable, metaclass=ComputedObjectMetaclass):
|
|
|
38
38
|
necessary to interact well with the rest of the API.
|
|
39
39
|
|
|
40
40
|
ComputedObjects come in two flavors:
|
|
41
|
-
1. If func
|
|
42
|
-
invocation of func with args.
|
|
43
|
-
2. If func
|
|
41
|
+
1. If func is not None and args is not None, the ComputedObject is encoded as
|
|
42
|
+
an invocation of func with args.
|
|
43
|
+
2. If func is None and args is None, the ComputedObject is a variable
|
|
44
44
|
reference. The variable name is stored in its varName member. Note that
|
|
45
|
-
in this case, varName may still be
|
|
45
|
+
in this case, varName may still be None; this allows the name to be
|
|
46
46
|
deterministically generated at a later time. This is used to generate
|
|
47
47
|
deterministic variable names for mapped functions, ensuring that nested
|
|
48
48
|
mapping calls do not use the same variable name.
|
|
@@ -184,7 +184,7 @@ class ComputedObject(encodable.Encodable, metaclass=ComputedObjectMetaclass):
|
|
|
184
184
|
|
|
185
185
|
def __str__(self) -> str:
|
|
186
186
|
"""Writes out the object in a human-readable form."""
|
|
187
|
-
return 'ee.{
|
|
187
|
+
return f'ee.{self.name()}({serializer.toReadableJSON(self)})'
|
|
188
188
|
|
|
189
189
|
def isVariable(self) -> bool:
|
|
190
190
|
"""Returns whether this computed object is a variable reference."""
|
ee/customfunction.py
CHANGED
|
@@ -171,19 +171,19 @@ class CustomFunction(function.Function, encodable.Encodable):
|
|
|
171
171
|
return CountNodes(expression['values'].values())
|
|
172
172
|
|
|
173
173
|
# There are three function building phases, which each call body():
|
|
174
|
-
# 1 - Check Return.
|
|
174
|
+
# 1 - Check Return. The constructor verifies that body() returns a result,
|
|
175
175
|
# but does not try to serialize the result. If the function tries to use
|
|
176
176
|
# unbound variables (e.g., using .getInfo() or print()), ComputedObject will
|
|
177
177
|
# throw an exception when these calls try to serialize themselves, so that
|
|
178
178
|
# unbound variables are not passed in server calls.
|
|
179
|
-
# 2 - Count Functions.
|
|
179
|
+
# 2 - Count Functions. We serialize the result here. At this point all
|
|
180
180
|
# variables must have names for serialization to succeed, but we don't yet
|
|
181
181
|
# know the correct function depth. So we serialize with unbound_name set to
|
|
182
182
|
# '<unbound>', which should silently succeed. If this does end up in server
|
|
183
183
|
# calls, the function is very unusual: the first call doesn't use unbound
|
|
184
184
|
# variables but the second call does. In this rare case we will return
|
|
185
185
|
# server errors complaining about <unbound>.
|
|
186
|
-
# 3 - Final Serialize.
|
|
186
|
+
# 3 - Final Serialize. Finally, the constructor calls body() with the
|
|
187
187
|
# correct, depth-dependent names, which are used when the CustomFunction
|
|
188
188
|
# is serialized and sent to the server.
|
|
189
189
|
serialized_body = serializer.encode(
|