earthengine-api 1.6.13__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 (44) hide show
  1. {earthengine_api-1.6.13.dist-info → earthengine_api-1.7.4.dist-info}/METADATA +2 -3
  2. {earthengine_api-1.6.13.dist-info → earthengine_api-1.7.4.dist-info}/RECORD +44 -44
  3. ee/__init__.py +13 -13
  4. ee/_cloud_api_utils.py +29 -28
  5. ee/_helpers.py +6 -6
  6. ee/_utils.py +2 -1
  7. ee/apitestcase.py +10 -10
  8. ee/batch.py +7 -0
  9. ee/cli/commands.py +6 -10
  10. ee/cli/utils.py +28 -23
  11. ee/collection.py +3 -2
  12. ee/computedobject.py +4 -1
  13. ee/customfunction.py +2 -1
  14. ee/data.py +32 -31
  15. ee/deprecation.py +8 -2
  16. ee/deserializer.py +10 -10
  17. ee/ee_number.py +6 -16
  18. ee/encodable.py +2 -1
  19. ee/image_converter.py +3 -3
  20. ee/imagecollection.py +2 -2
  21. ee/model.py +29 -31
  22. ee/oauth.py +37 -37
  23. ee/reducer.py +2 -0
  24. ee/serializer.py +6 -6
  25. ee/table_converter.py +3 -3
  26. ee/terrain.py +1 -1
  27. ee/tests/batch_test.py +67 -3
  28. ee/tests/collection_test.py +35 -0
  29. ee/tests/data_test.py +300 -4
  30. ee/tests/deprecation_test.py +4 -2
  31. ee/tests/deserializer_test.py +47 -0
  32. ee/tests/ee_number_test.py +40 -1
  33. ee/tests/image_converter_test.py +1 -3
  34. ee/tests/kernel_test.py +4 -0
  35. ee/tests/model_test.py +12 -0
  36. ee/tests/oauth_test.py +170 -7
  37. ee/tests/reducer_test.py +13 -0
  38. ee/tests/serializer_test.py +29 -2
  39. ee/tests/table_converter_test.py +49 -5
  40. ee/tests/terrain_test.py +8 -0
  41. {earthengine_api-1.6.13.dist-info → earthengine_api-1.7.4.dist-info}/WHEEL +0 -0
  42. {earthengine_api-1.6.13.dist-info → earthengine_api-1.7.4.dist-info}/entry_points.txt +0 -0
  43. {earthengine_api-1.6.13.dist-info → earthengine_api-1.7.4.dist-info}/licenses/LICENSE +0 -0
  44. {earthengine_api-1.6.13.dist-info → earthengine_api-1.7.4.dist-info}/top_level.txt +0 -0
ee/tests/oauth_test.py CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """Test for the oauth module."""
3
3
 
4
+ import http.client
5
+ import io
4
6
  import json
5
7
  import sys
6
8
  import tempfile
@@ -34,20 +36,33 @@ class OAuthTest(unittest.TestCase):
34
36
  self.assertEqual('xyz', parsed[b'code_verifier'][0].decode())
35
37
  return MockResponse(parsed[b'code'][0])
36
38
 
37
- with mock.patch('urllib.request.urlopen', new=mock_urlopen):
39
+ with mock.patch.object(urllib.request, 'urlopen', new=mock_urlopen):
38
40
  auth_code = '123'
39
41
  verifier = 'xyz'
40
42
  refresh_token = oauth.request_token(auth_code, verifier)
41
43
  self.assertEqual('123456', refresh_token)
42
44
 
45
+ def test_request_token_http_error(self):
46
+ mock_fp = io.BytesIO(b'error details')
47
+ http_error = urllib.error.HTTPError(
48
+ 'url', 400, 'message', hdrs=http.client.HTTPMessage(), fp=mock_fp
49
+ )
50
+ with mock.patch.object(urllib.request, 'urlopen', side_effect=http_error):
51
+ with self.assertRaisesRegex(
52
+ Exception,
53
+ r"Problem requesting tokens.*HTTP Error 400: message.*b'error"
54
+ r" details'",
55
+ ):
56
+ oauth.request_token('auth_code', 'code_verifier')
57
+
43
58
  def test_write_token(self):
44
59
 
45
60
  def mock_credentials_path():
46
- return self.test_tmpdir + '/tempfile'
61
+ return f'{self.test_tmpdir}/tempfile'
47
62
 
