earthengine-api 1.5.13rc0__py3-none-any.whl → 1.7.4__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.

Potentially problematic release.


This version of earthengine-api might be problematic. Click here for more details.

Files changed (102) hide show
  1. {earthengine_api-1.5.13rc0.dist-info → earthengine_api-1.7.4.dist-info}/METADATA +3 -3
  2. earthengine_api-1.7.4.dist-info/RECORD +109 -0
  3. {earthengine_api-1.5.13rc0.dist-info → earthengine_api-1.7.4.dist-info}/WHEEL +1 -1
  4. ee/__init__.py +29 -28
  5. ee/_arg_types.py +7 -6
  6. ee/_cloud_api_utils.py +95 -78
  7. ee/_helpers.py +17 -13
  8. ee/_state.py +105 -0
  9. ee/_utils.py +2 -1
  10. ee/apifunction.py +21 -19
  11. ee/apitestcase.py +33 -38
  12. ee/batch.py +87 -77
  13. ee/blob.py +10 -12
  14. ee/classifier.py +57 -59
  15. ee/cli/commands.py +178 -114
  16. ee/cli/eecli.py +1 -1
  17. ee/cli/utils.py +61 -42
  18. ee/clusterer.py +39 -41
  19. ee/collection.py +64 -54
  20. ee/computedobject.py +19 -16
  21. ee/confusionmatrix.py +9 -9
  22. ee/customfunction.py +13 -12
  23. ee/data.py +220 -322
  24. ee/daterange.py +10 -10
  25. ee/deprecation.py +21 -13
  26. ee/deserializer.py +25 -20
  27. ee/dictionary.py +11 -11
  28. ee/ee_array.py +22 -20
  29. ee/ee_date.py +23 -23
  30. ee/ee_list.py +15 -16
  31. ee/ee_number.py +11 -21
  32. ee/ee_string.py +24 -32
  33. ee/ee_types.py +4 -4
  34. ee/element.py +15 -15
  35. ee/encodable.py +7 -4
  36. ee/errormargin.py +4 -4
  37. ee/feature.py +68 -71
  38. ee/featurecollection.py +41 -40
  39. ee/filter.py +90 -92
  40. ee/function.py +8 -8
  41. ee/geometry.py +95 -93
  42. ee/image.py +238 -236
  43. ee/image_converter.py +4 -4
  44. ee/imagecollection.py +30 -27
  45. ee/join.py +13 -15
  46. ee/kernel.py +55 -57
  47. ee/mapclient.py +9 -9
  48. ee/model.py +29 -31
  49. ee/oauth.py +76 -63
  50. ee/pixeltype.py +6 -6
  51. ee/projection.py +5 -4
  52. ee/reducer.py +41 -41
  53. ee/serializer.py +14 -14
  54. ee/table_converter.py +7 -6
  55. ee/terrain.py +7 -9
  56. ee/tests/_cloud_api_utils_test.py +21 -6
  57. ee/tests/_helpers_test.py +57 -4
  58. ee/tests/_state_test.py +49 -0
  59. ee/tests/algorithms.json +85 -2
  60. ee/tests/apifunction_test.py +5 -5
  61. ee/tests/batch_test.py +135 -57
  62. ee/tests/blob_test.py +5 -5
  63. ee/tests/classifier_test.py +3 -3
  64. ee/tests/clusterer_test.py +3 -3
  65. ee/tests/collection_test.py +48 -13
  66. ee/tests/confusionmatrix_test.py +3 -3
  67. ee/tests/data_test.py +484 -55
  68. ee/tests/daterange_test.py +4 -4
  69. ee/tests/deprecation_test.py +6 -4
  70. ee/tests/deserializer_test.py +64 -5
  71. ee/tests/dictionary_test.py +12 -12
  72. ee/tests/ee_array_test.py +3 -3
  73. ee/tests/ee_date_test.py +4 -4
  74. ee/tests/ee_list_test.py +3 -3
  75. ee/tests/ee_number_test.py +75 -30
  76. ee/tests/ee_string_test.py +11 -3
  77. ee/tests/ee_test.py +40 -22
  78. ee/tests/element_test.py +2 -2
  79. ee/tests/errormargin_test.py +1 -1
  80. ee/tests/feature_test.py +10 -10
  81. ee/tests/featurecollection_test.py +3 -3
  82. ee/tests/filter_test.py +4 -4
  83. ee/tests/function_test.py +5 -5
  84. ee/tests/geometry_point_test.py +3 -3
  85. ee/tests/geometry_test.py +93 -52
  86. ee/tests/image_converter_test.py +1 -3
  87. ee/tests/image_test.py +3 -3
  88. ee/tests/imagecollection_test.py +3 -3
  89. ee/tests/join_test.py +3 -3
  90. ee/tests/kernel_test.py +7 -3
  91. ee/tests/model_test.py +17 -5
  92. ee/tests/oauth_test.py +189 -7
  93. ee/tests/pixeltype_test.py +6 -7
  94. ee/tests/projection_test.py +5 -6
  95. ee/tests/reducer_test.py +16 -3
  96. ee/tests/serializer_test.py +39 -12
  97. ee/tests/table_converter_test.py +51 -7
  98. ee/tests/terrain_test.py +11 -3
  99. earthengine_api-1.5.13rc0.dist-info/RECORD +0 -107
  100. {earthengine_api-1.5.13rc0.dist-info → earthengine_api-1.7.4.dist-info}/entry_points.txt +0 -0
  101. {earthengine_api-1.5.13rc0.dist-info → earthengine_api-1.7.4.dist-info}/licenses/LICENSE +0 -0
  102. {earthengine_api-1.5.13rc0.dist-info → earthengine_api-1.7.4.dist-info}/top_level.txt +0 -0
