From b85f7afd549a79bbbf1a427cfa74949b61872c4d Mon Sep 17 00:00:00 2001 From: Google Earth Engine Authors Date: Thu, 4 Apr 2024 15:47:32 +0000 Subject: [PATCH] v0.1.398 PiperOrigin-RevId: 621871759 --- javascript/build/ee_api_js.js | 32 +-- javascript/build/ee_api_js_debug.js | 176 ++++++++--------- javascript/build/ee_api_js_npm.js | 222 ++++++++++----------- javascript/package.json | 2 +- javascript/src/apiclient.js | 2 +- python/ee/__init__.py | 2 +- python/ee/data.py | 16 +- python/ee/dictionary.py | 283 ++++++++++++++++++++++++++ python/ee/tests/data_test.py | 22 +++ python/ee/tests/dictionary_test.py | 294 ++++++++++++++++++++++++++++ python/pyproject.toml | 2 +- 11 files changed, 833 insertions(+), 220 deletions(-) diff --git a/javascript/build/ee_api_js.js b/javascript/build/ee_api_js.js index 76fea2747..302e9403d 100644 --- a/javascript/build/ee_api_js.js +++ b/javascript/build/ee_api_js.js @@ -456,7 +456,7 @@ Wc(p,f[m]))});return b?b(l):l};return this.callback?(Hj(d,null,function(f,l){ret Ij.prototype.send=function(a,b){var c=[a.B+" "+a.path+" HTTP/1.1"];c.push("Content-Type: application/json; charset=utf-8");var d=Kj();null!=d&&c.push("Authorization: "+d);a=a.body?JSON.stringify(a.body):"";return[c.join("\r\n")+"\r\n\r\n"+a,b]}; var Lj=function(a,b,c){a=n(b.split("--"+a.split("; boundary=")[1]));for(b=a.next();!b.done;b=a.next())if(b=b.value.split("\r\n\r\n"),!(3>b.length)){var d=b[0].match(/\r\nContent-ID: ]*)>/)[1],e=Number(b[1].match(/^HTTP\S*\s(\d+)\s/)[1]);c(d,e,b.slice(2).join("\r\n\r\n"))}},Fj=function(){var a=Mj.replace(/\/api$/,"");return"window"in r&&!a.match(/^https?:\/\/content-/)?a.replace(/^(https?:\/\/)(.*\.googleapis\.com)$/,"$1content-$2"):a},Oj=function(a,b,c){var d=[];a&&(d=d.concat(Nj)); b&&d.push("https://www.googleapis.com/auth/devstorage.read_write");a=d=d.concat(c);c=b=0;for(var e={};cg)break;A++}return jk(C.status,function(J){try{return C.getResponseHeader(J)}catch(fa){return null}},C.responseText,l,void 0,e,d,f)},hk=function(a,b,c,d,e,g,f){var l=0,m={url:a,method:c,content:d,headers:e},p=bk,v=null!= g?g:10;m.callback=function(A){A=A.target;if(429==A.getStatus()&&la.y||a.y>=1< Any: +) -> Dict[str, Any]: """Creates an asset from a JSON value. To create an empty image collection or folder, pass in a "value" object @@ -1535,6 +1535,20 @@ def createAsset( ) +def createFolder(path: str) -> Dict[str, Any]: + """Creates an asset folder. + + Returns a description of the newly created folder. + + Args: + path: The path to the folder to create. + + Returns: + A description of the newly created folder. + """ + return createAsset({'type': 'FOLDER'}, path) + + def copyAsset( sourceId: str, destinationId: str, diff --git a/python/ee/dictionary.py b/python/ee/dictionary.py index 3d95b1094..9e44d70fb 100644 --- a/python/ee/dictionary.py +++ b/python/ee/dictionary.py @@ -6,10 +6,27 @@ from ee import _utils from ee import apifunction from ee import computedobject +from ee import ee_array +from ee import ee_list +from ee import ee_number +from ee import ee_string +from ee import geometry +from ee import image _DictType = Union[ Dict[Any, Any], Sequence[Any], 'Dictionary', computedobject.ComputedObject ] +_EeAnyType = Union[Any, computedobject.ComputedObject] +_EeBoolType = Union[Any, computedobject.ComputedObject] +# bool, float, and int are automatically converted to ee.String for keys. +_EeKeyType = Union[bool, float, int, str, computedobject.ComputedObject] +# TODO: Make a better type for a list of keys. +_EeKeyListType = _EeAnyType +_IntegerType = Union[int, ee_number.Number, computedobject.ComputedObject] +_StringType = Union[str, ee_string.String, computedobject.ComputedObject] +# TODO: Make a better type for a list of strings. +# Or is this the same as _EeKeyListType? +_StringListType = Union[Any, computedobject.ComputedObject] class Dictionary(computedobject.ComputedObject): @@ -76,3 +93,269 @@ def encode_cloud_value(self, encoder=None): return {'valueReference': encoder(self._dictionary)} else: return super().encode_cloud_value(encoder) + + def combine( + self, second: _DictType, overwrite: Optional[_EeBoolType] = None + ) -> Dictionary: + """Combines two dictionaries. + + In the case of duplicate key names, the output will contain the value of the + second dictionary unless overwrite is false. Null values in both + dictionaries are ignored / removed. + + Args: + second: The other dictionary to merge in. + overwrite: If true, this keeps the value of the original dictionary. + Defaults to true. + + Returns: + An ee.Dictionary. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.combine', self, second, overwrite + ) + + def contains(self, key: _StringType) -> computedobject.ComputedObject: + """Returns true if the dictionary contains the given key. + + Args: + key: A string to look for in the dictionary. + + Returns: + An ee.Boolean. + """ + + return apifunction.ApiFunction.call_(self.name() + '.contains', self, key) + + # TODO: Add fromLists + + def get( + self, + key: _EeKeyType, + # pylint: disable-next=invalid-name + defaultValue: Optional[_EeAnyType] = None, + ) -> computedobject.ComputedObject: + """Extracts a named value from a dictionary. + + If the dictionary does not contain the given key, then defaultValue is + returned, unless it is null. + + Args: + key: A string to look for in the dictionary. + defaultValue: The value to return if the key is not found. + + Returns: + Returns an ee.ComputedObject. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.get', self, key, defaultValue + ) + + def getArray(self, key: _EeKeyType) -> ee_array.Array: + """Extracts a named array value from a dictionary. + + Args: + key: A string to look for in the dictionary. + + Returns: + An ee.Array. + """ + + return apifunction.ApiFunction.call_(self.name() + '.getArray', self, key) + + def getGeometry(self, key: _EeKeyType) -> geometry.Geometry: + """Extracts a named geometry value from a dictionary. + + Args: + key: A string to look for in the dictionary. + + Returns: + An ee.Geometry. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.getGeometry', self, key + ) + + def getNumber(self, key: _EeKeyType) -> ee_number.Number: + """Extracts a named number value from a dictionary. + + Args: + key: A string to look for in the dictionary. + + Returns: + An ee.Number. + """ + + return apifunction.ApiFunction.call_(self.name() + '.getNumber', self, key) + + def getString(self, key: _EeKeyType) -> ee_string.String: + """Extracts a named string value from a dictionary. + + Args: + key: A string to look for in the dictionary. + + Returns: + An ee.String. + """ + + return apifunction.ApiFunction.call_(self.name() + '.getString', self, key) + + def keys(self) -> ee_list.List: + """Retrieve the keys of a dictionary as a list.""" + + return apifunction.ApiFunction.call_(self.name() + '.keys', self) + + # pylint: disable-next=invalid-name + def map(self, baseAlgorithm: _EeAnyType) -> Dictionary: + """Map an algorithm over a dictionary. + + The algorithm is expected to take 2 arguments, a key from the existing + dictionary and the value it corresponds to, and return a new value for the + given key. If the algorithm returns null, the key is dropped. + + Args: + baseAlgorithm: A function taking key, value and returning the new value. + + Returns: + An ee.Dictionary with new values for each key. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.map', self, baseAlgorithm + ) + + def remove( + self, + selectors: _EeAnyType, + # pylint: disable-next=invalid-name + ignoreMissing: Optional[_EeBoolType] = None, + ) -> Dictionary: + """Returns a dictionary with the specified keys removed. + + Args: + selectors: A list of key names or regular expressions of key names to + remove. + ignoreMissing: Ignore selectors that don't match at least 1 key. Defaults + to false. + + Returns: + An ee.Dictionary. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.remove', self, selectors, ignoreMissing + ) + + # TODO: Make a tighter method signature. + # pylint: disable-next=g-doc-args + def rename(self, *args, **kwargs) -> Dictionary: + """Rename elements in a dictionary. + + Args: + from: A list of keys to be renamed. + to: A list of the new names for the keys listed in the 'from' parameter. + Must have the same length as the 'from' list. + overwrite: Allow overwriting existing properties with the same name. + + Returns: + An ee.Dictionary. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.rename', self, *args, **kwargs + ) + + def select( + self, + selectors: _EeAnyType, + # pylint: disable-next=invalid-name + ignoreMissing: Optional[_EeBoolType] = None, + ) -> Dictionary: + """Returns a dictionary with only the specified keys. + + Args: + selectors: A list of keys or regular expressions to select. + ignoreMissing: Ignore selectors that don't match at least 1 key. + Defaults to false. + + Returns: + An ee.Dictionary. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.select', self, selectors, ignoreMissing + ) + + def set(self, key: _EeKeyType, value: _EeAnyType) -> Dictionary: + """Set a value in a dictionary. + + Args: + key: A string for where to set the value. Does not need to already exist. + value: The value to set for the key. + + Returns: + An ee.Dictionary. + """ + + return apifunction.ApiFunction.call_(self.name() + '.set', self, key, value) + + def size(self) -> ee_number.Number: + """Returns the number of entries in a dictionary.""" + + return apifunction.ApiFunction.call_(self.name() + '.size', self) + + def toArray( + self, + keys: Optional[_EeKeyListType] = None, + axis: Optional[_IntegerType] = None, + ) -> ee_array.Array: + """Returns numeric values of a dictionary as an array. + + If no keys are specified, all values are returned in the natural ordering of + the dictionary's keys. The default 'axis' is 0. + + Args: + keys: An optional list of keys to subselect. + axis: How to interpret values that are ee.Arrays. Defaults to 0. + + Returns: + An ee.Array. + """ + + return apifunction.ApiFunction.call_( + self.name() + '.toArray', self, keys, axis + ) + + def toImage(self, names: Optional[_EeAnyType] = None) -> image.Image: + """Creates an image of constants from values in a dictionary. + + The bands of the image are ordered and named according to the names + argument. If no names are specified, the bands are sorted + alpha-numerically. + + Args: + names: The order of the output bands. + + Returns: + An ee.Image. + """ + + return apifunction.ApiFunction.call_(self.name() + '.toImage', self, names) + + def values(self, keys: Optional[_EeKeyListType] = None) -> ee_list.List: + """Returns the values of a dictionary as a list. + + If no keys are specified, all values are returned in the natural ordering of + the dictionary's keys. + + Args: + keys: An optional list of keys to subselect. + + Returns: + An ee.Array. + """ + + return apifunction.ApiFunction.call_(self.name() + '.values', self, keys) diff --git a/python/ee/tests/data_test.py b/python/ee/tests/data_test.py index ec8de2744..166c55d63 100644 --- a/python/ee/tests/data_test.py +++ b/python/ee/tests/data_test.py @@ -129,6 +129,28 @@ def testCreateAssetWithV1AlphaParams(self): {'uris': ['gs://my-bucket/path']}, ) + @unittest.skip('Does not work on github with python 3.7') + def testCreateFolder(self): + cloud_api_resource = mock.MagicMock() + with apitestcase.UsingCloudApi(cloud_api_resource=cloud_api_resource): + mock_result = { + 'type': 'FOLDER', + 'name': 'projects/earthengine-legacy/assets/users/foo/xyz1234', + 'id': 'users/foo/xyz1234', + } + cloud_api_resource.projects().assets().create.execute.return_value = ( + mock_result + ) + ee.data.createFolder('users/foo/xyz123') + mock_create_asset = cloud_api_resource.projects().assets().create + mock_create_asset.assert_called_once() + parent = mock_create_asset.call_args.kwargs['parent'] + self.assertEqual(parent, 'projects/earthengine-legacy') + asset_id = mock_create_asset.call_args.kwargs['assetId'] + self.assertEqual(asset_id, 'users/foo/xyz123') + asset = mock_create_asset.call_args.kwargs['body'] + self.assertEqual(asset, {'type': 'FOLDER'}) + def testSetAssetProperties(self): mock_http = mock.MagicMock(httplib2.Http) with apitestcase.UsingCloudApi(mock_http=mock_http), mock.patch.object( diff --git a/python/ee/tests/dictionary_test.py b/python/ee/tests/dictionary_test.py index 4d73ca150..05e233e8c 100644 --- a/python/ee/tests/dictionary_test.py +++ b/python/ee/tests/dictionary_test.py @@ -1,11 +1,22 @@ #!/usr/bin/env python3 """Test for the ee.dictionary module.""" +import json +from typing import Any, Dict import ee from ee import apitestcase import unittest +def make_expression_graph( + function_invocation_value: Dict[str, Any], +) -> Dict[str, Any]: + return { + 'result': '0', + 'values': {'0': {'functionInvocationValue': function_invocation_value}}, + } + + class DictionaryTest(apitestcase.ApiTestCase): def testDictionary(self): @@ -50,6 +61,289 @@ def testInternals(self): self.assertNotEqual(b, c) self.assertNotEqual(hash(a), hash(b)) + def test_combine(self): + expect = make_expression_graph({ + 'arguments': { + 'first': {'constantValue': {'a': 1}}, + 'second': {'constantValue': {'b': 2}}, + 'overwrite': {'constantValue': True}, + }, + 'functionName': 'Dictionary.combine', + }) + expression = ee.Dictionary({'a': 1}).combine({'b': 2}, True) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).combine( + second={'b': 2}, overwrite=True + ) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_contains(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'a key'}, + }, + 'functionName': 'Dictionary.contains', + }) + expression = ee.Dictionary({'a': 1}).contains('a key') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).contains(key='a key') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_fromLists(self): + expect = make_expression_graph({ + 'arguments': { + 'keys': {'constantValue': ['a']}, + 'values': {'constantValue': [1]}, + }, + 'functionName': 'Dictionary.fromLists', + }) + expression = ee.Dictionary().fromLists(['a'], [1]) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary().fromLists(keys=['a'], values=[1]) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_get(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'b'}, + 'defaultValue': {'constantValue': 'a default'}, + }, + 'functionName': 'Dictionary.get', + }) + expression = ee.Dictionary({'a': 1}).get('b', 'a default') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).get(key='b', defaultValue='a default') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_getArray(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'b'}, + }, + 'functionName': 'Dictionary.getArray', + }) + expression = ee.Dictionary({'a': 1}).getArray('b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).getArray(key='b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_getGeometry(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'b'}, + }, + 'functionName': 'Dictionary.getGeometry', + }) + expression = ee.Dictionary({'a': 1}).getGeometry('b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).getGeometry(key='b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_getNumber(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'b'}, + }, + 'functionName': 'Dictionary.getNumber', + }) + expression = ee.Dictionary({'a': 1}).getNumber('b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).getNumber(key='b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_getString(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'b'}, + }, + 'functionName': 'Dictionary.getString', + }) + expression = ee.Dictionary({'a': 1}).getString('b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).getString(key='b') + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_keys(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + }, + 'functionName': 'Dictionary.keys', + }) + expression = ee.Dictionary({'a': 1}).keys() + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).keys() + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + # TODO: test_map + + def test_remove(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'selectors': {'constantValue': ['b']}, + 'ignoreMissing': {'constantValue': True}, + }, + 'functionName': 'Dictionary.remove', + }) + expression = ee.Dictionary({'a': 1}).remove(['b'], True) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).remove( + selectors=['b'], ignoreMissing=True + ) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_rename(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'from': {'constantValue': ['b']}, + 'to': {'constantValue': ['c']}, + 'overwrite': {'constantValue': True}, + }, + 'functionName': 'Dictionary.rename', + }) + expression = ee.Dictionary({'a': 1}).rename(['b'], ['c'], True) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + # Cannot use `from` kwarg as it is a python keyword. + expression = ee.Dictionary({'a': 1}).rename(['b'], to=['c'], overwrite=True) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_select(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'selectors': {'constantValue': ['b']}, + 'ignoreMissing': {'constantValue': True}, + }, + 'functionName': 'Dictionary.select', + }) + expression = ee.Dictionary({'a': 1}).select(['b'], True) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).select( + selectors=['b'], ignoreMissing=True + ) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_set(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'key': {'constantValue': 'b'}, + 'value': {'constantValue': 2}, + }, + 'functionName': 'Dictionary.set', + }) + expression = ee.Dictionary({'a': 1}).set('b', 2) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).set(key='b', value=2) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_size(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + }, + 'functionName': 'Dictionary.size', + }) + expression = ee.Dictionary({'a': 1}).size() + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_toArray(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'keys': {'constantValue': ['b']}, + 'axis': {'constantValue': 2}, + }, + 'functionName': 'Dictionary.toArray', + }) + expression = ee.Dictionary({'a': 1}).toArray(['b'], 2) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).toArray(keys=['b'], axis=2) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_values(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'keys': {'constantValue': ['b']}, + }, + 'functionName': 'Dictionary.values', + }) + expression = ee.Dictionary({'a': 1}).values(['b']) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).values(keys=['b']) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + def test_toImage(self): + expect = make_expression_graph({ + 'arguments': { + 'dictionary': {'constantValue': {'a': 1}}, + 'names': {'constantValue': ['b']}, + }, + 'functionName': 'Dictionary.toImage', + }) + expression = ee.Dictionary({'a': 1}).toImage(['b']) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + + expression = ee.Dictionary({'a': 1}).toImage(names=['b']) + result = json.loads(expression.serialize()) + self.assertEqual(expect, result) + if __name__ == '__main__': unittest.main() diff --git a/python/pyproject.toml b/python/pyproject.toml index 91af9e5fc..e90cb3cb0 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "earthengine-api" -version = "0.1.397" +version = "0.1.398" description = "Earth Engine Python API" requires-python = ">=3.7" keywords = [