48
- oauth_pkg = 'ee.oauth'
49
- with mock.patch(
50
- oauth_pkg + '.get_credentials_path', new=mock_credentials_path):
63
+ with mock.patch.object(
64
+ oauth, 'get_credentials_path', new=mock_credentials_path
65
+ ):
51
66
  client_info = dict(refresh_token='123')
52
67
  oauth.write_private_json(oauth.get_credentials_path(), client_info)
53
68
 
@@ -55,6 +70,46 @@ class OAuthTest(unittest.TestCase):
55
70
  token = json.load(f)
56
71
  self.assertEqual({'refresh_token': '123'}, token)
57
72
 
73
+ def test_get_credentials_arguments(self):
74
+ credentials_path = f'{self.test_tmpdir}/temp_creds'
75
+
76
+ creds = {
77
+ 'refresh_token': 'REFRESH_TOKEN',
78
+ 'client_id': 'CLIENT_ID',
79
+ 'project': 'PROJECT',
80
+ }
81
+ with open(credentials_path, 'w') as f:
82
+ json.dump(creds, f)
83
+
84
+ with mock.patch.object(
85
+ oauth, 'get_credentials_path', return_value=credentials_path
86
+ ):
87
+ args = oauth.get_credentials_arguments()
88
+
89
+ expected_args = {
90
+ 'token_uri': oauth.TOKEN_URI,
91
+ 'refresh_token': 'REFRESH_TOKEN',
92
+ 'client_id': 'CLIENT_ID',
93
+ 'client_secret': oauth.CLIENT_SECRET,
94
+ 'scopes': oauth.SCOPES,
95
+ 'quota_project_id': 'PROJECT',
96
+ }
97
+ self.assertEqual(expected_args, args)
98
+
99
+ def test_is_valid_credentials(self):
100
+ self.assertFalse(oauth.is_valid_credentials(None))
101
+
102
+ mock_credentials_valid = mock.MagicMock()
103
+ self.assertTrue(oauth.is_valid_credentials(mock_credentials_valid))
104
+ mock_credentials_valid.refresh.assert_called_once()
105
+
106
+ mock_credentials_invalid = mock.MagicMock()
107
+ mock_credentials_invalid.refresh.side_effect = (
108
+ oauth.google.auth.exceptions.RefreshError()
109
+ )
110
+ self.assertFalse(oauth.is_valid_credentials(mock_credentials_invalid))
111
+ mock_credentials_invalid.refresh.assert_called_once()
112
+
58
113
  def test_in_colab_shell(self):
59
114
  with mock.patch.dict(sys.modules, {'google.colab': None}):
60
115
  self.assertFalse(oauth.in_colab_shell())
@@ -78,7 +133,7 @@ class OAuthTest(unittest.TestCase):
78
133
  )
79
134
  )
80
135
 
81
- def testAuthenticate_colabAuthModeWithNonstandardScopes_raisesException(self):
136
+ def test_colab_mode_with_nonstandard_scopes_raises_exception(self):
82
137
  with self.assertRaisesRegex(
83
138
  ee_exception.EEException,
84
139
  'Scopes cannot be customized when auth_mode is "colab".'
@@ -88,7 +143,7 @@ class OAuthTest(unittest.TestCase):
88
143
  scopes=['https://www.googleapis.com/auth/earthengine.readonly']
89
144
  )
90
145
 
91
- def testAuthenticate_colabAuthModeWithStandardScopes_succeeds(self):
146
+ def test_colab_auth_mode_with_standard_scopes_succeeds(self):
92
147
  # Should not raise an exception if the scopes are not narrowed.
93
148
  with mock.patch.dict(sys.modules, {'google.colab': mock.MagicMock()}):
94
149
  try:
@@ -96,5 +151,113 @@ class OAuthTest(unittest.TestCase):
96
151
  except ee_exception.EEException:
97
152
  self.fail('authenticate raised an exception unexpectedly.')
98
153
 