ee/_cloud_api_utils.py CHANGED
@@ -6,12 +6,13 @@ parameters and result values.
6
6
  """
7
7
 
8
8
  import calendar
9
+ from collections.abc import Callable, Sequence
9
10
  import copy
10
11
  import datetime
11
12
  import json
12
13
  import os
13
14
  import re
14
- from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
15
+ from typing import Any
15
16
  import warnings
16
17
 
17
18
  import google_auth_httplib2
@@ -23,6 +24,7 @@ import requests
23
24
 
24
25
  from ee import ee_exception
25
26
 
27
+
26
28
  # The Cloud API version.
27
29
  VERSION = os.environ.get('EE_CLOUD_API_VERSION', 'v1')
28
30
 
@@ -34,17 +36,24 @@ ASSET_NAME_PATTERN = (r'^projects/((?:\w+(?:[\w\-]+\.[\w\-]+)*?\.\w+\:)?'
34
36
  ASSET_ROOT_PATTERN = (r'^projects/((?:\w+(?:[\w\-]+\.[\w\-]+)*?\.\w+\:)?'
35
37
  r'[a-z][a-z0-9\-]{4,28}[a-z0-9])/assets/?$')
36
38
 
37
- # The default user project to use when making Cloud API calls.
38
- _cloud_api_user_project: Optional[str] = None
39
+ # Conversion from task state to operation state.
40
+ TASK_TO_OPERATION_STATE = {
41
+ 'READY': 'PENDING',
42
+ 'RUNNING': 'RUNNING',
43
+ 'CANCEL_REQUESTED': 'CANCELLING',
44
+ 'COMPLETED': 'SUCCEEDED',
45
+ 'CANCELLED': 'CANCELLED',
46
+ 'FAILED': 'FAILED',
47
+ }
39
48
 
40
49
 
41
50
  class _Http:
42
51
  """A httplib2.Http-like object based on requests."""
43
52
  _session: requests.Session
44
- _timeout: Optional[float]
53
+ _timeout: float | None
45
54
 
46
55
  def __init__(
47
- self, session: requests.Session, timeout: Optional[float] = None
56
+ self, session: requests.Session, timeout: float | None = None
48
57
  ):
49
58
  self._timeout = timeout
50
59
  self._session = session
@@ -53,11 +62,11 @@ class _Http:
53
62
  self,
54
63
  uri: str,
55
64
  method: str = 'GET',
56
- body: Optional[str] = None,
57
- headers: Optional[Dict[str, str]] = None,
58
- redirections: Optional[int] = None,
59
- connection_type: Optional[Type[Any]] = None,
60
- ) -> Tuple[httplib2.Response, Any]:
65
+ body: str | None = None,
66
+ headers: dict[str, str] | None = None,
67
+ redirections: int | None = None,
68
+ connection_type: type[Any] | None = None,
69
+ ) -> tuple[httplib2.Response, Any]:
61
70
  """Makes an HTTP request using httplib2 semantics."""
62
71
  del connection_type # Ignored
63
72
  del redirections # Ignored
@@ -84,7 +93,7 @@ class _Http:
84
93
 
85
94
 
86
95
  def _wrap_request(
87
- headers_supplier: Callable[[], Dict[str, Any]],
96
+ headers_supplier: Callable[[], dict[str, Any]],
88
97
  response_inspector: Callable[[Any], None],
89
98
  ) -> Callable[..., http.HttpRequest]:
90
99
  """Builds a callable that wraps an API request.