154
+ def test_authenticate_appdefault(self):
155
+ with mock.patch.object(
156
+ oauth,
157
+ '_valid_credentials_exist',
158
+ return_value=False,
159
+ ), mock.patch.object(
160
+ sys, 'stderr', new_callable=io.StringIO
161
+ ) as mock_stderr:
162
+ oauth.authenticate(auth_mode='appdefault')
163
+ self.assertIn('appdefault no longer necessary', mock_stderr.getvalue())
164
+
165
+ @mock.patch.object(oauth, '_load_gcloud_credentials')
166
+ @mock.patch.object(oauth, '_valid_credentials_exist', return_value=False)
167
+ @mock.patch.object(oauth, 'in_colab_shell', return_value=False)
168
+ @mock.patch.object(oauth, '_in_jupyter_shell', return_value=False)
169
+ @mock.patch.object(oauth, '_localhost_is_viable', return_value=False)
170
+ def test_authenticate_default_gcloud(
171
+ self,
172
+ mock_localhost_viable,
173
+ mock_jupyter,
174
+ mock_colab,
175
+ mock_valid_creds,
176
+ mock_load_gcloud,
177
+ ):
178
+ del (
179
+ mock_localhost_viable,
180
+ mock_jupyter,
181
+ mock_colab,
182
+ mock_valid_creds,
183
+ ) # Unused
184
+ oauth.authenticate()
185
+ mock_load_gcloud.assert_called_once_with(None, None, False)
186
+
187
+ def test_localhost_fetch_code(self):
188
+ mock_server = mock.MagicMock()
189
+ mock_server.url = 'http://localhost:8085'
190
+ mock_server.fetch_code.return_value = 'FETCHED_CODE'
191
+ with mock.patch.object(
192
+ oauth, '_start_server', return_value=mock_server
193
+ ), mock.patch.object(oauth, '_obtain_and_write_token') as mock_obtain:
194
+ flow = oauth.Flow(auth_mode='localhost')
195
+ flow.save_code()
196
+ mock_server.fetch_code.assert_called_once()
197
+ mock_obtain.assert_called_once_with(
198
+ 'FETCHED_CODE',
199
+ flow.code_verifier,
200
+ flow.scopes,
201
+ 'http://localhost:8085',
202
+ )
203
+
204
+ @mock.patch.object(oauth, '_display_auth_instructions_for_noninteractive')
205
+ def test_display_instructions_quiet(self, mock_display):
206
+ flow = oauth.Flow(auth_mode='notebook')
207
+ self.assertTrue(flow.display_instructions(quiet=True))
208
+ mock_display.assert_called_once_with(flow.auth_url, flow.code_verifier)
209
+
210
+ @mock.patch.object(oauth, '_display_auth_instructions_with_print')
211
+ @mock.patch.object(oauth, 'in_colab_shell', return_value=True)
212
+ def test_display_instructions_colab(self, mock_in_colab, mock_display_print):
213
+ del mock_in_colab # Unused
214
+ flow = oauth.Flow(auth_mode='notebook')
215
+ self.assertTrue(flow.display_instructions())
216
+ mock_display_print.assert_called_once_with(flow.auth_url, None)
217
+
218
+ @mock.patch.object(oauth, '_display_auth_instructions_with_html')
219
+ @mock.patch.object(oauth, '_in_jupyter_shell', return_value=True)
220
+ def test_display_instructions_jupyter(
221
+ self, mock_in_jupyter, mock_display_html
222
+ ):
223
+ del mock_in_jupyter # Unused
224
+ flow = oauth.Flow(auth_mode='notebook')
225
+ self.assertTrue(flow.display_instructions())
226
+ mock_display_html.assert_called_once_with(flow.auth_url, None)
227
+
228
+ @mock.patch.object(oauth, '_display_auth_instructions_with_print')
229
+ @mock.patch.object(oauth, 'in_colab_shell', return_value=False)
230
+ @mock.patch.object(oauth, '_in_jupyter_shell', return_value=False)
231
+ def test_display_instructions_print(
232
+ self, mock_in_jupyter, mock_in_colab, mock_display_print
233
+ ):
234
+ del mock_in_jupyter, mock_in_colab # Unused
235
+ flow = oauth.Flow(auth_mode='notebook')
236
+ self.assertTrue(flow.display_instructions())
237
+ mock_display_print.assert_called_once_with(flow.auth_url, None)
238
+
239
+ @mock.patch.object(oauth, '_display_auth_instructions_with_print')
240
+ @mock.patch.object(oauth, 'in_colab_shell', return_value=True)
241
+ @mock.patch.object(oauth.http.server, 'HTTPServer')
242
+ def test_display_instructions_localhost_colab(
243
+ self, mock_http_server, mock_in_colab, mock_display_print
244
+ ):
245
+ del mock_http_server, mock_in_colab # Unused
246
+ flow = oauth.Flow(auth_mode='localhost')
247
+ self.assertTrue(flow.display_instructions())
248
+ mock_display_print.assert_called_once_with(
249
+ flow.auth_url, oauth.WAITING_CODA
250
+ )
251
+
252
+ def test_flow_unknown_auth_mode(self):
253
+ with self.assertRaisesRegex(ee_exception.EEException, 'Unknown auth_mode'):
254
+ oauth.Flow(auth_mode='unknown')
255
+
256
+ def test_flow_localhost_with_port(self):
257
+ with mock.patch.object(oauth, '_start_server') as mock_start_server:
258
+ oauth.Flow(auth_mode='localhost:1234')
259
+ mock_start_server.assert_called_once_with(1234)
260
+
261
+
99
262
  if __name__ == '__main__':
100
263
  unittest.main()
ee/tests/reducer_test.py CHANGED
@@ -809,6 +809,19 @@ class ReducerTest(apitestcase.ApiTestCase):
809
809
  result = json.loads(expression.serialize())
810
810
  self.assertEqual(expect, result)
811
811
 
812
+ def test_ridge_regression_kwargs_with_lambda(self):
813
+ with self.assertRaisesRegex(
814
+ ValueError, 'lambda_ cannot be set when providing kwargs.'
815
+ ):
816
+ ee.Reducer.ridgeRegression(1, lambda_=3, **{'lambda': 4})
817
+
818
+ def test_ridge_regression_unexpected_kwargs(self):
819
+ with self.assertRaisesRegex(
820
+ ValueError,
821
+ r"Unexpected arguments: \['unexpected'\]\. Expected: lambda.",
822
+ ):
823
+ ee.Reducer.ridgeRegression(1, unexpected=4)
824
+
812
825
  def test_robust_linear_regression(self):
813
826
  num_x = 1
814
827
  num_y = 2
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
  """Test for the ee.serializer module."""
3
3
 
4
+ from collections.abc import Callable
4
5
  import datetime
5
6
  import json
6
- from typing import Any, Callable, Union
7
+ from typing import Any
7
8
 
8
9
  import unittest
9
10
  import ee
@@ -11,7 +12,7 @@ from ee import apitestcase
11
12
  from ee import serializer
12
13
 
13
14
 
14
- def _max_depth(x: Union[dict[str, Any], list[Any], str]) -> int:
15
+ def _max_depth(x: dict[str, Any] | list[Any] | str) -> int:
15
16
  """Computes the maximum nesting level of some dict, list, or str."""
16
17
  if isinstance(x, dict):
17
18
  return 1 + max(_max_depth(v) for v in x.values())
@@ -284,6 +285,32 @@ class SerializerTest(apitestcase.ApiTestCase):
284
285
  self.assertIn('\n', serializer.toJSON(ee.Image(0), opt_pretty=True))
285
286
  self.assertNotIn('\n', serializer.toJSON(ee.Image(0), opt_pretty=False))
286
287
 
288
+ def test_datetime_serialization_legacy(self):
289
+ a_date = datetime.datetime(2014, 8, 10, tzinfo=datetime.timezone.utc)
290
+ # 1407628800000000 microseconds.
291
+ expected = {
292
+ 'type': 'CompoundValue',
293
+ 'scope': [],
294
+ 'value': {
295
+ 'type': 'Invocation',
296
+ 'functionName': 'Date',
297
+ 'arguments': {'value': 1407628800000.0},
298
+ },
299
+ }
300
+ self.assertEqual(expected, serializer.encode(a_date, for_cloud_api=False))
301
+
302
+ def test_single_value_no_compound(self):
303
+ """Verifies serialization of a single non-primitive value."""
304
+ self.assertEqual([1], serializer.encode([1], for_cloud_api=False))
305
+
306
+ def test_unencodable_object(self):
307
+ class Unencodable:
308
+ pass
309
+ with self.assertRaisesRegex(ee.EEException, 'Cannot encode object: .*'):
310
+ serializer.encode(Unencodable(), for_cloud_api=False)
311
+ with self.assertRaisesRegex(ee.EEException, 'Cannot encode object: .*'):
312
+ serializer.encode(Unencodable(), for_cloud_api=True)
313
+
287
314
 
288
315
  if __name__ == '__main__':
289
316
  unittest.main()
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  """Tests for the table_converter module."""
3
3
 
4
- from typing import Any, Optional
4
+ import builtins
5
+ from typing import Any
6
+ from unittest import mock
5
7
 
6
8
  from absl.testing import parameterized
7
9
  import geopandas
@@ -28,8 +30,8 @@ class TableConverterTest(parameterized.TestCase):
28
30
  def test_from_file_format(
29
31
  self,
30
32
  data_format: str,
31
- expected: Optional[type[table_converter.TableConverter]],
32
- ) -> None:
33
+ expected: type[table_converter.TableConverter] | None,
34
+ ):
33
35
  """Verifies `from_file_format` returns the correct converter class."""
34
36
  if expected is None:
35
37
  self.assertIsNone(table_converter.from_file_format(data_format))
@@ -38,7 +40,17 @@ class TableConverterTest(parameterized.TestCase):
38
40
  table_converter.from_file_format(data_format), expected
39
41
  )
40
42
 
41
- def test_pandas_converter(self) -> None:
43
+ def test_from_file_format_instance(self):
44
+ """Verifies `from_file_format` returns the same instance."""
45
+ converter = table_converter.PandasConverter()
46
+ self.assertIs(table_converter.from_file_format(converter), converter)
47
+
48
+ def test_table_converter_fails(self):
49
+ """Verifies `TableConverter` cannot be used for conversion."""
50
+ with self.assertRaises(NotImplementedError):
51
+ table_converter.TableConverter().do_conversion(iter([]))
52
+
53
+ def test_pandas_converter(self):
42
54
  """Verifies `PandasConverter` does the correct conversion."""
43
55
  converter = table_converter.PandasConverter()
44
56
 
@@ -69,7 +81,23 @@ class TableConverterTest(parameterized.TestCase):
69
81
  ]),
70
82
  )
71
83
 
72
- def test_geopandas_converter(self) -> None:
84
+ def test_pandas_converter_importerror(self):
85
+ """Ensures ImportError is raised when pandas is not available."""
86
+ real_import = builtins.__import__
87
+
88
+ def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
89
+ if name == 'pandas':
90
+ raise ImportError
91
+ return real_import(name, globals, locals, fromlist, level)
92
+
93
+ with mock.patch('builtins.__import__', mock_import):
94
+ converter = table_converter.PandasConverter()
95
+ with self.assertRaisesRegex(
96
+ ImportError, 'Using format PANDAS_DATAFRAME requires pandas.'
97
+ ):
98
+ converter.do_conversion(iter([]))
99
+
100
+ def test_geopandas_converter(self):
73
101
  """Verifies `GeoPandasConverter` does the correct conversion."""
74
102
  converter = table_converter.GeoPandasConverter()
75
103
 
@@ -105,6 +133,22 @@ class TableConverterTest(parameterized.TestCase):
105
133
  geopandas.GeoDataFrame.from_features(feature_coll),
106
134
  )
107
135
 
136
+ def test_geopandas_converter_importerror(self):
137
+ """Ensures ImportError is raised when geopandas is not available."""
138
+ real_import = builtins.__import__
139
+
140
+ def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
141
+ if name == 'geopandas':
142
+ raise ImportError
143
+ return real_import(name, globals, locals, fromlist, level)
144
+
145
+ with mock.patch('builtins.__import__', mock_import):
146
+ converter = table_converter.GeoPandasConverter()
147
+ with self.assertRaisesRegex(
148
+ ImportError, 'Using format GEOPANDAS_GEODATAFRAME requires geopandas.'
149
+ ):
150
+ converter.do_conversion(iter([]))
151
+
108
152
 
109
153
  if __name__ == '__main__':
110
154
  unittest.main()
ee/tests/terrain_test.py CHANGED
@@ -141,6 +141,14 @@ class TerrainTest(apitestcase.ApiTestCase):
141
141
  result = json.loads(expression.serialize())
142
142
  self.assertEqual(expect, result)
143
143
 
144
+ def test_init(self):
145
+ message = r'Terrain should not be used as an object.*'
146
+ with self.assertRaisesRegex(RuntimeError, message):
147
+ ee.Terrain()
148
+
149
+ def test_name(self):
150
+ self.assertEqual('Terrain', ee.Terrain.name())
151
+
144
152
 
145
153
  if __name__ == '__main__':
146
154
  unittest.main()