@@ -109,10 +118,10 @@ def _wrap_request(
109
118
  postproc: Callable[..., Any],
110
119
  uri: str,
111
120
  method: str = 'GET',
112
- body: Optional[Any] = None,
113
- headers: Optional[Any] = None,
114
- methodId: Optional[Any] = None,
115
- resumable: Optional[Any] = None,
121
+ body: Any | None = None,
122
+ headers: Any | None = None,
123
+ methodId: Any | None = None,
124
+ resumable: Any | None = None,
116
125
  ) -> http.HttpRequest:
117
126
  """Builds an HttpRequest, adding headers and response inspection."""
118
127
  additional_headers = headers_supplier()
@@ -135,22 +144,17 @@ def _wrap_request(
135
144
  return builder
136
145
 
137
146
 
138
- def set_cloud_api_user_project(cloud_api_user_project: str) -> None:
139
- global _cloud_api_user_project
140
- _cloud_api_user_project = cloud_api_user_project
141
-
142
-
143
147
  def build_cloud_resource(
144
148
  api_base_url: str,
145
149
  session: requests.Session,
146
- api_key: Optional[str] = None,
147
- credentials: Optional[Any] = None,
148
- timeout: Optional[float] = None,
150
+ api_key: str | None = None,
151
+ credentials: Any | None = None,
152
+ timeout: float | None = None,
149
153
  num_retries: int = 1,
150
- headers_supplier: Optional[Callable[[], Dict[str, Any]]] = None,
151
- response_inspector: Optional[Callable[[Any], None]] = None,
152
- http_transport: Optional[Any] = None,
153
- raw: Optional[bool] = False,
154
+ headers_supplier: Callable[[], dict[str, Any]] | None = None,
155
+ response_inspector: Callable[[Any], None] | None = None,
156
+ http_transport: Any | None = None,
157
+ raw: bool | None = False,
154
158
  ) -> Any:
155
159
  """Builds an Earth Engine Cloud API resource.
156
160
 
@@ -222,9 +226,9 @@ def build_cloud_resource(
222
226
 
223
227
  def build_cloud_resource_from_document(
224
228
  discovery_document: Any,
225
- http_transport: Optional[httplib2.Http] = None,
226
- headers_supplier: Optional[Callable[..., Any]] = None,
227
- response_inspector: Optional[Callable[..., Any]] = None,
229
+ http_transport: httplib2.Http | None = None,
230
+ headers_supplier: Callable[..., Any] | None = None,
231
+ response_inspector: Callable[..., Any] | None = None,
228
232
  raw: bool = False,
229
233
  ) -> discovery.Resource:
230
234
  """Builds an Earth Engine Cloud API resource from a description of the API.
@@ -256,12 +260,12 @@ def build_cloud_resource_from_document(
256
260
 
257
261
 
258
262
  def _convert_dict(
259
- to_convert: Dict[str, Any],
260
- conversions: Dict[str, Any],
261
- defaults: Optional[Dict[str, Any]] = None,
263
+ to_convert: dict[str, Any],
264
+ conversions: dict[str, Any],
265
+ defaults: dict[str, Any] | None = None,
262
266
  key_warnings: bool = False,
263
267
  retain_keys: bool = False,
264
- ) -> Dict[str, Any]:
268
+ ) -> dict[str, Any]:
265
269
  """Applies a set of conversion rules to a dict.
266
270
 
267
271
  Args:
@@ -287,14 +291,14 @@ def _convert_dict(
287
291
  not contain these keys.
288
292
  key_warnings: Whether to print warnings for input keys that are not mapped
289
293
  to anything in the output.
290
- retain_keys: Whether or not to retain the state of dict. If false, any keys
294
+ retain_keys: Whether or not to retain the state of dict. If false, any keys
291
295
  that don't show up in the conversions dict will be dropped from result.
292
296
 
293
297
  Returns:
294
298
  The "to_convert" dict with keys renamed, values converted, and defaults
295
299
  added.
296
300
  """
297
- result: Dict[str, Any] = {}
301
+ result: dict[str, Any] = {}
298
302
  for key, value in to_convert.items():
299
303
  if key in conversions:
300
304
  conversion = conversions[key]
@@ -319,7 +323,7 @@ def _convert_dict(
319
323
 
320
324
 
321
325
  def _convert_value(
322
- value: str, conversions: Dict[str, Any], default: Any) -> Any:
326
+ value: str, conversions: dict[str, Any], default: Any) -> Any:
323
327
  """Converts a value using a set of value mappings.
324
328
 
325
329
  Args:
@@ -382,7 +386,7 @@ def _convert_bounding_box_to_geo_json(bbox: Sequence[float]) -> str:
382
386
  lng_min, lat_min, lng_max, lat_max))
383
387
 
384
388
 
385
- def convert_get_list_params_to_list_assets_params(params) -> Dict[str, Any]:
389
+ def convert_get_list_params_to_list_assets_params(params) -> dict[str, Any]:
386
390
  """Converts a getList params dict to something usable with listAssets."""
387
391
  params = _convert_dict(
388
392
  params, {
@@ -401,7 +405,7 @@ def convert_get_list_params_to_list_assets_params(params) -> Dict[str, Any]:
401
405
  return convert_list_images_params_to_list_assets_params(params)
402
406
 
403
407
 
404
- def convert_list_assets_result_to_get_list_result(result) -> List[Any]:
408
+ def convert_list_assets_result_to_get_list_result(result) -> list[Any]:
405
409
  """Converts a listAssets result to something getList can return."""
406
410
  if 'assets' not in result:
407
411
  return []
@@ -434,7 +438,7 @@ def _convert_list_images_filter_params_to_list_assets_params(params) -> str:
434
438
  # query in a set of double quotes. We trivially avoid doubly-escaping the
435
439
  # quotes by replacing double quotes with single quotes.
436
440
  region = region.replace('"', "'")
437
- query_strings.append('intersects("{}")'.format(region))
441
+ query_strings.append(f'intersects("{region}")')
438
442
  del params['region']
439
443
  if 'properties' in params:
440
444
  if isinstance(params['properties'], list) and any(
@@ -452,8 +456,8 @@ def _convert_list_images_filter_params_to_list_assets_params(params) -> str:
452
456
 
453
457
 
454
458
  def convert_list_images_params_to_list_assets_params(
455
- params: Dict[str, Any]
456
- ) -> Dict[str, Any]:
459
+ params: dict[str, Any]
460
+ ) -> dict[str, Any]:
457
461
  """Converts a listImages params dict to something usable with listAssets."""
458
462
  params = params.copy()
459
463
  extra_filters = _convert_list_images_filter_params_to_list_assets_params(
@@ -470,14 +474,14 @@ def is_asset_root(asset_name: str) -> bool:
470
474
  return bool(re.match(ASSET_ROOT_PATTERN, asset_name))
471
475
 
472
476
 
473
- def convert_list_images_result_to_get_list_result(result) -> List[Any]:
477
+ def convert_list_images_result_to_get_list_result(result) -> list[Any]:
474
478
  """Converts a listImages result to something getList can return."""
475
479
  if 'images' not in result:
476
480
  return []
477
481
  return [_convert_image_for_get_list_result(i) for i in result['images']]
478
482
 
479
483
 
480
- def _convert_asset_for_get_list_result(asset) -> Dict[str, Any]:
484
+ def _convert_asset_for_get_list_result(asset) -> dict[str, Any]:
481
485
  """Converts an EarthEngineAsset to the format returned by getList."""
482
486
  result = _convert_dict(
483
487
  asset, {
@@ -488,7 +492,7 @@ def _convert_asset_for_get_list_result(asset) -> Dict[str, Any]:
488
492
  return result
489
493
 
490
494
 
491
- def _convert_image_for_get_list_result(asset) -> Dict[str, Any]:
495
+ def _convert_image_for_get_list_result(asset) -> dict[str, Any]:
492
496
  """Converts an Image to the format returned by getList."""
493
497
  result = _convert_dict(
494
498
  asset, {
@@ -534,12 +538,12 @@ def convert_asset_id_to_asset_name(asset_id: str) -> str:
534
538
  if re.match(ASSET_NAME_PATTERN, asset_id) or is_asset_root(asset_id):
535
539
  return asset_id
536
540
  elif asset_id.split('/')[0] in ['users', 'projects']:
537
- return 'projects/earthengine-legacy/assets/{}'.format(asset_id)
541
+ return f'projects/earthengine-legacy/assets/{asset_id}'
538
542
  else:
539
- return 'projects/earthengine-public/assets/{}'.format(asset_id)
543
+ return f'projects/earthengine-public/assets/{asset_id}'
540
544
 
541
545
 
542
- def split_asset_name(asset_name: str) -> Tuple[str, str]:
546
+ def split_asset_name(asset_name: str) -> tuple[str, str]:
543
547
  """Splits an asset name into the parent and ID parts.
544
548
 
545
549
  Args:
@@ -559,12 +563,12 @@ def convert_operation_name_to_task_id(operation_name: str) -> str:
559
563
  return found.group(1) if found else operation_name
560
564
 
561
565
 
562
- def convert_task_id_to_operation_name(task_id: str) -> str:
566
+ def convert_task_id_to_operation_name(project: str, task_id: str) -> str:
563
567
  """Converts a task ID to an Operation name."""
564
- return 'projects/{}/operations/{}'.format(_cloud_api_user_project, task_id)
568
+ return f'projects/{project}/operations/{task_id}'
565
569
 
566
570
 
567
- def convert_params_to_image_manifest(params: Dict[str, Any]) -> Dict[str, Any]:
571
+ def convert_params_to_image_manifest(params: dict[str, Any]) -> dict[str, Any]:
568
572
  """Converts params to an ImageManifest for ingestion."""
569
573
  return _convert_dict(
570
574
  params, {
@@ -574,7 +578,7 @@ def convert_params_to_image_manifest(params: Dict[str, Any]) -> Dict[str, Any]:
574
578
  retain_keys=True)
575
579
 
576
580
 
577
- def convert_params_to_table_manifest(params: Dict[str, Any]) -> Dict[str, Any]:
581
+ def convert_params_to_table_manifest(params: dict[str, Any]) -> dict[str, Any]:
578
582
  """Converts params to a TableManifest for ingestion."""
579
583
  return _convert_dict(
580
584
  params, {
@@ -584,7 +588,7 @@ def convert_params_to_table_manifest(params: Dict[str, Any]) -> Dict[str, Any]:
584
588
  retain_keys=True)
585
589
 
586
590
 
587
- def convert_tilesets_to_one_platform_tilesets(tilesets: List[Any]) -> List[Any]:
591
+ def convert_tilesets_to_one_platform_tilesets(tilesets: list[Any]) -> list[Any]:
588
592
  """Converts a tileset to a one platform representation of a tileset."""
589
593
  converted_tilesets = []
590
594
  for tileset in tilesets:
@@ -596,7 +600,7 @@ def convert_tilesets_to_one_platform_tilesets(tilesets: List[Any]) -> List[Any]:
596
600
  return converted_tilesets
597
601
 
598
602
 
599
- def convert_sources_to_one_platform_sources(sources: List[Any]) -> List[Any]:
603
+ def convert_sources_to_one_platform_sources(sources: list[Any]) -> list[Any]:
600
604
  """Converts the sources to one platform representation of sources."""
601
605
  converted_sources = []
602
606
  for source in sources:
@@ -615,7 +619,7 @@ def convert_sources_to_one_platform_sources(sources: List[Any]) -> List[Any]:
615
619
  return converted_sources
616
620
 
617
621
 
618
- def encode_number_as_cloud_value(number: float) -> Dict[str, Union[float, str]]:
622
+ def encode_number_as_cloud_value(number: float) -> dict[str, float | str]:
619
623
  # Numeric values in constantValue-style nodes end up stored in doubles. If the
620
624
  # input is an integer that loses precision as a double, use the int64 slot
621
625
  # ("integerValue") in ValueNode.
@@ -625,7 +629,7 @@ def encode_number_as_cloud_value(number: float) -> Dict[str, Union[float, str]]:
625
629
  return {'constantValue': number}
626
630
 
627
631
 
628
- def convert_algorithms(algorithms) -> Dict[str, Any]:
632
+ def convert_algorithms(algorithms) -> dict[str, Any]:
629
633
  """Converts a ListAlgorithmsResult to the internal format.
630
634
 
631
635
  The internal code expects a dict mapping each algorithm's name to a dict
@@ -653,7 +657,7 @@ def convert_algorithms(algorithms) -> Dict[str, Any]:
653
657
  return dict(_convert_algorithm(algorithm) for algorithm in algs)
654
658
 
655
659
 
656
- def _convert_algorithm(algorithm: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
660
+ def _convert_algorithm(algorithm: dict[str, Any]) -> tuple[str, dict[str, Any]]:
657
661
  """Converts an Algorithm to the internal format."""
658
662
  # Strip leading 'algorithms/' from the name.
659
663
  algorithm_name = algorithm['name'][11:]
@@ -677,11 +681,11 @@ def _convert_algorithm(algorithm: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
677
681
 
678
682
 
679
683
  def _convert_algorithm_arguments(
680
- args: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
684
+ args: list[dict[str, Any]]) -> list[dict[str, Any]]:
681
685
  return [_convert_algorithm_argument(arg) for arg in args]
682
686
 
683
687
 
684
- def _convert_algorithm_argument(arg: Dict[str, Any]) -> Dict[str, Any]:
688
+ def _convert_algorithm_argument(arg: dict[str, Any]) -> dict[str, Any]:
685
689
  return _convert_dict(
686
690
  arg, {
687
691
  'argumentName': 'name',
@@ -696,7 +700,7 @@ def _convert_algorithm_argument(arg: Dict[str, Any]) -> Dict[str, Any]:
696
700
  })
697
701
 
698
702
 
699
- def convert_to_image_file_format(format_str: Optional[str]) -> str:
703
+ def convert_to_image_file_format(format_str: str | None) -> str:
700
704
  """Converts a legacy file format string to an ImageFileFormat enum value.
701
705
 
702
706
  Args:
@@ -723,7 +727,7 @@ def convert_to_image_file_format(format_str: Optional[str]) -> str:
723
727
  return format_str
724
728
 
725
729
 
726
- def convert_to_table_file_format(format_str: Optional[str]) -> str:
730
+ def convert_to_table_file_format(format_str: str | None) -> str:
727
731
  """Converts a legacy file format string to a TableFileFormat enum value.
728
732
 
729
733
  Args:
@@ -746,7 +750,7 @@ def convert_to_table_file_format(format_str: Optional[str]) -> str:
746
750
  return format_str
747
751
 
748
752
 
749
- def convert_to_band_list(bands: Union[List[str], None, str]) -> List[str]:
753
+ def convert_to_band_list(bands: list[str] | None | str) -> list[str]:
750
754
  """Converts a band list, possibly as CSV, to a real list of bands.
751
755
 
752
756
  Args:
@@ -766,7 +770,7 @@ def convert_to_band_list(bands: Union[List[str], None, str]) -> List[str]:
766
770
  raise ee_exception.EEException('Invalid band list ' + bands)
767
771
 
768
772
 
769
- def convert_to_visualization_options(params: Dict[str, Any]) -> Dict[str, Any]:
773
+ def convert_to_visualization_options(params: dict[str, Any]) -> dict[str, Any]:
770
774
  """Extracts a VisualizationOptions from a param dict.
771
775
 
772
776
  Args:
@@ -829,14 +833,14 @@ def convert_to_visualization_options(params: Dict[str, Any]) -> Dict[str, Any]:
829
833
  return result
830
834
 
831
835
 
832
- def _convert_csv_numbers_to_list(value: str) -> List[float]:
836
+ def _convert_csv_numbers_to_list(value: str) -> list[float]:
833
837
  """Converts a string containing CSV numbers to a list."""
834
838
  if not value:
835
839
  return []
836
840
  return [float(x) for x in value.split(',')]
837
841
 
838
842
 
839
- def convert_operation_to_task(operation: Dict[str, Any]) -> Dict[str, Any]:
843
+ def convert_operation_to_task(operation: dict[str, Any]) -> dict[str, Any]:
840
844
  """Converts an Operation to a legacy Task."""
841
845
  result = _convert_dict(
842
846
  operation['metadata'], {
@@ -860,19 +864,32 @@ def convert_operation_to_task(operation: Dict[str, Any]) -> Dict[str, Any]:
860
864
 
861
865
 
862
866
  def _convert_operation_state_to_task_state(state: str) -> str:
863
- """Converts a state string from an Operation to the Task equivalent."""
867
+ """Converts an Operation state to a Task state."""
864
868
  return _convert_value(
865
- state, {
866
- 'PENDING': 'READY',
867
- 'RUNNING': 'RUNNING',
868
- 'CANCELLING': 'CANCEL_REQUESTED',
869
- 'SUCCEEDED': 'COMPLETED',
870
- 'CANCELLED': 'CANCELLED',
871
- 'FAILED': 'FAILED'
872
- }, 'UNKNOWN')
869
+ state,
870
+ {value: key for key, value in TASK_TO_OPERATION_STATE.items()},
871
+ 'UNKNOWN',
872
+ )
873
+
874
+
875
+ def _convert_task_state_to_operation_state(state: str) -> str:
876
+ """Converts a Task state to an Operation state."""
877
+ return _convert_value(state, TASK_TO_OPERATION_STATE, 'UNKNOWN')
878
+
879
+
880
+ def convert_to_operation_state(state: str) -> str:
881
+ """Converts a Task state or an Operation state to an Operation state."""
882
+ # First, try converting the state assuming it's a task state.
883
+ operation_state = _convert_task_state_to_operation_state(state)
884
+ if operation_state != 'UNKNOWN':
885
+ return operation_state
886
+
887
+ # If it wasn't a task state, check if the input is a valid operation state.
888
+ valid_operation_states = set(TASK_TO_OPERATION_STATE.values())
889
+ return state if state in valid_operation_states else 'UNKNOWN'
873
890
 
874
891
 
875
- def convert_iam_policy_to_acl(policy: Dict[str, Any]) -> Dict[str, Any]:
892
+ def convert_iam_policy_to_acl(policy: dict[str, Any]) -> dict[str, Any]:
876
893
  """Converts an IAM Policy proto to the legacy ACL format."""
877
894
  bindings = {
878
895
  binding['role']: binding.get('members', [])
@@ -892,7 +909,7 @@ def convert_iam_policy_to_acl(policy: Dict[str, Any]) -> Dict[str, Any]:
892
909
  return result
893
910
 
894
911
 
895
- def convert_acl_to_iam_policy(acl: Dict[str, Any]) -> Dict[str, Any]:
912
+ def convert_acl_to_iam_policy(acl: dict[str, Any]) -> dict[str, Any]:
896
913
  """Converts the legacy ACL format to an IAM Policy proto."""
897
914
  owners = acl.get('owners', [])
898
915
  readers = acl.get('readers', [])
@@ -910,8 +927,8 @@ def convert_acl_to_iam_policy(acl: Dict[str, Any]) -> Dict[str, Any]:
910
927
 
911
928
 
912
929
  def convert_to_grid_dimensions(
913
- dimensions: Union[float, Sequence[float]]
914
- ) -> Dict[str, float]:
930
+ dimensions: float | Sequence[float]
931
+ ) -> dict[str, float]:
915
932
  """Converts an input value to GridDimensions.
916
933
 
917
934
  Args:
ee/_helpers.py CHANGED
@@ -7,10 +7,11 @@ referenced from there (e.g., "ee.profilePrinting").
7
7
  # Using lowercase function naming to match the JavaScript names.
8
8
  # pylint: disable=g-bad-name
9
9
 
10
+ from collections.abc import Iterator
10
11
  import contextlib
11
12
  import json
12
13
  import sys
13
- from typing import Any, Dict, Iterator, Optional, TextIO, Union
14
+ from typing import Any, TextIO
14
15
 
15
16
  from google.auth import crypt
16
17
  from google.oauth2 import service_account
@@ -21,13 +22,14 @@ from ee import data
21
22
  from ee import ee_exception
22
23
  from ee import oauth
23
24
 
24
-
25
25
  # Number of times to retry fetching profile data.
26
26
  _PROFILE_RETRIES = 5
27
27
 
28
28
 
29
29
  def ServiceAccountCredentials(
30
- email: str, key_file: Optional[str] = None, key_data: Optional[str] = None
30
+ email: str | None = None,
31
+ key_file: str | None = None,
32
+ key_data: str | None = None,
31
33
  ) -> service_account.Credentials:
32
34
  """Configure OAuth2 credentials for a Google Service Account.
33
35
 
@@ -41,6 +43,10 @@ def ServiceAccountCredentials(
41
43
  Returns:
42
44
  An OAuth2 credentials object.
43
45
  """
46
+ if not email and not key_file and not key_data:
47
+ raise ValueError(
48
+ 'At least one of email, key_file, or key_data must be specified.'
49
+ )
44
50
 
45
51
  # Assume anything that doesn't end in '.pem' is a JSON key.
46
52
  if key_file and not key_file.endswith('.pem'):
@@ -59,7 +65,7 @@ def ServiceAccountCredentials(
59
65
 
60
66
  # Probably a PEM key - just read the file into 'key_data'.
61
67
  if key_file:
62
- with open(key_file, 'r') as file_:
68
+ with open(key_file) as file_:
63
69
  key_data = file_.read()
64
70
 
65
71
  # Raw PEM key.
@@ -69,7 +75,7 @@ def ServiceAccountCredentials(
69
75
 
70
76
 
71
77
  def call(
72
- func: Union[str, apifunction.ApiFunction], *args, **kwargs
78
+ func: str | apifunction.ApiFunction, *args, **kwargs
73
79
  ) -> computedobject.ComputedObject:
74
80
  """Invoke the given algorithm with the specified args.
75
81
 
@@ -91,7 +97,7 @@ def call(
91
97
 
92
98
  # pylint: disable-next=redefined-builtin
93
99
  def apply(
94
- func: Union[str, apifunction.ApiFunction], named_args: Dict[str, Any]
100
+ func: str | apifunction.ApiFunction, named_args: dict[str, Any]
95
101
  ) -> computedobject.ComputedObject:
96
102
  """Call a function with a dictionary of named arguments.
97
103
 
@@ -118,14 +124,13 @@ def profilePrinting(destination: TextIO = sys.stderr) -> Iterator[None]:
118
124
  The profile will be printed when the context ends, whether or not any error
119
125
  occurred within the context.
120
126
 
121
- # Simple example:
122
- with ee.profilePrinting():
123
- print ee.Number(1).add(1).getInfo()
127
+ Example:
128
+ with ee.profilePrinting():
129
+ print(ee.Number(1).add(1).getInfo())
124
130
 
125
131
  Args:
126
132
  destination: A file-like object to which the profile text is written.
127
133
  Defaults to sys.stderr.
128
-
129
134
  """
130
135
  # Profile.getProfiles is `hidden`, so call it explicitly.
131
136
  get_profiles = apifunction.ApiFunction.lookup('Profile.getProfiles').call
@@ -135,12 +140,11 @@ def profilePrinting(destination: TextIO = sys.stderr) -> Iterator[None]:
135
140
  yield
136
141
  finally:
137
142
  # Make several attempts in case of transient errors.
138
- attempts = _PROFILE_RETRIES
139
- for i in range(_PROFILE_RETRIES):
143
+ for attempt in range(_PROFILE_RETRIES):
140
144
  try:
141
145
  profile_text = get_profiles(ids=profile_ids).getInfo()
142
146
  destination.write(profile_text)
143
147
  break
144
148
  except ee_exception.EEException as exception:
145
- if i == attempts - 1:
149
+ if attempt == _PROFILE_RETRIES - 1:
146
150
  raise exception
ee/_state.py ADDED
@@ -0,0 +1,105 @@
1
+ """Stores the current state of the EE library."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import re
7
+ from typing import Any
8
+
9
+ # Rename to avoid redefined-outer-name warning.
10
+ from google.oauth2 import credentials as credentials_lib
11
+ import requests
12
+
13
+
14
+ # The default project to use for Cloud API calls.
15
+ DEFAULT_CLOUD_API_USER_PROJECT = 'earthengine-legacy'
16
+
17
+
18
+ @dataclasses.dataclass()
19
+ class _WorkloadTag:
20
+ """A helper class to manage the workload tag."""
21
+
22
+ _tag: str
23
+ _default: str
24
+
25
+ def __init__(self):
26
+ self._tag = ''
27
+ self._default = ''
28
+
29
+ def get(self) -> str:
30
+ return self._tag
31
+
32
+ def set(self, tag: int | str | None) -> None:
33
+ self._tag = self.validate(tag)
34
+
35
+ def set_default(self, new_default: int | str | None) -> None:
36
+ self._default = self.validate(new_default)
37
+
38
+ def reset(self) -> None:
39
+ self._tag = self._default
40
+
41
+ def validate(self, tag: int | str | None) -> str:
42
+ """Returns the validated tag or raises a ValueError.
43
+
44
+ Args:
45
+ tag: the tag to validate.
46
+
47
+ Raises:
48
+ ValueError if the tag does not match the expected format.
49
+ """
50
+ if not tag and tag != 0:
51
+ return ''
52
+ tag = str(tag)
53
+ if not re.fullmatch(r'([a-z0-9]|[a-z0-9][-_a-z0-9]{0,61}[a-z0-9])', tag):
54
+ validation_message = (
55
+ 'Tags must be 1-63 characters, '
56
+ 'beginning and ending with a lowercase alphanumeric character '
57
+ '([a-z0-9]) with dashes (-), underscores (_), '
58
+ 'and lowercase alphanumerics between.'
59
+ )
60
+ raise ValueError(f'Invalid tag, "{tag}". {validation_message}')
61
+ return tag
62
+
63
+
64
+ @dataclasses.dataclass()
65
+ class EEState:
66
+ """Holds the configuration and initialized resources for an EE session."""
67
+
68
+ initialized: bool = False
69
+ credentials: credentials_lib.Credentials | None = None
70
+ api_base_url: str | None = None
71
+ tile_base_url: str | None = None
72
+ cloud_api_base_url: str | None = None
73
+ cloud_api_key: str | None = None
74
+ requests_session: requests.Session | None = None
75
+ cloud_api_resource: Any | None = None
76
+ cloud_api_resource_raw: Any | None = None
77
+ cloud_api_user_project: str | None = DEFAULT_CLOUD_API_USER_PROJECT
78
+ cloud_api_client_version: str | None = None
79
+ http_transport: Any | None = None
80
+ deadline_ms: int = 0
81
+ max_retries: int = 5
82
+ user_agent: str | None = None
83
+ workload_tag: _WorkloadTag = dataclasses.field(default_factory=_WorkloadTag)
84
+
85
+
86
+ _state: EEState | None = None
87
+
88
+
89
+ def get_state() -> EEState:
90
+ """Retrieves the EEState for the current execution context.
91
+
92
+ Returns:
93
+ The EEState for the current execution context.
94
+ """
95
+
96
+ global _state
97
+ if not _state:
98
+ _state = EEState()
99
+ return _state
100
+
101
+
102
+ def reset_state() -> None:
103
+ """Resets the EEState to the default state."""
104
+ global _state
105
+ _state = None
ee/_utils.py CHANGED
@@ -1,7 +1,8 @@
1
1
  """General decorators and helper methods which should not import ee."""
2
2
 
3
3
  import functools
4
- from typing import Any, Callable
4
+ from typing import Any
5
+ from collections.abc import Callable
5
6
 
6
7
 
7
8
  def accept_opt_prefix(*opt_args) -> Callable[..., Any]: