├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── ee.cli.rst │ ├── ee.rst │ ├── index.rst │ └── modules.rst ├── ee ├── __init__.py ├── _cloud_api_utils.py ├── _helpers.py ├── apifunction.py ├── apitestcase.py ├── batch.py ├── cli │ ├── __init__.py │ ├── build_archive.sh │ ├── commands.py │ ├── eecli.py │ ├── eecli_wrapper.py │ └── utils.py ├── collection.py ├── computedobject.py ├── customfunction.py ├── data.py ├── deprecation.py ├── deserializer.py ├── dictionary.py ├── ee_date.py ├── ee_exception.py ├── ee_list.py ├── ee_number.py ├── ee_string.py ├── ee_types.py ├── element.py ├── encodable.py ├── feature.py ├── featurecollection.py ├── filter.py ├── function.py ├── geometry.py ├── image.py ├── imagecollection.py ├── mapclient.py ├── oauth.py ├── serializer.py ├── terrain.py └── tests │ ├── _cloud_api_utils_test.py │ ├── _helpers_test.py │ ├── apifunction_test.py │ ├── batch_test.py │ ├── cloud_api_discovery_document.json │ ├── collection_test.py │ ├── computedobject_test.py │ ├── data_test.py │ ├── date_test.py │ ├── deserializer_test.py │ ├── dictionary_test.py │ ├── ee_test.py │ ├── element_test.py │ ├── feature_test.py │ ├── featurecollection_test.py │ ├── filter_test.py │ ├── function_test.py │ ├── geometry_test.py │ ├── image_test.py │ ├── imagecollection_test.py │ ├── list_test.py │ ├── number_test.py │ ├── oauth_test.py │ ├── serializer_test.py │ └── string_test.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 无形的风 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GEE Python API 2 | 3 | Google Earth Engine Python API 4 | 5 | From [GEE API](https://github.com/google/earthengine-api) 6 | 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('./../../')) 16 | import sphinx_rtd_theme 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'GEE-Python-API' 21 | copyright = '2019, LSW' 22 | author = 'LSW' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.0' 26 | 27 | # source_suffix = ['.rst', '.md'] 28 | source_suffix = '.rst' 29 | 30 | # The master toctree document. 31 | master_doc = 'index' 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | "sphinx.ext.autodoc" 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = [] 50 | 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | # html_theme = 'alabaster' 58 | html_theme = "sphinx_rtd_theme" 59 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 60 | 61 | # Add any paths that contain custom static files (such as style sheets) here, 62 | # relative to this directory. They are copied after the builtin static files, 63 | # so a file named "default.css" will overwrite the builtin "default.css". 64 | html_static_path = ['_static'] 65 | -------------------------------------------------------------------------------- /docs/source/ee.cli.rst: -------------------------------------------------------------------------------- 1 | ee.cli package 2 | ============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | ee.cli.commands module 8 | ---------------------- 9 | 10 | .. automodule:: ee.cli.commands 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ee.cli.eecli module 16 | ------------------- 17 | 18 | .. automodule:: ee.cli.eecli 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | ee.cli.eecli\_wrapper module 24 | ---------------------------- 25 | 26 | .. automodule:: ee.cli.eecli_wrapper 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | ee.cli.utils module 32 | ------------------- 33 | 34 | .. automodule:: ee.cli.utils 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: ee.cli 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/source/ee.rst: -------------------------------------------------------------------------------- 1 | ee package 2 | ========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | ee.cli 10 | 11 | Submodules 12 | ---------- 13 | 14 | ee.apifunction module 15 | --------------------- 16 | 17 | .. automodule:: ee.apifunction 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | ee.apitestcase module 23 | --------------------- 24 | 25 | .. automodule:: ee.apitestcase 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | ee.batch module 31 | --------------- 32 | 33 | .. automodule:: ee.batch 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | ee.collection module 39 | -------------------- 40 | 41 | .. automodule:: ee.collection 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | ee.computedobject module 47 | ------------------------ 48 | 49 | .. automodule:: ee.computedobject 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | ee.customfunction module 55 | ------------------------ 56 | 57 | .. automodule:: ee.customfunction 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | ee.data module 63 | -------------- 64 | 65 | .. automodule:: ee.data 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | ee.deprecation module 71 | --------------------- 72 | 73 | .. automodule:: ee.deprecation 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | ee.deserializer module 79 | ---------------------- 80 | 81 | .. automodule:: ee.deserializer 82 | :members: 83 | :undoc-members: 84 | :show-inheritance: 85 | 86 | ee.dictionary module 87 | -------------------- 88 | 89 | .. automodule:: ee.dictionary 90 | :members: 91 | :undoc-members: 92 | :show-inheritance: 93 | 94 | ee.ee\_date module 95 | ------------------ 96 | 97 | .. automodule:: ee.ee_date 98 | :members: 99 | :undoc-members: 100 | :show-inheritance: 101 | 102 | ee.ee\_exception module 103 | ----------------------- 104 | 105 | .. automodule:: ee.ee_exception 106 | :members: 107 | :undoc-members: 108 | :show-inheritance: 109 | 110 | ee.ee\_list module 111 | ------------------ 112 | 113 | .. automodule:: ee.ee_list 114 | :members: 115 | :undoc-members: 116 | :show-inheritance: 117 | 118 | ee.ee\_number module 119 | -------------------- 120 | 121 | .. automodule:: ee.ee_number 122 | :members: 123 | :undoc-members: 124 | :show-inheritance: 125 | 126 | ee.ee\_string module 127 | -------------------- 128 | 129 | .. automodule:: ee.ee_string 130 | :members: 131 | :undoc-members: 132 | :show-inheritance: 133 | 134 | ee.ee\_types module 135 | ------------------- 136 | 137 | .. automodule:: ee.ee_types 138 | :members: 139 | :undoc-members: 140 | :show-inheritance: 141 | 142 | ee.element module 143 | ----------------- 144 | 145 | .. automodule:: ee.element 146 | :members: 147 | :undoc-members: 148 | :show-inheritance: 149 | 150 | ee.encodable module 151 | ------------------- 152 | 153 | .. automodule:: ee.encodable 154 | :members: 155 | :undoc-members: 156 | :show-inheritance: 157 | 158 | ee.feature module 159 | ----------------- 160 | 161 | .. automodule:: ee.feature 162 | :members: 163 | :undoc-members: 164 | :show-inheritance: 165 | 166 | ee.featurecollection module 167 | --------------------------- 168 | 169 | .. automodule:: ee.featurecollection 170 | :members: 171 | :undoc-members: 172 | :show-inheritance: 173 | 174 | ee.filter module 175 | ---------------- 176 | 177 | .. automodule:: ee.filter 178 | :members: 179 | :undoc-members: 180 | :show-inheritance: 181 | 182 | ee.function module 183 | ------------------ 184 | 185 | .. automodule:: ee.function 186 | :members: 187 | :undoc-members: 188 | :show-inheritance: 189 | 190 | ee.geometry module 191 | ------------------ 192 | 193 | .. automodule:: ee.geometry 194 | :members: 195 | :undoc-members: 196 | :show-inheritance: 197 | 198 | ee.image module 199 | --------------- 200 | 201 | .. automodule:: ee.image 202 | :members: 203 | :undoc-members: 204 | :show-inheritance: 205 | 206 | ee.imagecollection module 207 | ------------------------- 208 | 209 | .. automodule:: ee.imagecollection 210 | :members: 211 | :undoc-members: 212 | :show-inheritance: 213 | 214 | ee.mapclient module 215 | ------------------- 216 | 217 | .. automodule:: ee.mapclient 218 | :members: 219 | :undoc-members: 220 | :show-inheritance: 221 | 222 | ee.oauth module 223 | --------------- 224 | 225 | .. automodule:: ee.oauth 226 | :members: 227 | :undoc-members: 228 | :show-inheritance: 229 | 230 | ee.serializer module 231 | -------------------- 232 | 233 | .. automodule:: ee.serializer 234 | :members: 235 | :undoc-members: 236 | :show-inheritance: 237 | 238 | ee.terrain module 239 | ----------------- 240 | 241 | .. automodule:: ee.terrain 242 | :members: 243 | :undoc-members: 244 | :show-inheritance: 245 | 246 | 247 | Module contents 248 | --------------- 249 | 250 | .. automodule:: ee 251 | :members: 252 | :undoc-members: 253 | :show-inheritance: 254 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. GEE-Python-API documentation master file, created by 2 | sphinx-quickstart on Thu Jun 20 15:47:30 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to GEE-Python-API's documentation! 7 | ========================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | ee 2 | == 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | ee 8 | -------------------------------------------------------------------------------- /ee/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """The EE Python library.""" 3 | 4 | 5 | __version__ = '0.1.182' 6 | 7 | # Using lowercase function naming to match the JavaScript names. 8 | # pylint: disable=g-bad-name 9 | 10 | # pylint: disable=g-bad-import-order 11 | import collections 12 | import datetime 13 | import inspect 14 | import numbers 15 | import os 16 | import six 17 | 18 | from . import batch 19 | from . import data 20 | from . import deserializer 21 | from . import ee_types as types 22 | from ._helpers import _GetPersistentCredentials 23 | 24 | # Public re-exports. 25 | from ._helpers import ServiceAccountCredentials 26 | from ._helpers import apply # pylint: disable=redefined-builtin 27 | from ._helpers import call 28 | from ._helpers import profilePrinting 29 | from .apifunction import ApiFunction 30 | from .collection import Collection 31 | from .computedobject import ComputedObject 32 | from .customfunction import CustomFunction 33 | from .dictionary import Dictionary 34 | from .ee_date import Date 35 | from .ee_exception import EEException 36 | from .ee_list import List 37 | from .ee_number import Number 38 | from .ee_string import String 39 | from .element import Element 40 | from .encodable import Encodable 41 | from .feature import Feature 42 | from .featurecollection import FeatureCollection 43 | from .filter import Filter 44 | from .function import Function 45 | from .geometry import Geometry 46 | from .image import Image 47 | from .imagecollection import ImageCollection 48 | from .serializer import Serializer 49 | from .terrain import Terrain 50 | 51 | # A list of autogenerated class names added by _InitializeGenerateClasses. 52 | _generatedClasses = [] 53 | 54 | 55 | class _AlgorithmsContainer(dict): 56 | """A lightweight class that is used as a dictionary with dot notation. 57 | """ 58 | 59 | def __getattr__(self, name): 60 | try: 61 | return self[name] 62 | except KeyError: 63 | raise AttributeError 64 | 65 | def __setattr__(self, name, value): 66 | self[name] = value 67 | 68 | def __delattr__(self, name): 69 | del self[name] 70 | 71 | # A dictionary of algorithms that are not bound to a specific class. 72 | Algorithms = _AlgorithmsContainer() 73 | 74 | 75 | def Initialize( 76 | credentials='persistent', 77 | opt_url=None, 78 | use_cloud_api=False, 79 | cloud_api_key=None, 80 | http_transport=None, 81 | project=None): 82 | """Initialize the EE library. 83 | 84 | If this hasn't been called by the time any object constructor is used, 85 | it will be called then. If this is called a second time with a different 86 | URL, this doesn't do an un-initialization of e.g.: the previously loaded 87 | Algorithms, but will overwrite them and let point at alternate servers. 88 | 89 | Args: 90 | credentials: OAuth2 credentials. 'persistent' (default) means use 91 | credentials already stored in the filesystem, or raise an explanatory 92 | exception guiding the user to create those credentials. 93 | opt_url: The base url for the EarthEngine REST API to connect to. 94 | use_cloud_api: Whether the Cloud API should be used. 95 | cloud_api_key: An optional API key to use the Cloud API. 96 | http_transport: The http transport method to use when making requests. 97 | project: The project-id or number to use when making api calls. 98 | """ 99 | if credentials == 'persistent': 100 | credentials = _GetPersistentCredentials() 101 | data.initialize( 102 | credentials=credentials, 103 | api_base_url=(opt_url + '/api' if opt_url else None), 104 | tile_base_url=opt_url, 105 | use_cloud_api=use_cloud_api, 106 | cloud_api_base_url=opt_url, 107 | cloud_api_key=cloud_api_key, 108 | project=project, 109 | http_transport=http_transport) 110 | # Initialize the dynamically loaded functions on the objects that want them. 111 | ApiFunction.initialize() 112 | Element.initialize() 113 | Image.initialize() 114 | Feature.initialize() 115 | Collection.initialize() 116 | ImageCollection.initialize() 117 | FeatureCollection.initialize() 118 | Filter.initialize() 119 | Geometry.initialize() 120 | List.initialize() 121 | Number.initialize() 122 | String.initialize() 123 | Date.initialize() 124 | Dictionary.initialize() 125 | Terrain.initialize() 126 | _InitializeGeneratedClasses() 127 | _InitializeUnboundMethods() 128 | 129 | 130 | def Reset(): 131 | """Reset the library. Useful for re-initializing to a different server.""" 132 | data.reset() 133 | ApiFunction.reset() 134 | Element.reset() 135 | Image.reset() 136 | Feature.reset() 137 | Collection.reset() 138 | ImageCollection.reset() 139 | FeatureCollection.reset() 140 | Filter.reset() 141 | Geometry.reset() 142 | List.reset() 143 | Number.reset() 144 | String.reset() 145 | Date.reset() 146 | Dictionary.reset() 147 | Terrain.reset() 148 | _ResetGeneratedClasses() 149 | global Algorithms 150 | Algorithms = _AlgorithmsContainer() 151 | 152 | 153 | def _ResetGeneratedClasses(): 154 | """Remove the dynamic classes.""" 155 | global _generatedClasses 156 | 157 | for name in _generatedClasses: 158 | ApiFunction.clearApi(globals()[name]) 159 | del globals()[name] 160 | _generatedClasses = [] 161 | # Warning: we're passing all of globals() into registerClasses. 162 | # This is a) pass by reference, and b) a lot more stuff. 163 | types._registerClasses(globals()) # pylint: disable=protected-access 164 | 165 | 166 | def _Promote(arg, klass): 167 | """Wrap an argument in an object of the specified class. 168 | 169 | This is used to e.g.: promote numbers or strings to Images and arrays 170 | to Collections. 171 | 172 | Args: 173 | arg: The object to promote. 174 | klass: The expected type. 175 | 176 | Returns: 177 | The argument promoted if the class is recognized, otherwise the 178 | original argument. 179 | """ 180 | if arg is None: 181 | return arg 182 | 183 | if klass == 'Image': 184 | return Image(arg) 185 | elif klass == 'Feature': 186 | if isinstance(arg, Collection): 187 | # TODO(user): Decide whether we want to leave this in. It can be 188 | # quite dangerous on large collections. 189 | return ApiFunction.call_( 190 | 'Feature', ApiFunction.call_('Collection.geometry', arg)) 191 | else: 192 | return Feature(arg) 193 | elif klass == 'Element': 194 | if isinstance(arg, Element): 195 | # Already an Element. 196 | return arg 197 | elif isinstance(arg, Geometry): 198 | # Geometries get promoted to Features. 199 | return Feature(arg) 200 | elif isinstance(arg, ComputedObject): 201 | # Try a cast. 202 | return Element(arg.func, arg.args, arg.varName) 203 | else: 204 | # No way to convert. 205 | raise EEException('Cannot convert %s to Element.' % arg) 206 | elif klass == 'Geometry': 207 | if isinstance(arg, Collection): 208 | return ApiFunction.call_('Collection.geometry', arg) 209 | else: 210 | return Geometry(arg) 211 | elif klass in ('FeatureCollection', 'Collection'): 212 | # For now Collection is synonymous with FeatureCollection. 213 | if isinstance(arg, Collection): 214 | return arg 215 | else: 216 | return FeatureCollection(arg) 217 | elif klass == 'ImageCollection': 218 | return ImageCollection(arg) 219 | elif klass == 'Filter': 220 | return Filter(arg) 221 | elif klass == 'Algorithm': 222 | if isinstance(arg, six.string_types): 223 | # An API function name. 224 | return ApiFunction.lookup(arg) 225 | elif callable(arg): 226 | # A native function that needs to be wrapped. 227 | args_count = len(inspect.getargspec(arg).args) 228 | return CustomFunction.create(arg, 'Object', ['Object'] * args_count) 229 | elif isinstance(arg, Encodable): 230 | # An ee.Function or a computed function like the return value of 231 | # Image.parseExpression(). 232 | return arg 233 | else: 234 | raise EEException('Argument is not a function: %s' % arg) 235 | elif klass == 'Dictionary': 236 | if isinstance(arg, dict): 237 | return arg 238 | else: 239 | return Dictionary(arg) 240 | elif klass == 'String': 241 | if (types.isString(arg) or 242 | isinstance(arg, ComputedObject) or 243 | isinstance(arg, String)): 244 | return String(arg) 245 | else: 246 | return arg 247 | elif klass == 'List': 248 | return List(arg) 249 | elif klass in ('Number', 'Float', 'Long', 'Integer', 'Short', 'Byte'): 250 | return Number(arg) 251 | elif klass in globals(): 252 | cls = globals()[klass] 253 | ctor = ApiFunction.lookupInternal(klass) 254 | # Handle dynamically created classes. 255 | if isinstance(arg, cls): 256 | # Return unchanged. 257 | return arg 258 | elif ctor: 259 | # The client-side constructor will call the server-side constructor. 260 | return cls(arg) 261 | elif isinstance(arg, six.string_types): 262 | if hasattr(cls, arg): 263 | # arg is the name of a method in klass. 264 | return getattr(cls, arg)() 265 | else: 266 | raise EEException('Unknown algorithm: %s.%s' % (klass, arg)) 267 | else: 268 | # Client-side cast. 269 | return cls(arg) 270 | else: 271 | return arg 272 | 273 | 274 | def _InitializeUnboundMethods(): 275 | # Sort the items by length, so parents get created before children. 276 | items = sorted( 277 | ApiFunction.unboundFunctions().items(), key=lambda x: len(x[0])) 278 | 279 | for name, func in items: 280 | signature = func.getSignature() 281 | if signature.get('hidden', False): 282 | continue 283 | 284 | # Create nested objects as needed. 285 | name_parts = name.split('.') 286 | target = Algorithms 287 | while len(name_parts) > 1: 288 | first = name_parts[0] 289 | # Set the attribute if it doesn't already exist. The try/except block 290 | # works in both Python 2 & 3. 291 | try: 292 | getattr(target, first) 293 | except AttributeError: 294 | setattr(target, first, _AlgorithmsContainer()) 295 | 296 | target = getattr(target, first) 297 | name_parts = name_parts[1:] 298 | 299 | # Attach the function. 300 | # We need a copy of the function to attach properties. 301 | def GenerateFunction(f): 302 | return lambda *args, **kwargs: f.call(*args, **kwargs) # pylint: disable=unnecessary-lambda 303 | bound = GenerateFunction(func) 304 | bound.signature = signature 305 | # Add docs. If there are non-ASCII characters in the docs, and we're in 306 | # Python 2, use a hammer to force them into a str. 307 | try: 308 | bound.__doc__ = str(func) 309 | except UnicodeEncodeError: 310 | bound.__doc__ = func.__str__().encode('utf8') 311 | setattr(target, name_parts[0], bound) 312 | 313 | 314 | def _InitializeGeneratedClasses(): 315 | """Generate classes for extra types that appear in the web API.""" 316 | signatures = ApiFunction.allSignatures() 317 | # Collect the first part of all function names. 318 | names = set([name.split('.')[0] for name in signatures]) 319 | # Collect the return types of all functions. 320 | returns = set([signatures[sig]['returns'] for sig in signatures]) 321 | 322 | want = [name for name in names.intersection(returns) if name not in globals()] 323 | 324 | for name in want: 325 | globals()[name] = _MakeClass(name) 326 | _generatedClasses.append(name) 327 | ApiFunction._bound_signatures.add(name) # pylint: disable=protected-access 328 | 329 | # Warning: we're passing all of globals() into registerClasses. 330 | # This is a) pass by reference, and b) a lot more stuff. 331 | types._registerClasses(globals()) # pylint: disable=protected-access 332 | 333 | 334 | def _MakeClass(name): 335 | """Generates a dynamic API class for a given name.""" 336 | 337 | def init(self, *args): 338 | """Initializer for dynamically created classes. 339 | 340 | Args: 341 | self: The instance of this class. Listed to make the linter hush. 342 | *args: Either a ComputedObject to be promoted to this type, or 343 | arguments to an algorithm with the same name as this class. 344 | 345 | Returns: 346 | The new class. 347 | """ 348 | klass = globals()[name] 349 | onlyOneArg = (len(args) == 1) 350 | # Are we trying to cast something that's already of the right class? 351 | if onlyOneArg and isinstance(args[0], klass): 352 | result = args[0] 353 | else: 354 | # Decide whether to call a server-side constructor or just do a 355 | # client-side cast. 356 | ctor = ApiFunction.lookupInternal(name) 357 | firstArgIsPrimitive = not isinstance(args[0], ComputedObject) 358 | shouldUseConstructor = False 359 | if ctor: 360 | if not onlyOneArg: 361 | # Can't client-cast multiple arguments. 362 | shouldUseConstructor = True 363 | elif firstArgIsPrimitive: 364 | # Can't cast a primitive. 365 | shouldUseConstructor = True 366 | elif args[0].func != ctor: 367 | # We haven't already called the constructor on this object. 368 | shouldUseConstructor = True 369 | 370 | # Apply our decision. 371 | if shouldUseConstructor: 372 | # Call ctor manually to avoid having promote() called on the output. 373 | ComputedObject.__init__( 374 | self, ctor, ctor.promoteArgs(ctor.nameArgs(args))) 375 | else: 376 | # Just cast and hope for the best. 377 | if not onlyOneArg: 378 | # We don't know what to do with multiple args. 379 | raise EEException( 380 | 'Too many arguments for ee.%s(): %s' % (name, args)) 381 | elif firstArgIsPrimitive: 382 | # Can't cast a primitive. 383 | raise EEException( 384 | 'Invalid argument for ee.%s(): %s. Must be a ComputedObject.' % 385 | (name, args)) 386 | else: 387 | result = args[0] 388 | ComputedObject.__init__(self, result.func, result.args, result.varName) 389 | 390 | properties = {'__init__': init, 'name': lambda self: name} 391 | new_class = type(str(name), (ComputedObject,), properties) 392 | ApiFunction.importApi(new_class, name, name) 393 | return new_class 394 | 395 | 396 | # Set up type promotion rules as soon the package is loaded. 397 | Function._registerPromoter(_Promote) # pylint: disable=protected-access 398 | -------------------------------------------------------------------------------- /ee/_helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Convenience functions and code used by ee/__init__.py. 3 | 4 | These functions are in general re-exported from the "ee" module and should be 5 | referenced from there (e.g. "ee.profilePrinting"). 6 | """ 7 | 8 | # Using lowercase function naming to match the JavaScript names. 9 | # pylint: disable=g-bad-name 10 | 11 | import contextlib 12 | import json 13 | import sys 14 | # pylint: disable=g-importing-member 15 | from . import data 16 | from . import oauth 17 | from .apifunction import ApiFunction 18 | from .ee_exception import EEException 19 | # pylint: enable=g-importing-member 20 | import six 21 | from google.auth import crypt 22 | from google.oauth2 import service_account 23 | from google.oauth2.credentials import Credentials 24 | 25 | 26 | def _GetPersistentCredentials(): 27 | """Read persistent credentials from ~/.config/earthengine. 28 | 29 | Raises EEException with helpful explanation if credentials don't exist. 30 | 31 | Returns: 32 | OAuth2Credentials built from persistently stored refresh_token 33 | """ 34 | try: 35 | tokens = json.load(open(oauth.get_credentials_path())) 36 | refresh_token = tokens['refresh_token'] 37 | return Credentials( 38 | None, 39 | refresh_token=refresh_token, 40 | token_uri=oauth.TOKEN_URI, 41 | client_id=oauth.CLIENT_ID, 42 | client_secret=oauth.CLIENT_SECRET, 43 | scopes=oauth.SCOPES) 44 | except IOError: 45 | raise EEException('Please authorize access to your Earth Engine account ' 46 | 'by running\n\nearthengine authenticate\n\nin your ' 47 | 'command line, and then retry.') 48 | 49 | 50 | def ServiceAccountCredentials(email, key_file=None, key_data=None): 51 | """Configure OAuth2 credentials for a Google Service Account. 52 | 53 | Args: 54 | email: The email address of the account for which to configure credentials. 55 | Ignored if key_file or key_data represents a JSON service account key. 56 | key_file: The path to a file containing the private key associated with 57 | the service account. Both JSON and PEM files are supported. 58 | key_data: Raw key data to use, if key_file is not specified. 59 | 60 | Returns: 61 | An OAuth2 credentials object. 62 | """ 63 | 64 | # Assume anything that doesn't end in '.pem' is a JSON key. 65 | if key_file and not key_file.endswith('.pem'): 66 | return service_account.Credentials.from_service_account_file( 67 | key_file, scopes=oauth.SCOPES) 68 | 69 | # If 'key_data' can be decoded as JSON, it's probably a raw JSON key. 70 | if key_data: 71 | try: 72 | key_data = json.loads(key_data) 73 | return service_account.Credentials.from_service_account_info( 74 | key_data, scopes=oauth.SCOPES) 75 | except ValueError: 76 | # It may actually be a raw PEM string, we'll try that below. 77 | pass 78 | 79 | # Probably a PEM key - just read the file into 'key_data'. 80 | if key_file: 81 | with open(key_file, 'r') as file_: 82 | key_data = file_.read() 83 | 84 | # Raw PEM key. 85 | signer = crypt.RSASigner.from_string(key_data) 86 | return service_account.Credentials( 87 | signer, email, oauth.TOKEN_URI, scopes=oauth.SCOPES) 88 | 89 | 90 | def call(func, *args, **kwargs): 91 | """Invoke the given algorithm with the specified args. 92 | 93 | Args: 94 | func: The function to call. Either an ee.Function object or the name of 95 | an API function. 96 | *args: The positional arguments to pass to the function. 97 | **kwargs: The named arguments to pass to the function. 98 | 99 | Returns: 100 | A ComputedObject representing the called function. If the signature 101 | specifies a recognized return type, the returned value will be cast 102 | to that type. 103 | """ 104 | if isinstance(func, six.string_types): 105 | func = ApiFunction.lookup(func) 106 | return func.call(*args, **kwargs) 107 | 108 | 109 | def apply(func, named_args): # pylint: disable=redefined-builtin 110 | """Call a function with a dictionary of named arguments. 111 | 112 | Args: 113 | func: The function to call. Either an ee.Function object or the name of 114 | an API function. 115 | named_args: A dictionary of arguments to the function. 116 | 117 | Returns: 118 | A ComputedObject representing the called function. If the signature 119 | specifies a recognized return type, the returned value will be cast 120 | to that type. 121 | """ 122 | if isinstance(func, six.string_types): 123 | func = ApiFunction.lookup(func) 124 | return func.apply(named_args) 125 | 126 | 127 | @contextlib.contextmanager 128 | def profilePrinting(destination=sys.stderr): 129 | # pylint: disable=g-doc-return-or-yield 130 | """Returns a context manager that prints a profile of enclosed API calls. 131 | 132 | The profile will be printed when the context ends, whether or not any error 133 | occurred within the context. 134 | 135 | # Simple example: 136 | with ee.profilePrinting(): 137 | print ee.Number(1).add(1).getInfo() 138 | 139 | Args: 140 | destination: A file-like object to which the profile text is written. 141 | Defaults to sys.stderr. 142 | 143 | """ 144 | # TODO(user): Figure out why ee.Profile.getProfiles isn't generated and fix 145 | # that. 146 | getProfiles = ApiFunction.lookup('Profile.getProfiles') 147 | 148 | profile_ids = [] 149 | try: 150 | with data.profiling(profile_ids.append): 151 | yield 152 | finally: 153 | profile_text = getProfiles.call(ids=profile_ids).getInfo() 154 | destination.write(profile_text) 155 | -------------------------------------------------------------------------------- /ee/apifunction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A class for representing built-in EE API Function. 3 | 4 | Earth Engine can dynamically produce a JSON array listing the 5 | algorithms available to the user. Each item in the dictionary identifies 6 | the name and return type of the algorithm, the name and type of its 7 | arguments, whether they're required or optional, default values and docs 8 | for each argument and the algorithms as a whole. 9 | 10 | This class manages the algorithm dictionary and creates JavaScript functions 11 | to apply each EE algorithm. 12 | """ 13 | 14 | 15 | 16 | # Using lowercase function naming to match the JavaScript names. 17 | # pylint: disable=g-bad-name 18 | 19 | import copy 20 | import keyword 21 | import re 22 | 23 | from . import computedobject 24 | from . import data 25 | from . import deprecation 26 | from . import ee_exception 27 | from . import ee_types 28 | from . import function 29 | 30 | 31 | class ApiFunction(function.Function): 32 | """An object representing an EE API Function.""" 33 | 34 | # A dictionary of functions defined by the API server. 35 | _api = None 36 | 37 | # A set of algorithm names containing all algorithms that have been bound to 38 | # a function so far using importApi(). 39 | _bound_signatures = set() 40 | 41 | def __init__(self, name, opt_signature=None): 42 | """Creates a function defined by the EE API. 43 | 44 | Args: 45 | name: The name of the function. 46 | opt_signature: The signature of the function. If unspecified, 47 | looked up dynamically. 48 | """ 49 | if opt_signature is None: 50 | opt_signature = ApiFunction.lookup(name).getSignature() 51 | 52 | # The signature of this API function. 53 | self._signature = copy.deepcopy(opt_signature) 54 | self._signature['name'] = name 55 | 56 | def __eq__(self, other): 57 | return (isinstance(other, ApiFunction) and 58 | self.getSignature() == other.getSignature()) 59 | 60 | # For Python 3, __hash__ is needed because __eq__ is defined. 61 | # See https://docs.python.org/3/reference/datamodel.html#object.__hash__ 62 | def __hash__(self): 63 | return hash(computedobject.ComputedObject.freeze(self.getSignature())) 64 | 65 | def __ne__(self, other): 66 | return not self.__eq__(other) 67 | 68 | @classmethod 69 | def call_(cls, name, *args, **kwargs): 70 | """Call a named API function with positional and keyword arguments. 71 | 72 | Args: 73 | name: The name of the API function to call. 74 | *args: Positional arguments to pass to the function. 75 | **kwargs: Keyword arguments to pass to the function. 76 | 77 | Returns: 78 | An object representing the called function. If the signature specifies 79 | a recognized return type, the returned value will be cast to that type. 80 | """ 81 | return cls.lookup(name).call(*args, **kwargs) 82 | 83 | @classmethod 84 | def apply_(cls, name, named_args): 85 | """Call a named API function with a dictionary of named arguments. 86 | 87 | Args: 88 | name: The name of the API function to call. 89 | named_args: A dictionary of arguments to the function. 90 | 91 | Returns: 92 | An object representing the called function. If the signature specifies 93 | a recognized return type, the returned value will be cast to that type. 94 | """ 95 | return cls.lookup(name).apply(named_args) 96 | 97 | def encode_invocation(self, unused_encoder): 98 | return self._signature['name'] 99 | 100 | def encode_cloud_invocation(self, unused_encoder): 101 | return {'functionName': self._signature['name']} 102 | 103 | def getSignature(self): 104 | """Returns a description of the interface provided by this function.""" 105 | return self._signature 106 | 107 | @classmethod 108 | def allSignatures(cls): 109 | """Returns a map from the name to signature for all API functions.""" 110 | cls.initialize() 111 | return dict([(name, func.getSignature()) 112 | for name, func in cls._api.items()]) 113 | 114 | @classmethod 115 | def unboundFunctions(cls): 116 | """Returns the functions that have not been bound using importApi() yet.""" 117 | cls.initialize() 118 | return dict([(name, func) for name, func in cls._api.items() 119 | if name not in cls._bound_signatures]) 120 | 121 | @classmethod 122 | def lookup(cls, name): 123 | """Looks up an API function by name. 124 | 125 | Args: 126 | name: The name of the function to get. 127 | 128 | Returns: 129 | The requested ApiFunction. 130 | """ 131 | result = cls.lookupInternal(name) 132 | if not name: 133 | raise ee_exception.EEException( 134 | 'Unknown built-in function name: %s' % name) 135 | return result 136 | 137 | @classmethod 138 | def lookupInternal(cls, name): 139 | """Looks up an API function by name. 140 | 141 | Args: 142 | name: The name of the function to get. 143 | 144 | Returns: 145 | The requested ApiFunction or None if not found. 146 | """ 147 | cls.initialize() 148 | return cls._api.get(name, None) 149 | 150 | @classmethod 151 | def initialize(cls): 152 | """Initializes the list of signatures from the Earth Engine front-end.""" 153 | if not cls._api: 154 | signatures = data.getAlgorithms() 155 | api = {} 156 | for name, sig in signatures.items(): 157 | # Strip type parameters. 158 | sig['returns'] = re.sub('<.*>', '', sig['returns']) 159 | for arg in sig['args']: 160 | arg['type'] = re.sub('<.*>', '', arg['type']) 161 | api[name] = cls(name, sig) 162 | cls._api = api 163 | 164 | @classmethod 165 | def reset(cls): 166 | """Clears the API functions list so it will be reloaded from the server.""" 167 | cls._api = None 168 | cls._bound_signatures = set() 169 | 170 | @classmethod 171 | def importApi(cls, target, prefix, type_name, opt_prepend=None): 172 | """Adds all API functions that begin with a given prefix to a target class. 173 | 174 | Args: 175 | target: The class to add to. 176 | prefix: The prefix to search for in the signatures. 177 | type_name: The name of the object's type. Functions whose 178 | first argument matches this type are bound as instance methods, and 179 | those whose first argument doesn't match are bound as static methods. 180 | opt_prepend: An optional string to prepend to the names of the 181 | added functions. 182 | """ 183 | cls.initialize() 184 | prepend = opt_prepend or '' 185 | for name, api_func in cls._api.items(): 186 | parts = name.split('.') 187 | if len(parts) == 2 and parts[0] == prefix: 188 | fname = prepend + parts[1] 189 | signature = api_func.getSignature() 190 | 191 | cls._bound_signatures.add(name) 192 | 193 | # Specifically handle the function names that are illegal in python. 194 | if keyword.iskeyword(fname): 195 | fname = fname.title() 196 | 197 | # Don't overwrite existing versions of this function. 198 | if (hasattr(target, fname) and 199 | not hasattr(getattr(target, fname), 'signature')): 200 | continue 201 | 202 | # Create a new function so we can attach properties to it. 203 | def MakeBoundFunction(func): 204 | # We need the lambda to capture "func" from the enclosing scope. 205 | return lambda *args, **kwargs: func.call(*args, **kwargs) # pylint: disable=unnecessary-lambda 206 | bound_function = MakeBoundFunction(api_func) 207 | 208 | # Add docs. If there are non-ASCII characters in the docs, and we're in 209 | # Python 2, use a hammer to force them into a str. 210 | try: 211 | setattr(bound_function, '__name__', str(name)) 212 | except TypeError: 213 | setattr(bound_function, '__name__', name.encode('utf8')) 214 | try: 215 | bound_function.__doc__ = str(api_func) 216 | except UnicodeEncodeError: 217 | bound_function.__doc__ = api_func.__str__().encode('utf8') 218 | 219 | # Attach the signature object for documentation generators. 220 | bound_function.signature = signature 221 | 222 | # Mark as deprecated if needed. 223 | if signature.get('deprecated'): 224 | deprecated_decorator = deprecation.Deprecated(signature['deprecated']) 225 | bound_function = deprecated_decorator(bound_function) 226 | 227 | # Decide whether this is a static or an instance function. 228 | is_instance = (signature['args'] and 229 | ee_types.isSubtype(signature['args'][0]['type'], 230 | type_name)) 231 | if not is_instance: 232 | bound_function = staticmethod(bound_function) 233 | 234 | # Attach the function as a method. 235 | setattr(target, fname, bound_function) 236 | 237 | @staticmethod 238 | def clearApi(target): 239 | """Removes all methods added by importApi() from a target class. 240 | 241 | Args: 242 | target: The class to remove from. 243 | """ 244 | for attr_name in dir(target): 245 | attr_value = getattr(target, attr_name) 246 | if callable(attr_value) and hasattr(attr_value, 'signature'): 247 | delattr(target, attr_name) 248 | -------------------------------------------------------------------------------- /ee/cli/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Exposes CLI tool as a package when installing via setup.py 3 | -------------------------------------------------------------------------------- /ee/cli/build_archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Builds a self-contained archive from the open source release of EE CLI tool. 4 | 5 | EE_CLI_DIR="earthengine-cli" 6 | mkdir -p $EE_CLI_DIR/third_party 7 | pip install -t $EE_CLI_DIR/third_party earthengine-api 8 | 9 | cp eecli.py commands.py utils.py $EE_CLI_DIR/ 10 | 11 | cp eecli_wrapper.py $EE_CLI_DIR/earthengine 12 | chmod +x $EE_CLI_DIR/earthengine 13 | 14 | tar cvf earthengine-cli.tar.gz $EE_CLI_DIR 15 | rm -rf $EE_CLI_DIR 16 | -------------------------------------------------------------------------------- /ee/cli/eecli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Executable for the Earth Engine command line interface. 3 | 4 | This executable starts a Python Cmd instance to receive and process command 5 | line input entered by the user. If the executable is invoked with some 6 | command line arguments, the Cmd is launched in the one-off mode, where 7 | the provided arguments are processed as a single command after which the 8 | program is terminated. Otherwise, this executable will launch the Cmd in the 9 | interactive (looping) mode, where the user will be able to run multiple 10 | commands as in a typical terminal program. 11 | """ 12 | 13 | from __future__ import print_function 14 | 15 | import argparse 16 | import sys 17 | 18 | import ee 19 | from ee.cli import commands 20 | from ee.cli import utils 21 | 22 | 23 | class CommandDispatcher(commands.Dispatcher): 24 | name = 'main' 25 | COMMANDS = commands.EXTERNAL_COMMANDS 26 | 27 | 28 | def main(): 29 | # Set the program name to 'earthengine' for proper help text display. 30 | parser = argparse.ArgumentParser( 31 | prog='earthengine', description='Earth Engine Command Line Interface.') 32 | parser.add_argument( 33 | '--ee_config', help='Path to the earthengine configuration file. ' 34 | 'Defaults to "~/%s".' % utils.DEFAULT_EE_CONFIG_FILE_RELATIVE) 35 | parser.add_argument( 36 | '--service_account_file', help='Path to a service account credentials' 37 | 'file. Overrides any ee_config if specified.') 38 | parser.add_argument( 39 | '--use_cloud_api', 40 | help='Enables the new experimental EE Cloud API backend. (on by default)', 41 | action='store_true', 42 | dest='use_cloud_api') 43 | parser.add_argument( 44 | '--no-use_cloud_api', 45 | help='Disables the new experimental EE Cloud API backend.', 46 | action='store_false', 47 | dest='use_cloud_api') 48 | parser.set_defaults(use_cloud_api=True) 49 | 50 | dispatcher = CommandDispatcher(parser) 51 | 52 | # Print the list of commands if the user supplied no arguments at all. 53 | if len(sys.argv) == 1: 54 | parser.print_help() 55 | return 56 | 57 | args = parser.parse_args() 58 | config = utils.CommandLineConfig( 59 | args.ee_config, args.service_account_file, args.use_cloud_api) 60 | 61 | # TODO(user): Remove this warning once things are officially launched 62 | # and the old API is removed. 63 | if args.use_cloud_api: 64 | print('Running command using Cloud API. Set --no-use_cloud_api to ' 65 | 'go back to using the API') 66 | 67 | # Catch EEException errors, which wrap server-side Earth Engine 68 | # errors, and print the error message without the irrelevant local 69 | # stack trace. (Individual commands may also catch EEException if 70 | # they want to be able to continue despite errors.) 71 | try: 72 | dispatcher.run(args, config) 73 | except ee.EEException as e: 74 | print(e) 75 | sys.exit(1) 76 | 77 | if __name__ == '__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /ee/cli/eecli_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Wrapper module for running eecli.main() from the command line.""" 4 | 5 | import os 6 | import sys 7 | 8 | if not (2, 6) <= sys.version_info[:3] < (3,): 9 | sys.exit('earthengine requires python 2.6 or 2.7.') 10 | 11 | 12 | def OutputAndExit(message): 13 | sys.stderr.write('%s\n' % message) 14 | sys.exit(1) 15 | 16 | 17 | EECLI_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) 18 | if not EECLI_DIR: 19 | OutputAndExit('Unable to determine where earthengine CLI is installed. Sorry,' 20 | ' cannot run correctly without this.\n') 21 | 22 | # The wrapper script adds all third_party libraries to the Python path, since 23 | # we don't assume any third party libraries are installed system-wide. 24 | THIRD_PARTY_DIR = os.path.join(EECLI_DIR, 'third_party') 25 | sys.path.insert(0, THIRD_PARTY_DIR) 26 | 27 | 28 | def RunMain(): 29 | import eecli # pylint: disable=g-import-not-at-top 30 | sys.exit(eecli.main()) 31 | 32 | if __name__ == '__main__': 33 | RunMain() 34 | -------------------------------------------------------------------------------- /ee/cli/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Support utilities used by the Earth Engine command line interface. 3 | 4 | This module defines the Command class which is the base class of all 5 | the commands supported by the EE command line tool. It also defines 6 | the classes for configuration and runtime context management. 7 | """ 8 | from __future__ import print_function 9 | import collections 10 | from datetime import datetime 11 | import json 12 | import os 13 | import re 14 | import threading 15 | import time 16 | 17 | import urllib 18 | import httplib2 19 | 20 | from google.oauth2.credentials import Credentials 21 | import ee 22 | 23 | HOMEDIR = os.path.expanduser('~') 24 | EE_CONFIG_FILE = 'EE_CONFIG_FILE' 25 | DEFAULT_EE_CONFIG_FILE_RELATIVE = os.path.join( 26 | '.config', 'earthengine', 'credentials') 27 | DEFAULT_EE_CONFIG_FILE = os.path.join( 28 | HOMEDIR, DEFAULT_EE_CONFIG_FILE_RELATIVE) 29 | 30 | CONFIG_PARAMS = { 31 | 'url': 'https://earthengine.googleapis.com', 32 | 'account': None, 33 | 'private_key': None, 34 | 'refresh_token': None, 35 | 'use_cloud_api': False, 36 | 'cloud_api_key': None, 37 | 'project': None, 38 | } 39 | 40 | TASK_FINISHED_STATES = (ee.batch.Task.State.COMPLETED, 41 | ee.batch.Task.State.FAILED, 42 | ee.batch.Task.State.CANCELLED) 43 | 44 | 45 | class CommandLineConfig(object): 46 | """Holds the configuration parameters used by the EE command line interface. 47 | 48 | This class attempts to load the configuration parameters from a file 49 | specified as a constructor argument. If not provided, it attempts to load 50 | the configuration from a file specified via the EE_CONFIG_FILE environment 51 | variable. If the variable is not set, it looks for a JSON file at the 52 | path ~/.config/earthengine/credentials. If all fails, it falls back to using 53 | some predefined defaults for each configuration parameter. 54 | 55 | If --service_account_file is specified, it is used instead. 56 | """ 57 | 58 | def __init__( 59 | self, config_file=None, service_account_file=None, use_cloud_api=False): 60 | if not config_file: 61 | config_file = os.environ.get(EE_CONFIG_FILE, DEFAULT_EE_CONFIG_FILE) 62 | self.config_file = config_file 63 | config = {} 64 | if os.path.exists(config_file): 65 | with open(config_file) as config_file_json: 66 | config = json.load(config_file_json) 67 | CONFIG_PARAMS['use_cloud_api'] = use_cloud_api 68 | for key, default_value in CONFIG_PARAMS.items(): 69 | setattr(self, key, config.get(key, default_value)) 70 | self.service_account_file = service_account_file 71 | if service_account_file: 72 | # Load the file to verify that it exists. 73 | with open(service_account_file) as service_file_json: 74 | service = json.load(service_file_json) 75 | for key, value in service.items(): 76 | setattr(self, key, value) 77 | 78 | def ee_init(self): 79 | """Loads the EE credentials and initializes the EE client.""" 80 | if self.service_account_file: 81 | credentials = ee.ServiceAccountCredentials( 82 | self.client_email, self.service_account_file) 83 | elif self.account and self.private_key: 84 | credentials = ee.ServiceAccountCredentials(self.account, self.private_key) 85 | elif self.refresh_token: 86 | credentials = Credentials( 87 | None, 88 | refresh_token=self.refresh_token, 89 | token_uri=ee.oauth.TOKEN_URI, 90 | client_id=ee.oauth.CLIENT_ID, 91 | client_secret=ee.oauth.CLIENT_SECRET, 92 | scopes=ee.oauth.SCOPES) 93 | else: 94 | credentials = 'persistent' 95 | 96 | ee.Initialize( 97 | credentials=credentials, 98 | opt_url=self.url, 99 | use_cloud_api=self.use_cloud_api, 100 | cloud_api_key=self.cloud_api_key, 101 | project=self.project) 102 | 103 | def save(self): 104 | config = {} 105 | for key in CONFIG_PARAMS: 106 | value = getattr(self, key) 107 | if value is not None: 108 | config[key] = value 109 | with open(self.config_file, 'w') as output_file: 110 | json.dump(config, output_file) 111 | 112 | 113 | def query_yes_no(msg): 114 | print('%s (y/n)' % msg) 115 | while True: 116 | confirm = raw_input().lower() 117 | if confirm == 'y': 118 | return True 119 | elif confirm == 'n': 120 | return False 121 | else: 122 | print('Please respond with \'y\' or \'n\'.') 123 | 124 | 125 | def truncate(string, length): 126 | return (string[:length] + '..') if len(string) > length else string 127 | 128 | 129 | def wait_for_task(task_id, timeout, log_progress=True): 130 | """Waits for the specified task to finish, or a timeout to occur.""" 131 | start = time.time() 132 | elapsed = 0 133 | last_check = 0 134 | while True: 135 | elapsed = time.time() - start 136 | status = ee.data.getTaskStatus(task_id)[0] 137 | state = status['state'] 138 | if state in TASK_FINISHED_STATES: 139 | error_message = status.get('error_message', None) 140 | print('Task %s ended at state: %s after %.2f seconds' 141 | % (task_id, state, elapsed)) 142 | if error_message: 143 | raise ee.ee_exception.EEException('Error: %s' % error_message) 144 | return 145 | if log_progress and elapsed - last_check >= 30: 146 | print('[{:%H:%M:%S}] Current state for task {}: {}' 147 | .format(datetime.now(), task_id, state)) 148 | last_check = elapsed 149 | remaining = timeout - elapsed 150 | if remaining > 0: 151 | time.sleep(min(10, remaining)) 152 | else: 153 | break 154 | print('Wait for task %s timed out after %.2f seconds' % (task_id, elapsed)) 155 | 156 | 157 | def wait_for_tasks(task_id_list, timeout, log_progress=False): 158 | """For each task specified in task_id_list, wait for that task or timeout.""" 159 | 160 | if len(task_id_list) == 1: 161 | wait_for_task(task_id_list[0], timeout, log_progress) 162 | return 163 | 164 | threads = [] 165 | for task_id in task_id_list: 166 | t = threading.Thread(target=wait_for_task, 167 | args=(task_id, timeout, log_progress)) 168 | threads.append(t) 169 | t.start() 170 | 171 | for thread in threads: 172 | thread.join() 173 | 174 | status_list = ee.data.getTaskStatus(task_id_list) 175 | status_counts = collections.defaultdict(int) 176 | for status in status_list: 177 | status_counts[status['state']] += 1 178 | num_incomplete = (len(status_list) - status_counts['COMPLETED'] 179 | - status_counts['FAILED'] - status_counts['CANCELLED']) 180 | print('Finished waiting for tasks.\n Status summary:') 181 | print(' %d tasks completed successfully.' % status_counts['COMPLETED']) 182 | print(' %d tasks failed.' % status_counts['FAILED']) 183 | print(' %d tasks cancelled.' % status_counts['CANCELLED']) 184 | print(' %d tasks are still incomplete (timed-out)' % num_incomplete) 185 | 186 | 187 | def expand_gcs_wildcards(source_files): 188 | """Implements glob-like '*' wildcard completion for cloud storage objects. 189 | 190 | Args: 191 | source_files: A list of one or more cloud storage paths of the format 192 | gs://[bucket]/[path-maybe-with-wildcards] 193 | 194 | Yields: 195 | cloud storage paths of the above format with '*' wildcards expanded. 196 | Raises: 197 | EEException: If badly formatted source_files 198 | (e.g., missing gs://) are specified 199 | """ 200 | for source in source_files: 201 | if '*' not in source: 202 | yield source 203 | continue 204 | 205 | # We extract the bucket and prefix from the input path to match 206 | # the parameters for calling GCS list objects and reduce the number 207 | # of items returned by that API call 208 | 209 | # Capture the part of the path after gs:// and before the first / 210 | bucket_regex = 'gs://([a-z0-9_.-]+)(/.*)' 211 | bucket_match = re.match(bucket_regex, source) 212 | if bucket_match: 213 | bucket, rest = bucket_match.group(1, 2) 214 | else: 215 | raise ee.ee_exception.EEException( 216 | 'Badly formatted source file or bucket: %s' % source) 217 | prefix = rest[:rest.find('*')] # Everything before the first wildcard 218 | 219 | bucket_files = _gcs_ls(bucket, prefix) 220 | 221 | # Regex to match the source path with wildcards expanded 222 | regex = re.escape(source).replace(r'\*', '[^/]*') + '$' 223 | for gcs_path in bucket_files: 224 | if re.match(regex, gcs_path): 225 | yield gcs_path 226 | 227 | 228 | def _gcs_ls(bucket, prefix=''): 229 | """Retrieve a list of cloud storage filepaths from the given bucket. 230 | 231 | Args: 232 | bucket: The cloud storage bucket to be queried 233 | prefix: Optional, a prefix used to select the objects to return 234 | Yields: 235 | Cloud storage filepaths matching the given bucket and prefix 236 | Raises: 237 | EEException: 238 | If there is an error in accessing the specified bucket 239 | """ 240 | 241 | base_url = 'https://www.googleapis.com/storage/v1/b/%s/o'%bucket 242 | method = 'GET' 243 | http = ee.data.authorizeHttp(httplib2.Http(0)) 244 | next_page_token = None 245 | 246 | # Loop to handle paginated responses from GCS; 247 | # Exits once no 'next page token' is returned 248 | while True: 249 | params = {'fields': 'items/name,nextPageToken'} 250 | if next_page_token: 251 | params['pageToken'] = next_page_token 252 | if prefix: 253 | params['prefix'] = prefix 254 | payload = urllib.urlencode(params) 255 | 256 | url = base_url + '?' + payload 257 | try: 258 | response, content = http.request(url, method=method) 259 | except httplib2.HttpLib2Error as e: 260 | raise ee.ee_exception.EEException( 261 | 'Unexpected HTTP error: %s' % e.message) 262 | 263 | if response.status < 100 or response.status >= 300: 264 | raise ee.ee_exception.EEException(('Error retrieving bucket %s;' 265 | ' Server returned HTTP code: %d' % 266 | (bucket, response.status))) 267 | 268 | json_content = json.loads(content) 269 | if 'error' in json_content: 270 | json_error = json_content['error']['message'] 271 | raise ee.ee_exception.EEException('Error retrieving bucket %s: %s' % 272 | (bucket, json_error)) 273 | 274 | if 'items' not in json_content: 275 | raise ee.ee_exception.EEException( 276 | 'Cannot find items list in the response from GCS: %s' % json_content) 277 | objects = json_content['items'] 278 | object_names = [str(gc_object['name']) for gc_object in objects] 279 | 280 | for name in object_names: 281 | yield 'gs://%s/%s' % (bucket, name) 282 | 283 | # GCS indicates no more results 284 | if 'nextPageToken' not in json_content: 285 | return 286 | 287 | # Load next page, continue at beginning of while True: 288 | next_page_token = json_content['nextPageToken'] 289 | -------------------------------------------------------------------------------- /ee/collection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Common representation for ImageCollection and FeatureCollection. 3 | 4 | This class is never intended to be instantiated by the user. 5 | """ 6 | 7 | 8 | 9 | # Using lowercase function naming to match the JavaScript names. 10 | # pylint: disable=g-bad-name 11 | 12 | from . import apifunction 13 | from . import deprecation 14 | from . import ee_exception 15 | from . import element 16 | from . import filter # pylint: disable=redefined-builtin 17 | 18 | 19 | class Collection(element.Element): 20 | """Base class for ImageCollection and FeatureCollection.""" 21 | 22 | _initialized = False 23 | 24 | def __init__(self, func, args, opt_varName=None): 25 | """Constructs a collection by initializing its ComputedObject.""" 26 | super(Collection, self).__init__(func, args, opt_varName) 27 | 28 | @classmethod 29 | def initialize(cls): 30 | """Imports API functions to this class.""" 31 | if not cls._initialized: 32 | apifunction.ApiFunction.importApi(cls, 'Collection', 'Collection') 33 | apifunction.ApiFunction.importApi( 34 | cls, 'AggregateFeatureCollection', 'Collection', 'aggregate_') 35 | cls._initialized = True 36 | 37 | @classmethod 38 | def reset(cls): 39 | """Removes imported API functions from this class. 40 | 41 | Also resets the serial ID used for mapping Python functions to 0. 42 | """ 43 | apifunction.ApiFunction.clearApi(cls) 44 | cls._initialized = False 45 | 46 | def filter(self, new_filter): 47 | """Apply a filter to this collection. 48 | 49 | Args: 50 | new_filter: Filter to add to this collection. 51 | 52 | Returns: 53 | The filtered collection object. 54 | """ 55 | if not new_filter: 56 | raise ee_exception.EEException('Empty filters.') 57 | return self._cast(apifunction.ApiFunction.call_( 58 | 'Collection.filter', self, new_filter)) 59 | 60 | @deprecation.CanUseDeprecated 61 | def filterMetadata(self, name, operator, value): 62 | """Shortcut to add a metadata filter to a collection. 63 | 64 | This is equivalent to self.filter(Filter().metadata(...)). 65 | 66 | Args: 67 | name: Name of a property to filter. 68 | operator: Name of a comparison operator as defined 69 | by FilterCollection. Possible values are: "equals", "less_than", 70 | "greater_than", "not_equals", "not_less_than", "not_greater_than", 71 | "starts_with", "ends_with", "not_starts_with", "not_ends_with", 72 | "contains", "not_contains". 73 | value: The value to compare against. 74 | 75 | Returns: 76 | The filtered collection. 77 | """ 78 | return self.filter(filter.Filter.metadata_(name, operator, value)) 79 | 80 | def filterBounds(self, geometry): 81 | """Shortcut to add a geometry filter to a collection. 82 | 83 | Items in the collection with a footprint that fails to intersect 84 | the given geometry will be excluded when the collection is evaluated. 85 | This is equivalent to self.filter(Filter().geometry(...)). 86 | 87 | Args: 88 | geometry: The boundary to filter to either as a GeoJSON geometry, 89 | or a FeatureCollection, from which a geometry will be extracted. 90 | 91 | Returns: 92 | The filter object. 93 | """ 94 | return self.filter(filter.Filter.geometry(geometry)) 95 | 96 | def filterDate(self, start, opt_end=None): 97 | """Shortcut to filter a collection with a date range. 98 | 99 | Items in the collection with a time_start property that doesn't 100 | fall between the start and end dates will be excluded. 101 | This is equivalent to self.filter(Filter().date(...)). 102 | 103 | Args: 104 | start: The start date as a Date object, a string representation of 105 | a date, or milliseconds since epoch. 106 | opt_end: The end date as a Date object, a string representation of 107 | a date, or milliseconds since epoch. 108 | 109 | Returns: 110 | The filter object. 111 | """ 112 | return self.filter(filter.Filter.date(start, opt_end)) 113 | 114 | def getInfo(self): 115 | """Returns all the known information about this collection. 116 | 117 | This function makes an REST call to to retrieve all the known information 118 | about this collection. 119 | 120 | Returns: 121 | The return contents vary but will include at least: 122 | features: an array containing metadata about the items in the 123 | collection that passed all filters. 124 | properties: a dictionary containing the collection's metadata 125 | properties. 126 | """ 127 | return super(Collection, self).getInfo() 128 | 129 | def limit(self, maximum, opt_property=None, opt_ascending=None): 130 | """Limit a collection to the specified number of elements. 131 | 132 | This limits a collection to the specified number of elements, optionally 133 | sorting them by a specified property first. 134 | 135 | Args: 136 | maximum: The number to limit the collection to. 137 | opt_property: The property to sort by, if sorting. 138 | opt_ascending: Whether to sort in ascending or descending order. 139 | The default is true (ascending). 140 | 141 | Returns: 142 | The collection. 143 | """ 144 | args = {'collection': self, 'limit': maximum} 145 | if opt_property is not None: 146 | args['key'] = opt_property 147 | if opt_ascending is not None: 148 | args['ascending'] = opt_ascending 149 | return self._cast( 150 | apifunction.ApiFunction.apply_('Collection.limit', args)) 151 | 152 | def sort(self, prop, opt_ascending=None): 153 | """Sort a collection by the specified property. 154 | 155 | Args: 156 | prop: The property to sort by. 157 | opt_ascending: Whether to sort in ascending or descending 158 | order. The default is true (ascending). 159 | 160 | Returns: 161 | The collection. 162 | """ 163 | args = {'collection': self, 'key': prop} 164 | if opt_ascending is not None: 165 | args['ascending'] = opt_ascending 166 | return self._cast( 167 | apifunction.ApiFunction.apply_('Collection.limit', args)) 168 | 169 | @staticmethod 170 | def name(): 171 | return 'Collection' 172 | 173 | @staticmethod 174 | def elementType(): 175 | """Returns the type of the collection's elements.""" 176 | return element.Element 177 | 178 | def map(self, algorithm, opt_dropNulls=None): 179 | """Maps an algorithm over a collection. 180 | 181 | Args: 182 | algorithm: The operation to map over the images or features of the 183 | collection, a Python function that receives an image or features and 184 | returns one. The function is called only once and the result is 185 | captured as a description, so it cannot perform imperative operations 186 | or rely on external state. 187 | opt_dropNulls: If true, the mapped algorithm is allowed to return nulls, 188 | and the elements for which it returns nulls will be dropped. 189 | 190 | Returns: 191 | The mapped collection. 192 | 193 | Raises: 194 | ee_exception.EEException: if algorithm is not a function. 195 | """ 196 | element_type = self.elementType() 197 | with_cast = lambda e: algorithm(element_type(e)) 198 | return self._cast(apifunction.ApiFunction.call_( 199 | 'Collection.map', self, with_cast, opt_dropNulls)) 200 | 201 | def iterate(self, algorithm, first=None): 202 | """Iterates over a collection with an algorithm. 203 | 204 | Applies a user-supplied function to each element of a collection. The 205 | user-supplied function is given two arguments: the current element, and 206 | the value returned by the previous call to iterate() or the first argument, 207 | for the first iteration. The result is the value returned by the final 208 | call to the user-supplied function. 209 | 210 | Args: 211 | algorithm: The function to apply to each element. Must take two 212 | arguments - an element of the collection and the value from the 213 | previous iteration. 214 | first: The initial state. 215 | 216 | Returns: 217 | The result of the Collection.iterate() call. 218 | 219 | Raises: 220 | ee_exception.EEException: if algorithm is not a function. 221 | """ 222 | element_type = self.elementType() 223 | with_cast = lambda e, prev: algorithm(element_type(e), prev) 224 | return apifunction.ApiFunction.call_( 225 | 'Collection.iterate', self, with_cast, first) 226 | -------------------------------------------------------------------------------- /ee/computedobject.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A representation of an Earth Engine computed object.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | # pylint: disable=g-bad-import-order 10 | import six 11 | 12 | from . import data 13 | from . import ee_exception 14 | from . import encodable 15 | from . import serializer 16 | 17 | 18 | class ComputedObjectMetaclass(type): 19 | """A meta-class that makes type coercion idempotent. 20 | 21 | If an instance of a ComputedObject subclass is instantiated by passing 22 | another instance of that class as the sole argument, this short-circuits 23 | and returns that argument. 24 | """ 25 | 26 | def __call__(cls, *args, **kwargs): 27 | """Creates a computed object, catching self-casts.""" 28 | if len(args) == 1 and not kwargs and isinstance(args[0], cls): 29 | # Self-casting returns the argument unchanged. 30 | return args[0] 31 | else: 32 | return type.__call__(cls, *args, **kwargs) 33 | 34 | 35 | class ComputedObject(six.with_metaclass( 36 | ComputedObjectMetaclass, encodable.Encodable)): 37 | """A representation of an Earth Engine computed object. 38 | 39 | This is a base class for most API objects. 40 | 41 | The class itself is not abstract as it is used to wrap the return values of 42 | algorithms that produce unrecognized types with the minimal functionality 43 | necessary to interact well with the rest of the API. 44 | 45 | ComputedObjects come in two flavors: 46 | 1. If func != null and args != null, the ComputedObject is encoded as an 47 | invocation of func with args. 48 | 2. If func == null and args == null, the ComputedObject is a variable 49 | reference. The variable name is stored in its varName member. Note that 50 | in this case, varName may still be null; this allows the name to be 51 | deterministically generated at a later time. This is used to generate 52 | deterministic variable names for mapped functions, ensuring that nested 53 | mapping calls do not use the same variable name. 54 | """ 55 | 56 | def __init__(self, func, args, opt_varName=None): 57 | """Creates a computed object. 58 | 59 | Args: 60 | func: The ee.Function called to compute this object, either as an 61 | Algorithm name or an ee.Function object. 62 | args: A dictionary of arguments to pass to the specified function. 63 | Note that the caller is responsible for promoting the arguments 64 | to the correct types. 65 | opt_varName: A variable name. If not None, the object will be encoded 66 | as a reference to a CustomFunction variable of this name, and both 67 | 'func' and 'args' must be None. If all arguments are None, the 68 | object is considered an unnamed variable, and a name will be 69 | generated when it is included in an ee.CustomFunction. 70 | """ 71 | if opt_varName and (func or args): 72 | raise ee_exception.EEException( 73 | 'When "opt_varName" is specified, "func" and "args" must be null.') 74 | self.func = func 75 | self.args = args 76 | self.varName = opt_varName 77 | 78 | def __eq__(self, other): 79 | # pylint: disable=unidiomatic-typecheck 80 | return (type(self) == type(other) and 81 | self.__dict__ == other.__dict__) 82 | 83 | def __ne__(self, other): 84 | return not self.__eq__(other) 85 | 86 | def __hash__(self): 87 | return hash(ComputedObject.freeze(self.__dict__)) 88 | 89 | def getInfo(self): 90 | """Fetch and return information about this object. 91 | 92 | Returns: 93 | The object can evaluate to anything. 94 | """ 95 | return data.computeValue(self) 96 | 97 | def encode(self, encoder): 98 | """Encodes the object in a format compatible with Serializer.""" 99 | if self.isVariable(): 100 | return { 101 | 'type': 'ArgumentRef', 102 | 'value': self.varName 103 | } 104 | else: 105 | # Encode the function that we're calling. 106 | func = encoder(self.func) 107 | # Built-in functions are encoded as strings under a different key. 108 | key = 'functionName' if isinstance(func, six.string_types) else 'function' 109 | 110 | # Encode all arguments recursively. 111 | encoded_args = {} 112 | for name, value in self.args.items(): 113 | if value is not None: 114 | encoded_args[name] = encoder(value) 115 | 116 | return { 117 | 'type': 'Invocation', 118 | 'arguments': encoded_args, 119 | key: func 120 | } 121 | 122 | def encode_cloud_value(self, encoder): 123 | if self.isVariable(): 124 | return {'argumentReference': self.varName} 125 | else: 126 | if isinstance(self.func, six.string_types): 127 | invocation = {'functionName': self.func} 128 | else: 129 | invocation = self.func.encode_cloud_invocation(encoder) 130 | 131 | # Encode all arguments recursively. 132 | encoded_args = {} 133 | for name in sorted(self.args): 134 | value = self.args[name] 135 | if value is not None: 136 | encoded_args[name] = {'valueReference': encoder(value)} 137 | invocation['arguments'] = encoded_args 138 | return {'functionInvocationValue': invocation} 139 | 140 | def serialize( 141 | self, 142 | opt_pretty=False, 143 | for_cloud_api=False 144 | ): 145 | """Serialize this object into a JSON string. 146 | 147 | Args: 148 | opt_pretty: A flag indicating whether to pretty-print the JSON. 149 | for_cloud_api: Whether the encoding should be done for the Cloud API 150 | or the legacy API. 151 | 152 | Returns: 153 | The serialized representation of this object. 154 | """ 155 | return serializer.toJSON( 156 | self, 157 | opt_pretty, 158 | for_cloud_api=for_cloud_api 159 | ) 160 | 161 | def __str__(self): 162 | """Writes out the object in a human-readable form.""" 163 | return 'ee.%s(%s)' % (self.name(), serializer.toReadableJSON(self)) 164 | 165 | def isVariable(self): 166 | """Returns whether this computed object is a variable reference.""" 167 | # We can't just check for varName != null, since we allow that 168 | # to remain null until for CustomFunction.resolveNamelessArgs_(). 169 | return self.func is None and self.args is None 170 | 171 | def aside(self, func, *var_args): 172 | """Calls a function passing this object as the first argument. 173 | 174 | Returns the object itself for chaining. Convenient e.g. when debugging: 175 | 176 | c = (ee.ImageCollection('foo').aside(logging.info) 177 | .filterDate('2001-01-01', '2002-01-01').aside(logging.info) 178 | .filterBounds(geom).aside(logging.info) 179 | .aside(addToMap, {'min': 0, 'max': 142}) 180 | .select('a', 'b')) 181 | 182 | Args: 183 | func: The function to call. 184 | *var_args: Any extra arguments to pass to the function. 185 | 186 | Returns: 187 | The same object, for chaining. 188 | """ 189 | func(self, *var_args) 190 | return self 191 | 192 | @classmethod 193 | def name(cls): 194 | """Returns the name of the object, used in __str__().""" 195 | return 'ComputedObject' 196 | 197 | @classmethod 198 | def _cast(cls, obj): 199 | """Cast a ComputedObject to a new instance of the same class as this. 200 | 201 | Args: 202 | obj: The object to cast. 203 | 204 | Returns: 205 | The cast object, and instance of the class on which this method is called. 206 | """ 207 | if isinstance(obj, cls): 208 | return obj 209 | else: 210 | result = cls.__new__(cls) 211 | result.func = obj.func 212 | result.args = obj.args 213 | result.varName = obj.varName 214 | return result 215 | 216 | @staticmethod 217 | def freeze(obj): 218 | """Freeze a list or dict so it can be hashed.""" 219 | if isinstance(obj, dict): 220 | return frozenset( 221 | (key, ComputedObject.freeze(val)) for key, val in obj.items()) 222 | elif isinstance(obj, list): 223 | return tuple(map(ComputedObject.freeze, obj)) 224 | else: 225 | return obj 226 | -------------------------------------------------------------------------------- /ee/customfunction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """An object representing a custom EE Function.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | # pylint: disable=g-bad-import-order 10 | import six 11 | 12 | from . import computedobject 13 | from . import ee_types 14 | from . import encodable 15 | from . import function 16 | from . import serializer 17 | 18 | 19 | # Multiple inheritance, yay! This is necessary because a CustomFunction needs to 20 | # know how to encode itself in different ways: 21 | # - as an Encodable: encode its definition 22 | # - as a Function: encode its invocation (which may also involve encoding its 23 | # definition, if that hasn't happened yet). 24 | class CustomFunction(function.Function, encodable.Encodable): 25 | """An object representing a custom EE Function.""" 26 | 27 | def __init__(self, signature, body): 28 | """Creates a function defined by a given expression with unbound variables. 29 | 30 | The expression is created by evaluating the given function 31 | using variables as placeholders. 32 | 33 | Args: 34 | signature: The function signature. If any of the argument names are 35 | null, their names will be generated deterministically, based on 36 | the body. 37 | body: The Python function to evaluate. 38 | """ 39 | variables = [CustomFunction.variable(arg['type'], arg['name']) 40 | for arg in signature['args']] 41 | 42 | # The signature of the function. 43 | self._signature = CustomFunction._resolveNamelessArgs( 44 | signature, variables, body) 45 | 46 | # The expression to evaluate. 47 | self._body = body(*variables) 48 | 49 | def encode(self, encoder): 50 | return { 51 | 'type': 'Function', 52 | 'argumentNames': [x['name'] for x in self._signature['args']], 53 | 'body': encoder(self._body) 54 | } 55 | 56 | def encode_cloud_value(self, encoder): 57 | return { 58 | 'functionDefinitionValue': { 59 | 'argumentNames': [x['name'] for x in self._signature['args']], 60 | 'body': encoder(self._body) 61 | } 62 | } 63 | 64 | def encode_invocation(self, encoder): 65 | return self.encode(encoder) 66 | 67 | def encode_cloud_invocation(self, encoder): 68 | return {'functionReference': encoder(self)} 69 | 70 | def getSignature(self): 71 | """Returns a description of the interface provided by this function.""" 72 | return self._signature 73 | 74 | @staticmethod 75 | def variable(type_name, name): 76 | """Returns a placeholder variable with a given name and EE type. 77 | 78 | Args: 79 | type_name: A class to mimic. 80 | name: The name of the variable as it will appear in the 81 | arguments of the custom functions that use this variable. If null, 82 | a name will be auto-generated in _resolveNamelessArgs(). 83 | 84 | Returns: 85 | A variable with the given name implementing the given type. 86 | """ 87 | var_type = ee_types.nameToClass(type_name) or computedobject.ComputedObject 88 | result = var_type.__new__(var_type) 89 | result.func = None 90 | result.args = None 91 | result.varName = name 92 | return result 93 | 94 | @staticmethod 95 | def create(func, return_type, arg_types): 96 | """Creates a CustomFunction. 97 | 98 | The result calls a given native function with the specified return type and 99 | argument types and auto-generated argument names. 100 | 101 | Args: 102 | func: The native function to wrap. 103 | return_type: The type of the return value, either as a string or a 104 | class reference. 105 | arg_types: The types of the arguments, either as strings or class 106 | references. 107 | 108 | Returns: 109 | The constructed CustomFunction. 110 | """ 111 | 112 | def StringifyType(t): 113 | return t if isinstance(t, six.string_types) else ee_types.classToName(t) 114 | 115 | args = [{'name': None, 'type': StringifyType(i)} for i in arg_types] 116 | signature = { 117 | 'name': '', 118 | 'returns': StringifyType(return_type), 119 | 'args': args 120 | } 121 | return CustomFunction(signature, func) 122 | 123 | @staticmethod 124 | def _resolveNamelessArgs(signature, variables, body): 125 | """Deterministically generates names for unnamed variables. 126 | 127 | The names are based on the body of the function. 128 | 129 | Args: 130 | signature: The signature which may contain null argument names. 131 | variables: A list of variables, some of which may be nameless. 132 | These will be updated to include names when this method returns. 133 | body: The Python function to evaluate. 134 | 135 | Returns: 136 | The signature with null arg names resolved. 137 | """ 138 | nameless_arg_indices = [] 139 | for i, variable in enumerate(variables): 140 | if variable.varName is None: 141 | nameless_arg_indices.append(i) 142 | 143 | # Do we have any nameless arguments at all? 144 | if not nameless_arg_indices: 145 | return signature 146 | 147 | # Generate the name base by counting the number of custom functions 148 | # within the body. 149 | def CountFunctions(expression): 150 | """Counts the number of custom functions in a serialized expression.""" 151 | count = 0 152 | if isinstance(expression, dict): 153 | if expression.get('type') == 'Function': 154 | # Technically this allows false positives if one of the user 155 | # dictionaries contains type=Function, but that does not matter 156 | # for this use case, as we only care about determinism. 157 | count += 1 158 | else: 159 | for sub_expression in expression.values(): 160 | count += CountFunctions(sub_expression) 161 | elif isinstance(expression, (list, tuple)): 162 | for sub_expression in expression: 163 | count += CountFunctions(sub_expression) 164 | return count 165 | serialized_body = serializer.encode(body(*variables)) 166 | base_name = '_MAPPING_VAR_%d_' % CountFunctions(serialized_body) 167 | 168 | # Update the vars and signature by the name. 169 | for (i, index) in enumerate(nameless_arg_indices): 170 | name = base_name + str(i) 171 | variables[index].varName = name 172 | signature['args'][index]['name'] = name 173 | 174 | return signature 175 | -------------------------------------------------------------------------------- /ee/deprecation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Decorators to mark function deprecated.""" 3 | 4 | 5 | 6 | import functools 7 | import warnings 8 | 9 | 10 | def Deprecated(message): 11 | """Returns a decorator with a given warning message.""" 12 | 13 | def Decorator(func): 14 | """Emits a deprecation warning when the decorated function is called. 15 | 16 | Also adds the deprecation message to the function's docstring. 17 | 18 | Args: 19 | func: The function to deprecate. 20 | 21 | Returns: 22 | func: The wrapped function. 23 | """ 24 | 25 | @functools.wraps(func) 26 | def Wrapper(*args, **kwargs): 27 | warnings.warn_explicit( 28 | '%s() is deprecated: %s' % (func.__name__, message), 29 | category=DeprecationWarning, 30 | filename=func.__code__.co_filename, 31 | lineno=func.__code__.co_firstlineno + 1) 32 | return func(*args, **kwargs) 33 | deprecation_message = '\nDEPRECATED: ' + message 34 | try: 35 | Wrapper.__doc__ += deprecation_message 36 | # If there are non-ASCII characters in the docs, and we're in 37 | # Python 2, use a hammer to force them into a str. 38 | except UnicodeDecodeError: 39 | Wrapper.__doc__ += deprecation_message.encode('utf8') 40 | return Wrapper 41 | return Decorator 42 | 43 | 44 | def CanUseDeprecated(func): 45 | """Ignores deprecation warnings emitted while the decorated function runs.""" 46 | 47 | @functools.wraps(func) 48 | def Wrapper(*args, **kwargs): 49 | with warnings.catch_warnings(): 50 | warnings.filterwarnings('ignore', category=DeprecationWarning) 51 | return func(*args, **kwargs) 52 | return Wrapper 53 | -------------------------------------------------------------------------------- /ee/deserializer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A deserializer that decodes EE object trees from JSON DAGs.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | # pylint: disable=g-bad-import-order 10 | import json 11 | import numbers 12 | import six 13 | 14 | from . import apifunction 15 | from . import computedobject 16 | from . import customfunction 17 | from . import ee_date 18 | from . import ee_exception 19 | from . import encodable 20 | from . import function 21 | from . import geometry 22 | 23 | 24 | def fromJSON(json_obj): 25 | """Deserialize an object from a JSON string appropriate for API calls. 26 | 27 | Args: 28 | json_obj: The JSON represenation of the input. 29 | 30 | Returns: 31 | The deserialized object. 32 | """ 33 | return decode(json.loads(json_obj)) 34 | 35 | 36 | def decode(json_obj): 37 | """Decodes an object previously encoded using the EE API v2 (DAG) format. 38 | 39 | Args: 40 | json_obj: The serialied object to decode. 41 | 42 | Returns: 43 | The decoded object. 44 | """ 45 | named_values = {} 46 | 47 | # Incrementally decode scope entries if there are any. 48 | if isinstance(json_obj, dict) and json_obj['type'] == 'CompoundValue': 49 | for i, (key, value) in enumerate(json_obj['scope']): 50 | if key in named_values: 51 | raise ee_exception.EEException( 52 | 'Duplicate scope key "%s" in scope #%d.' % (key, i)) 53 | named_values[key] = _decodeValue(value, named_values) 54 | json_obj = json_obj['value'] 55 | 56 | # Decode the final value. 57 | return _decodeValue(json_obj, named_values) 58 | 59 | 60 | def _decodeValue(json_obj, named_values): 61 | """Decodes an object previously encoded using the EE API v2 (DAG) format. 62 | 63 | This uses a provided scope for ValueRef lookup and does not not allow the 64 | input to be a CompoundValue. 65 | 66 | Args: 67 | json_obj: The serialied object to decode. 68 | named_values: The objects that can be referenced by ValueRefs. 69 | 70 | Returns: 71 | The decoded object. 72 | """ 73 | 74 | # Check for primitive values. 75 | if (json_obj is None or 76 | isinstance(json_obj, (bool, numbers.Number, six.string_types))): 77 | return json_obj 78 | 79 | # Check for array values. 80 | if isinstance(json_obj, (list, tuple)): 81 | return [_decodeValue(element, named_values) for element in json_obj] 82 | 83 | # Ensure that we've got a proper object at this point. 84 | if not isinstance(json_obj, dict): 85 | raise ee_exception.EEException('Cannot decode object: ' + json_obj) 86 | 87 | # Check for explicitly typed values. 88 | type_name = json_obj['type'] 89 | if type_name == 'ValueRef': 90 | if json_obj['value'] in named_values: 91 | return named_values[json_obj['value']] 92 | else: 93 | raise ee_exception.EEException('Unknown ValueRef: ' + json_obj) 94 | elif type_name == 'ArgumentRef': 95 | var_name = json_obj['value'] 96 | if not isinstance(var_name, six.string_types): 97 | raise ee_exception.EEException('Invalid variable name: ' + var_name) 98 | return customfunction.CustomFunction.variable(None, var_name) # pylint: disable=protected-access 99 | elif type_name == 'Date': 100 | microseconds = json_obj['value'] 101 | if not isinstance(microseconds, numbers.Number): 102 | raise ee_exception.EEException('Invalid date value: ' + microseconds) 103 | return ee_date.Date(microseconds / 1e3) 104 | elif type_name == 'Bytes': 105 | result = encodable.Encodable() 106 | result.encode = lambda encoder: json_obj 107 | return result 108 | elif type_name == 'Invocation': 109 | if 'functionName' in json_obj: 110 | func = apifunction.ApiFunction.lookup(json_obj['functionName']) 111 | else: 112 | func = _decodeValue(json_obj['function'], named_values) 113 | args = dict((key, _decodeValue(value, named_values)) 114 | for (key, value) in json_obj['arguments'].items()) 115 | if isinstance(func, function.Function): 116 | return func.apply(args) 117 | elif isinstance(func, computedobject.ComputedObject): 118 | # We have to allow ComputedObjects for cases where invocations 119 | # return a function, e.g. Image.parseExpression(). 120 | return computedobject.ComputedObject(func, args) 121 | else: 122 | raise ee_exception.EEException( 123 | 'Invalid function value: %s' % json_obj['function']) 124 | elif type_name == 'Dictionary': 125 | return dict((key, _decodeValue(value, named_values)) 126 | for (key, value) in json_obj['value'].items()) 127 | elif type_name == 'Function': 128 | body = _decodeValue(json_obj['body'], named_values) 129 | signature = { 130 | 'name': '', 131 | 'args': [{'name': arg_name, 'type': 'Object', 'optional': False} 132 | for arg_name in json_obj['argumentNames']], 133 | 'returns': 'Object' 134 | } 135 | return customfunction.CustomFunction(signature, lambda *args: body) 136 | elif type_name in ('Point', 'MultiPoint', 'LineString', 'MultiLineString', 137 | 'Polygon', 'MultiPolygon', 'LinearRing', 138 | 'GeometryCollection'): 139 | return geometry.Geometry(json_obj) 140 | elif type_name == 'CompoundValue': 141 | raise ee_exception.EEException('Nested CompoundValues are disallowed.') 142 | else: 143 | raise ee_exception.EEException('Unknown encoded object type: ' + type_name) 144 | -------------------------------------------------------------------------------- /ee/dictionary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A wrapper for dictionaries.""" 3 | 4 | 5 | 6 | from . import apifunction 7 | from . import computedobject 8 | 9 | # Using lowercase function naming to match the JavaScript names. 10 | # pylint: disable=g-bad-name 11 | 12 | 13 | class Dictionary(computedobject.ComputedObject): 14 | """An object to represent dictionaries.""" 15 | 16 | _initialized = False 17 | 18 | def __init__(self, arg=None): 19 | """Construct a dictionary. 20 | 21 | Args: 22 | arg: This constructor accepts the following args: 23 | 1) Another dictionary. 24 | 2) A list of key/value pairs. 25 | 3) A null or no argument (producing an empty dictionary) 26 | """ 27 | self.initialize() 28 | 29 | if isinstance(arg, dict): 30 | super(Dictionary, self).__init__(None, None) 31 | self._dictionary = arg 32 | else: 33 | self._dictionary = None 34 | if (isinstance(arg, computedobject.ComputedObject) 35 | and arg.func 36 | and arg.func.getSignature()['returns'] == 'Dictionary'): 37 | # If it's a call that's already returning a Dictionary, just cast. 38 | super(Dictionary, self).__init__(arg.func, arg.args, arg.varName) 39 | else: 40 | # Delegate everything else to the server-side constructor. 41 | super(Dictionary, self).__init__( 42 | apifunction.ApiFunction('Dictionary'), {'input': arg}) 43 | 44 | @classmethod 45 | def initialize(cls): 46 | """Imports API functions to this class.""" 47 | if not cls._initialized: 48 | apifunction.ApiFunction.importApi(cls, 'Dictionary', 'Dictionary') 49 | cls._initialized = True 50 | 51 | @classmethod 52 | def reset(cls): 53 | """Removes imported API functions from this class.""" 54 | apifunction.ApiFunction.clearApi(cls) 55 | cls._initialized = False 56 | 57 | @staticmethod 58 | def name(): 59 | return 'Dictionary' 60 | 61 | def encode(self, opt_encoder=None): 62 | if self._dictionary is not None: 63 | return opt_encoder(self._dictionary) 64 | else: 65 | return super(Dictionary, self).encode(opt_encoder) 66 | 67 | def encode_cloud_value(self, opt_encoder=None): 68 | if self._dictionary is not None: 69 | return {'valueReference': opt_encoder(self._dictionary)} 70 | else: 71 | return super(Dictionary, self).encode_cloud_value(opt_encoder) 72 | -------------------------------------------------------------------------------- /ee/ee_date.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A wrapper for dates.""" 3 | 4 | 5 | 6 | # pylint: disable=g-bad-import-order 7 | import datetime 8 | import math 9 | import six 10 | 11 | from . import apifunction 12 | from . import computedobject 13 | from . import ee_exception 14 | from . import ee_types as types 15 | from . import serializer 16 | 17 | # Using lowercase function naming to match the JavaScript names. 18 | # pylint: disable=g-bad-name 19 | 20 | 21 | class Date(computedobject.ComputedObject): 22 | """An object to represent dates.""" 23 | 24 | _initialized = False 25 | 26 | def __init__(self, date, opt_tz=None): 27 | """Construct a date. 28 | 29 | This sends all inputs (except another Date) through the Date function. 30 | 31 | This constructor accepts the following args: 32 | 1) A bare date. 33 | 2) An ISO string 34 | 3) A integer number of milliseconds since the epoch. 35 | 4) A ComputedObject. 36 | 37 | Args: 38 | date: The date to wrap. 39 | opt_tz: An optional timezone, only useable with a string date. 40 | """ 41 | self.initialize() 42 | 43 | func = apifunction.ApiFunction('Date') 44 | args = None 45 | varName = None 46 | if isinstance(date, datetime.datetime): 47 | args = {'value': 48 | math.floor(serializer.DatetimeToMicroseconds(date) / 1000)} 49 | elif types.isNumber(date): 50 | args = {'value': date} 51 | elif isinstance(date, six.string_types): 52 | args = {'value': date} 53 | if opt_tz: 54 | if isinstance(opt_tz, six.string_types): 55 | args['timeZone'] = opt_tz 56 | else: 57 | raise ee_exception.EEException( 58 | 'Invalid argument specified for ee.Date(..., opt_tz): %s' % date) 59 | elif isinstance(date, computedobject.ComputedObject): 60 | if date.func and date.func.getSignature()['returns'] == 'Date': 61 | # If it's a call that's already returning a Date, just cast. 62 | func = date.func 63 | args = date.args 64 | varName = date.varName 65 | else: 66 | args = {'value': date} 67 | else: 68 | raise ee_exception.EEException( 69 | 'Invalid argument specified for ee.Date(): %s' % date) 70 | 71 | super(Date, self).__init__(func, args, varName) 72 | 73 | @classmethod 74 | def initialize(cls): 75 | """Imports API functions to this class.""" 76 | if not cls._initialized: 77 | apifunction.ApiFunction.importApi(cls, 'Date', 'Date') 78 | cls._initialized = True 79 | 80 | @classmethod 81 | def reset(cls): 82 | """Removes imported API functions from this class.""" 83 | apifunction.ApiFunction.clearApi(cls) 84 | cls._initialized = False 85 | 86 | @staticmethod 87 | def name(): 88 | return 'Date' 89 | -------------------------------------------------------------------------------- /ee/ee_exception.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A simple exception for the EE library.""" 3 | 4 | 5 | 6 | 7 | class EEException(Exception): 8 | """A simple exception for the EE library.""" 9 | pass 10 | -------------------------------------------------------------------------------- /ee/ee_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A wrapper for lists.""" 3 | 4 | 5 | 6 | from . import apifunction 7 | from . import computedobject 8 | from . import ee_exception 9 | 10 | # Using lowercase function naming to match the JavaScript names. 11 | # pylint: disable=g-bad-name 12 | 13 | 14 | class List(computedobject.ComputedObject): 15 | """An object to represent lists.""" 16 | 17 | _initialized = False 18 | 19 | def __init__(self, arg): 20 | """Construct a list wrapper. 21 | 22 | This constructor accepts the following args: 23 | 1) A bare list. 24 | 2) A ComputedObject returning a list. 25 | 26 | Args: 27 | arg: The list to wrap. 28 | 29 | Raises: 30 | ee_exception.EEException: On bad input. 31 | """ 32 | self.initialize() 33 | 34 | if isinstance(arg, (list, tuple)): 35 | super(List, self).__init__(None, None) 36 | self._list = arg 37 | elif isinstance(arg, computedobject.ComputedObject): 38 | super(List, self).__init__(arg.func, arg.args, arg.varName) 39 | self._list = None 40 | else: 41 | raise ee_exception.EEException( 42 | 'Invalid argument specified for ee.List(): %s' % arg) 43 | 44 | @classmethod 45 | def initialize(cls): 46 | """Imports API functions to this class.""" 47 | if not cls._initialized: 48 | apifunction.ApiFunction.importApi(cls, 'List', 'List') 49 | cls._initialized = True 50 | 51 | @classmethod 52 | def reset(cls): 53 | """Removes imported API functions from this class.""" 54 | apifunction.ApiFunction.clearApi(cls) 55 | cls._initialized = False 56 | 57 | @staticmethod 58 | def name(): 59 | return 'List' 60 | 61 | def encode(self, opt_encoder=None): 62 | if isinstance(self._list, (list, tuple)): 63 | return [opt_encoder(elem) for elem in self._list] 64 | else: 65 | return super(List, self).encode(opt_encoder) 66 | 67 | def encode_cloud_value(self, opt_encoder=None): 68 | if isinstance(self._list, (list, tuple)): 69 | return {'valueReference': opt_encoder(self._list)} 70 | else: 71 | return super(List, self).encode_cloud_value(opt_encoder) 72 | -------------------------------------------------------------------------------- /ee/ee_number.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A wrapper for numbers.""" 3 | 4 | 5 | 6 | import numbers 7 | 8 | from . import _cloud_api_utils 9 | from . import apifunction 10 | from . import computedobject 11 | from . import ee_exception 12 | 13 | # Using lowercase function naming to match the JavaScript names. 14 | # pylint: disable=g-bad-name 15 | 16 | 17 | class Number(computedobject.ComputedObject): 18 | """An object to represent numbers.""" 19 | 20 | _initialized = False 21 | 22 | def __init__(self, number): 23 | """Construct a number wrapper. 24 | 25 | This constructor accepts the following args: 26 | 1) A bare number. 27 | 2) A ComputedObject returning a number. 28 | 29 | Args: 30 | number: The number to wrap. 31 | """ 32 | self.initialize() 33 | 34 | if isinstance(number, numbers.Number): 35 | super(Number, self).__init__(None, None) 36 | self._number = number 37 | elif isinstance(number, computedobject.ComputedObject): 38 | super(Number, self).__init__(number.func, number.args, number.varName) 39 | self._number = None 40 | else: 41 | raise ee_exception.EEException( 42 | 'Invalid argument specified for ee.Number(): %s' % number) 43 | 44 | @classmethod 45 | def initialize(cls): 46 | """Imports API functions to this class.""" 47 | if not cls._initialized: 48 | apifunction.ApiFunction.importApi(cls, 'Number', 'Number') 49 | cls._initialized = True 50 | 51 | @classmethod 52 | def reset(cls): 53 | """Removes imported API functions from this class.""" 54 | apifunction.ApiFunction.clearApi(cls) 55 | cls._initialized = False 56 | 57 | @staticmethod 58 | def name(): 59 | return 'Number' 60 | 61 | def encode(self, opt_encoder=None): 62 | if isinstance(self._number, numbers.Number): 63 | return self._number 64 | else: 65 | return super(Number, self).encode(opt_encoder) 66 | 67 | def encode_cloud_value(self, opt_encoder=None): 68 | if isinstance(self._number, numbers.Number): 69 | return _cloud_api_utils.encode_number_as_cloud_value(self._number) 70 | else: 71 | return super(Number, self).encode_cloud_value(opt_encoder) 72 | -------------------------------------------------------------------------------- /ee/ee_string.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A wrapper for strings.""" 3 | 4 | 5 | 6 | # pylint: disable=g-bad-import-order 7 | import six # For Python 2/3 compatibility 8 | 9 | from . import apifunction 10 | from . import computedobject 11 | from . import ee_exception 12 | 13 | # Using lowercase function naming to match the JavaScript names. 14 | # pylint: disable=g-bad-name 15 | 16 | 17 | class String(computedobject.ComputedObject): 18 | """An object to represent strings.""" 19 | 20 | _initialized = False 21 | 22 | def __init__(self, string): 23 | """Construct a string wrapper. 24 | 25 | This constructor accepts the following args: 26 | 1) A bare string. 27 | 2) A ComputedObject returning a string. 28 | 29 | Args: 30 | string: The string to wrap. 31 | """ 32 | self.initialize() 33 | 34 | if isinstance(string, six.string_types): 35 | super(String, self).__init__(None, None) 36 | elif isinstance(string, computedobject.ComputedObject): 37 | if string.func and string.func.getSignature()['returns'] == 'String': 38 | # If it's a call that's already returning a String, just cast. 39 | super(String, self).__init__(string.func, string.args, string.varName) 40 | else: 41 | super(String, self).__init__(apifunction.ApiFunction('String'), { 42 | 'input': string 43 | }) 44 | else: 45 | raise ee_exception.EEException( 46 | 'Invalid argument specified for ee.String(): %s' % string) 47 | self._string = string 48 | 49 | @classmethod 50 | def initialize(cls): 51 | """Imports API functions to this class.""" 52 | if not cls._initialized: 53 | apifunction.ApiFunction.importApi(cls, 'String', 'String') 54 | cls._initialized = True 55 | 56 | @classmethod 57 | def reset(cls): 58 | """Removes imported API functions from this class.""" 59 | apifunction.ApiFunction.clearApi(cls) 60 | cls._initialized = False 61 | 62 | @staticmethod 63 | def name(): 64 | return 'String' 65 | 66 | def encode(self, opt_encoder=None): 67 | if isinstance(self._string, six.string_types): 68 | return self._string 69 | else: 70 | return self._string.encode(opt_encoder) 71 | 72 | def encode_cloud_value(self, opt_encoder=None): 73 | if isinstance(self._string, six.string_types): 74 | return {'constantValue': self._string} 75 | else: 76 | return self._string.encode_cloud_value(opt_encoder) 77 | -------------------------------------------------------------------------------- /ee/ee_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A set of utilities to work with EE types.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | # pylint: disable=g-bad-import-order 10 | import datetime 11 | import numbers 12 | import six 13 | 14 | from . import computedobject 15 | 16 | 17 | # A dictionary of the classes in the ee module. Set by registerClasses. 18 | _registered_classes = {} 19 | 20 | 21 | def _registerClasses(classes): 22 | """Registers the known classes. 23 | 24 | Args: 25 | classes: A dictionary of the classes available in the ee module. 26 | """ 27 | global _registered_classes 28 | _registered_classes = classes 29 | 30 | 31 | def classToName(klass): 32 | """Converts a class to the API-friendly type name. 33 | 34 | Args: 35 | klass: The class. 36 | 37 | Returns: 38 | The name of the class, or "Object" if not recognized. 39 | """ 40 | if issubclass(klass, computedobject.ComputedObject): 41 | return klass.name() 42 | elif issubclass(klass, numbers.Number): 43 | return 'Number' 44 | elif issubclass(klass, six.string_types): 45 | return 'String' 46 | elif issubclass(klass, (list, tuple)): 47 | return 'Array' 48 | elif issubclass(klass, datetime.datetime): 49 | return 'Date' 50 | else: 51 | return 'Object' 52 | 53 | 54 | def nameToClass(name): 55 | """Converts a class name to a class. Returns None if not an ee class. 56 | 57 | Args: 58 | name: The class name. 59 | 60 | Returns: 61 | The named class. 62 | """ 63 | return _registered_classes.get(name) 64 | 65 | 66 | def isSubtype(firstType, secondType): 67 | """Checks whether a type is a subtype of another. 68 | 69 | Args: 70 | firstType: The first type name. 71 | secondType: The second type name. 72 | 73 | Returns: 74 | Whether secondType is a subtype of firstType. 75 | """ 76 | if secondType == firstType: 77 | return True 78 | 79 | if firstType == 'Element': 80 | return secondType in ('Element', 'Image', 'Feature', 81 | 'Collection', 'ImageCollection', 'FeatureCollection') 82 | elif firstType in ('FeatureCollection', 'Collection'): 83 | return secondType in ('Collection', 'ImageCollection', 'FeatureCollection') 84 | elif firstType == object: 85 | return True 86 | else: 87 | return False 88 | 89 | 90 | def isNumber(obj): 91 | """Returns true if this object is a number or number variable. 92 | 93 | Args: 94 | obj: The object to check. 95 | 96 | Returns: 97 | Whether the object is a number or number variable. 98 | """ 99 | return (isinstance(obj, numbers.Number) or 100 | (isinstance(obj, computedobject.ComputedObject) and 101 | obj.name() == 'Number')) 102 | 103 | 104 | def isString(obj): 105 | """Returns true if this object is a string or string variable. 106 | 107 | Args: 108 | obj: The object to check. 109 | 110 | Returns: 111 | Whether the object is a string or string variable. 112 | """ 113 | return (isinstance(obj, six.string_types) or 114 | (isinstance(obj, computedobject.ComputedObject) and 115 | obj.name() == 'String')) 116 | 117 | 118 | def isArray(obj): 119 | """Returns true if this object is an array or array variable. 120 | 121 | Args: 122 | obj: The object to check. 123 | 124 | Returns: 125 | Whether the object is an array or array variable. 126 | """ 127 | return (isinstance(obj, (list, tuple)) or 128 | (isinstance(obj, computedobject.ComputedObject) and 129 | obj.name() == 'List')) 130 | -------------------------------------------------------------------------------- /ee/element.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Base class for Image, Feature and Collection. 3 | 4 | This class is never intended to be instantiated by the user. 5 | """ 6 | 7 | 8 | 9 | # Using lowercase function naming to match the JavaScript names. 10 | # pylint: disable=g-bad-name 11 | 12 | from . import apifunction 13 | from . import computedobject 14 | from . import ee_exception 15 | 16 | 17 | class Element(computedobject.ComputedObject): 18 | """Base class for ImageCollection and FeatureCollection.""" 19 | 20 | _initialized = False 21 | 22 | def __init__(self, func, args, opt_varName=None): 23 | """Constructs a collection by initializing its ComputedObject.""" 24 | super(Element, self).__init__(func, args, opt_varName) 25 | 26 | @classmethod 27 | def initialize(cls): 28 | """Imports API functions to this class.""" 29 | if not cls._initialized: 30 | apifunction.ApiFunction.importApi(cls, 'Element', 'Element') 31 | cls._initialized = True 32 | 33 | @classmethod 34 | def reset(cls): 35 | """Removes imported API functions from this class.""" 36 | apifunction.ApiFunction.clearApi(cls) 37 | cls._initialized = False 38 | 39 | @staticmethod 40 | def name(): 41 | return 'Element' 42 | 43 | def set(self, *args): 44 | """Overrides one or more metadata properties of an Element. 45 | 46 | Args: 47 | *args: Either a dictionary of properties, or a vararg sequence of 48 | properties, e.g. key1, value1, key2, value2, ... 49 | 50 | Returns: 51 | The element with the specified properties overridden. 52 | """ 53 | if len(args) == 1: 54 | properties = args[0] 55 | 56 | # If this is a keyword call, unwrap it. 57 | if (isinstance(properties, dict) and 58 | (len(properties) == 1 and 'properties' in properties) and 59 | isinstance(properties['properties'], 60 | (dict, computedobject.ComputedObject))): 61 | # Looks like a call with keyword parameters. Extract them. 62 | properties = properties['properties'] 63 | 64 | if isinstance(properties, dict): 65 | # Still a plain object. Extract its keys. Setting the keys separately 66 | # allows filter propagation. 67 | result = self 68 | for key, value in properties.items(): 69 | result = apifunction.ApiFunction.call_( 70 | 'Element.set', result, key, value) 71 | elif (isinstance(properties, computedobject.ComputedObject) and 72 | apifunction.ApiFunction.lookupInternal('Element.setMulti')): 73 | # A computed dictionary. Can't set each key separately. 74 | result = apifunction.ApiFunction.call_( 75 | 'Element.setMulti', self, properties) 76 | else: 77 | raise ee_exception.EEException( 78 | 'When Element.set() is passed one argument, ' 79 | 'it must be a dictionary.') 80 | else: 81 | # Interpret as key1, value1, key2, value2, ... 82 | if len(args) % 2 != 0: 83 | raise ee_exception.EEException( 84 | 'When Element.set() is passed multiple arguments, there ' 85 | 'must be an even number of them.') 86 | result = self 87 | for i in range(0, len(args), 2): 88 | key = args[i] 89 | value = args[i + 1] 90 | result = apifunction.ApiFunction.call_( 91 | 'Element.set', result, key, value) 92 | 93 | # Manually cast the result to an image. 94 | return self._cast(result) 95 | -------------------------------------------------------------------------------- /ee/encodable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Interfaces implemented by serializable objects.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable-msg=g-bad-name 8 | 9 | 10 | class Encodable(object): 11 | """An interface implemented by objects that can serialize themselves.""" 12 | 13 | def encode(self, encoder): 14 | """Encodes the object in a format compatible with Serializer. 15 | 16 | Args: 17 | encoder: A function that can be called to encode the components of 18 | an object. 19 | 20 | Returns: 21 | The encoded form of the object. 22 | """ 23 | raise NotImplementedError('Encodable classes must implement encode().') 24 | 25 | def encode_cloud_value(self, encoder): 26 | """Encodes the object as a ValueNode. 27 | 28 | Args: 29 | encoder: A function that can be called to encode the components of 30 | an object. 31 | 32 | Returns: 33 | The encoded form of the object. 34 | """ 35 | raise NotImplementedError( 36 | 'Encodable classes must implement encode_cloud_value().') 37 | 38 | 39 | class EncodableFunction(object): 40 | """An interface implemented by functions that can serialize themselves.""" 41 | 42 | def encode_invocation(self, encoder): 43 | """Encodes the function in a format compatible with Serializer. 44 | 45 | Args: 46 | encoder: A function that can be called to encode the components of 47 | an object. 48 | 49 | Returns: 50 | The encoded form of the function. 51 | """ 52 | raise NotImplementedError( 53 | 'EncodableFunction classes must implement encode_invocation().') 54 | 55 | def encode_cloud_invocation(self, encoder): 56 | """Encodes the function as a FunctionInvocation. 57 | 58 | Args: 59 | encoder: A function that can be called to encode the components of 60 | an object. Returns a reference to the encoded value. 61 | 62 | Returns: 63 | The encoded form of the function. 64 | """ 65 | raise NotImplementedError( 66 | 'EncodableFunction classes must implement encode_cloud_invocation().') 67 | -------------------------------------------------------------------------------- /ee/feature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """An object representing EE Features.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | from . import apifunction 10 | from . import computedobject 11 | from . import ee_exception 12 | from . import element 13 | from . import geometry 14 | 15 | 16 | class Feature(element.Element): 17 | """An object representing EE Features.""" 18 | 19 | _initialized = False 20 | 21 | def __init__(self, geom, opt_properties=None): 22 | """Creates a feature a geometry or computed object. 23 | 24 | Features can be constructed from one of the following arguments plus an 25 | optional dictionary of properties: 26 | 1) An ee.Geometry. 27 | 2) A GeoJSON Geometry. 28 | 3) A GeoJSON Feature. 29 | 4) A computed object - reinterpreted as a geometry if properties 30 | are specified, and as a feature if they aren't. 31 | 32 | Args: 33 | geom: A geometry or feature. 34 | opt_properties: A dictionary of metadata properties. If the first 35 | parameter is a Feature (instead of a geometry), this is unused. 36 | 37 | Raises: 38 | EEException: if the given geometry isn't valid. 39 | """ 40 | if isinstance(geom, Feature): 41 | if opt_properties is not None: 42 | raise ee_exception.EEException( 43 | 'Can\'t create Feature out of a Feature and properties.') 44 | # A pre-constructed Feature. Copy. 45 | super(Feature, self).__init__(geom.func, geom.args) 46 | return 47 | 48 | self.initialize() 49 | 50 | feature_constructor = apifunction.ApiFunction.lookup('Feature') 51 | if geom is None or isinstance(geom, geometry.Geometry): 52 | # A geometry object. 53 | super(Feature, self).__init__(feature_constructor, { 54 | 'geometry': geom, 55 | 'metadata': opt_properties or None 56 | }) 57 | elif isinstance(geom, computedobject.ComputedObject): 58 | # A custom object to reinterpret as a Feature. 59 | super(Feature, self).__init__(geom.func, geom.args, geom.varName) 60 | elif isinstance(geom, dict) and geom.get('type') == 'Feature': 61 | properties = geom.get('properties', {}) 62 | if 'id' in geom: 63 | if 'system:index' in properties: 64 | raise ee_exception.EEException( 65 | 'Can\'t specify both "id" and "system:index".') 66 | properties = properties.copy() 67 | properties['system:index'] = geom['id'] 68 | # Try to convert a GeoJSON Feature. 69 | super(Feature, self).__init__(feature_constructor, { 70 | 'geometry': geometry.Geometry(geom.get('geometry', None)), 71 | 'metadata': properties 72 | }) 73 | else: 74 | # Try to convert the geometry arg to a Geometry, in the hopes of it 75 | # turning out to be GeoJSON. 76 | super(Feature, self).__init__(feature_constructor, { 77 | 'geometry': geometry.Geometry(geom), 78 | 'metadata': opt_properties or None 79 | }) 80 | 81 | @classmethod 82 | def initialize(cls): 83 | """Imports API functions to this class.""" 84 | if not cls._initialized: 85 | apifunction.ApiFunction.importApi(cls, 'Feature', 'Feature') 86 | cls._initialized = True 87 | 88 | @classmethod 89 | def reset(cls): 90 | """Removes imported API functions from this class.""" 91 | apifunction.ApiFunction.clearApi(cls) 92 | cls._initialized = False 93 | 94 | def getMapId(self, vis_params=None): 95 | """Fetch and return a map id and token, suitable for use in a Map overlay. 96 | 97 | Args: 98 | vis_params: The visualization parameters. Currently only one parameter, 99 | 'color', containing a hex RGB color string is allowed. 100 | 101 | Returns: 102 | An object containing a mapid string, an access token, plus a 103 | Collection.draw image wrapping a FeatureCollection containing 104 | this feature. 105 | """ 106 | # Create a collection containing this one feature and render it. 107 | collection = apifunction.ApiFunction.call_('Collection', [self]) 108 | return collection.getMapId(vis_params) 109 | 110 | @staticmethod 111 | def name(): 112 | return 'Feature' 113 | -------------------------------------------------------------------------------- /ee/featurecollection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Representation of an Earth Engine FeatureCollection.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | # pylint: disable=g-long-lambda 9 | 10 | from . import apifunction 11 | from . import collection 12 | from . import computedobject 13 | from . import data 14 | from . import deprecation 15 | from . import ee_exception 16 | from . import ee_list 17 | from . import ee_types 18 | from . import feature 19 | from . import geometry 20 | 21 | 22 | class FeatureCollection(collection.Collection): 23 | """A representation of a FeatureCollection.""" 24 | 25 | _initialized = False 26 | 27 | def __init__(self, args, opt_column=None): 28 | """Constructs a collection features. 29 | 30 | Args: 31 | args: constructor argument. One of: 32 | 1) A string - assumed to be the name of a collection. 33 | 2) A geometry. 34 | 3) A feature. 35 | 4) An array of features. 36 | 5) A computed object - reinterpreted as a collection. 37 | opt_column: The name of the geometry column to use. Only useful with the 38 | string constructor. 39 | 40 | Raises: 41 | EEException: if passed something other than the above. 42 | """ 43 | self.initialize() 44 | 45 | # Wrap geometries with features. 46 | if isinstance(args, geometry.Geometry): 47 | args = feature.Feature(args) 48 | 49 | # Wrap single features in an array. 50 | if isinstance(args, feature.Feature): 51 | args = [args] 52 | 53 | if ee_types.isString(args): 54 | # An ID. 55 | actual_args = {'tableId': args} 56 | if opt_column: 57 | actual_args['geometryColumn'] = opt_column 58 | super(FeatureCollection, self).__init__( 59 | apifunction.ApiFunction.lookup('Collection.loadTable'), actual_args) 60 | elif isinstance(args, (list, tuple)): 61 | # A list of features. 62 | super(FeatureCollection, self).__init__( 63 | apifunction.ApiFunction.lookup('Collection'), { 64 | 'features': [feature.Feature(i) for i in args] 65 | }) 66 | elif isinstance(args, ee_list.List): 67 | # A computed list of features. 68 | super(FeatureCollection, self).__init__( 69 | apifunction.ApiFunction.lookup('Collection'), { 70 | 'features': args 71 | }) 72 | elif isinstance(args, computedobject.ComputedObject): 73 | # A custom object to reinterpret as a FeatureCollection. 74 | super(FeatureCollection, self).__init__( 75 | args.func, args.args, args.varName) 76 | else: 77 | raise ee_exception.EEException( 78 | 'Unrecognized argument type to convert to a FeatureCollection: %s' % 79 | args) 80 | 81 | @classmethod 82 | def initialize(cls): 83 | """Imports API functions to this class.""" 84 | if not cls._initialized: 85 | super(FeatureCollection, cls).initialize() 86 | apifunction.ApiFunction.importApi( 87 | cls, 'FeatureCollection', 'FeatureCollection') 88 | cls._initialized = True 89 | 90 | @classmethod 91 | def reset(cls): 92 | """Removes imported API functions from this class.""" 93 | apifunction.ApiFunction.clearApi(cls) 94 | cls._initialized = False 95 | 96 | def getMapId(self, vis_params=None): 97 | """Fetch and return a map id and token, suitable for use in a Map overlay. 98 | 99 | Args: 100 | vis_params: The visualization parameters. Currently only one parameter, 101 | 'color', containing a hex RGB color string is allowed. 102 | 103 | Returns: 104 | An object containing a mapid string, an access token, plus a 105 | Collection.draw image wrapping this collection. 106 | """ 107 | painted = apifunction.ApiFunction.apply_('Collection.draw', { 108 | 'collection': self, 109 | 'color': (vis_params or {}).get('color', '000000') 110 | }) 111 | return painted.getMapId({}) 112 | 113 | def getDownloadURL(self, filetype=None, selectors=None, filename=None): 114 | """Get a download URL for this feature collection. 115 | 116 | Args: 117 | filetype: The filetype of download, either CSV or JSON. Defaults to CSV. 118 | selectors: The selectors that should be used to determine which attributes 119 | will be downloaded. 120 | filename: The name of the file to be downloaded. 121 | 122 | Returns: 123 | A URL to download the specified feature collection. 124 | """ 125 | request = {} 126 | request['table'] = self.serialize() 127 | if filetype is not None: 128 | request['format'] = filetype.upper() 129 | if filename is not None: 130 | request['filename'] = filename 131 | if selectors is not None: 132 | if isinstance(selectors, (list, tuple)): 133 | selectors = ','.join(selectors) 134 | request['selectors'] = selectors 135 | return data.makeTableDownloadUrl(data.getTableDownloadId(request)) 136 | 137 | # Deprecated spelling to match the JS library. 138 | getDownloadUrl = deprecation.Deprecated('Use getDownloadURL().')( 139 | getDownloadURL) 140 | 141 | def select(self, propertySelectors, newProperties=None, 142 | retainGeometry=True, *args): 143 | """Select properties from each feature in a collection. 144 | 145 | Args: 146 | propertySelectors: An array of names or regexes specifying the properties 147 | to select. 148 | newProperties: An array of strings specifying the new names for the 149 | selected properties. If supplied, the length must match the number 150 | of properties selected. 151 | retainGeometry: A boolean. When false, the result will have no geometry. 152 | *args: Selector elements as varargs. 153 | 154 | Returns: 155 | The feature collection with selected properties. 156 | """ 157 | if len(args) or ee_types.isString(propertySelectors): 158 | args = list(args) 159 | if not isinstance(retainGeometry, bool): 160 | args.insert(0, retainGeometry) 161 | if newProperties is not None: 162 | args.insert(0, newProperties) 163 | args.insert(0, propertySelectors) 164 | return self.map(lambda feat: feat.select(args, None, True)) 165 | else: 166 | return self.map( 167 | lambda feat: feat.select( 168 | propertySelectors, newProperties, retainGeometry)) 169 | 170 | @staticmethod 171 | def name(): 172 | return 'FeatureCollection' 173 | 174 | @staticmethod 175 | def elementType(): 176 | return feature.Feature 177 | -------------------------------------------------------------------------------- /ee/filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Collection filters. 3 | 4 | Example usage: 5 | Filter('time', low, high) 6 | .bounds(ring) 7 | .eq('time', value) 8 | .lt('time', value) 9 | """ 10 | 11 | 12 | 13 | # Using lowercase function naming to match the JavaScript names. 14 | # pylint: disable=g-bad-name 15 | 16 | from . import apifunction 17 | from . import computedobject 18 | from . import ee_exception 19 | 20 | 21 | # A map from the deprecated old-style comparison operator names to API 22 | # function names, implicitly prefixed with "Filter.". Negative operators 23 | # (those starting with "not_") are not included. 24 | _FUNCTION_NAMES = { 25 | 'equals': 'equals', 26 | 'less_than': 'lessThan', 27 | 'greater_than': 'greaterThan', 28 | 'contains': 'stringContains', 29 | 'starts_with': 'stringStartsWith', 30 | 'ends_with': 'stringEndsWith', 31 | } 32 | 33 | 34 | class Filter(computedobject.ComputedObject): 35 | """An object to represent collection filters.""" 36 | 37 | _initialized = False 38 | 39 | def __init__(self, opt_filter=None): 40 | """Construct a filter. 41 | 42 | This constructor accepts the following args: 43 | 1) Another filter. 44 | 2) An array of filters (which are implicitly ANDed together). 45 | 3) A ComputedObject returning a filter. Users shouldn't be making these; 46 | they're produced by the generator functions below. 47 | 48 | Args: 49 | opt_filter: Optional filter to add. 50 | """ 51 | self.initialize() 52 | 53 | if isinstance(opt_filter, (list, tuple)): 54 | if not opt_filter: 55 | raise ee_exception.EEException('Empty list specified for ee.Filter().') 56 | elif len(opt_filter) == 1: 57 | opt_filter = opt_filter[0] 58 | else: 59 | self._filter = tuple(opt_filter) 60 | super(Filter, self).__init__( 61 | apifunction.ApiFunction.lookup('Filter.and'), 62 | {'filters': self._filter}) 63 | return 64 | 65 | if isinstance(opt_filter, computedobject.ComputedObject): 66 | super(Filter, self).__init__( 67 | opt_filter.func, opt_filter.args, opt_filter.varName) 68 | self._filter = (opt_filter,) 69 | elif opt_filter is None: 70 | # A silly call with no arguments left for backward-compatibility. 71 | # Encoding such a filter is expected to fail, but it can be composed 72 | # by calling the various methods that end up in _append(). 73 | super(Filter, self).__init__(None, None) 74 | self._filter = () 75 | else: 76 | raise ee_exception.EEException( 77 | 'Invalid argument specified for ee.Filter(): %s' % opt_filter) 78 | 79 | @classmethod 80 | def initialize(cls): 81 | """Imports API functions to this class.""" 82 | if not cls._initialized: 83 | apifunction.ApiFunction.importApi(cls, 'Filter', 'Filter') 84 | cls._initialized = True 85 | 86 | @classmethod 87 | def reset(cls): 88 | """Removes imported API functions from this class.""" 89 | apifunction.ApiFunction.clearApi(cls) 90 | cls._initialized = False 91 | 92 | def predicateCount(self): 93 | """Return the number of predicates that have been added to this filter. 94 | 95 | Returns: 96 | The number of predicates that have been added to this filter. 97 | This does not count nested predicates. 98 | """ 99 | return len(self._filter) 100 | 101 | def _append(self, new_filter): 102 | """Append a predicate to this filter. 103 | 104 | These are implicitly ANDed. 105 | 106 | Args: 107 | new_filter: The filter to append to this one. Possible types are: 108 | 1) another fully constructed Filter, 109 | 2) a JSON representation of a filter, 110 | 3) an array of 1 or 2. 111 | 112 | Returns: 113 | A new filter that is the combination of both. 114 | """ 115 | if new_filter is not None: 116 | prev = list(self._filter) 117 | if isinstance(new_filter, Filter): 118 | prev.extend(new_filter._filter) # pylint: disable=protected-access 119 | elif isinstance(new_filter, list): 120 | prev.extend(new_filter) 121 | else: 122 | prev.append(new_filter) 123 | return Filter(prev) 124 | 125 | def Not(self): 126 | """Returns the opposite of this filter. 127 | 128 | Returns: 129 | The negated filter, which will match iff this filter doesn't. 130 | """ 131 | return apifunction.ApiFunction.call_('Filter.not', self) 132 | 133 | @staticmethod 134 | def metadata_(name, operator, value): 135 | """Filter on metadata. This is deprecated. 136 | 137 | Args: 138 | name: The property name to filter on. 139 | operator: The type of comparison. One of: 140 | "equals", "less_than", "greater_than", "contains", "begins_with", 141 | "ends_with", or any of these prefixed with "not_". 142 | value: The value to compare against. 143 | 144 | Returns: 145 | The new filter. 146 | 147 | Deprecated. Use ee.Filter.eq(), ee.Filter.gte(), etc.' 148 | """ 149 | operator = operator.lower() 150 | 151 | # Check for negated filters. 152 | negated = False 153 | if operator.startswith('not_'): 154 | negated = True 155 | operator = operator[4:] 156 | 157 | # Convert the operator to a function. 158 | if operator not in _FUNCTION_NAMES: 159 | raise ee_exception.EEException( 160 | 'Unknown filtering operator: %s' % operator) 161 | func_name = 'Filter.' + _FUNCTION_NAMES[operator] 162 | new_filter = apifunction.ApiFunction.call_(func_name, name, value) 163 | 164 | return new_filter.Not() if negated else new_filter 165 | 166 | @staticmethod 167 | def eq(name, value): 168 | """Filter to metadata equal to the given value.""" 169 | return apifunction.ApiFunction.call_('Filter.equals', name, value) 170 | 171 | @staticmethod 172 | def neq(name, value): 173 | """Filter to metadata not equal to the given value.""" 174 | return Filter.eq(name, value).Not() 175 | 176 | @staticmethod 177 | def lt(name, value): 178 | """Filter to metadata less than the given value.""" 179 | return apifunction.ApiFunction.call_('Filter.lessThan', name, value) 180 | 181 | @staticmethod 182 | def gte(name, value): 183 | """Filter on metadata greater than or equal to the given value.""" 184 | return Filter.lt(name, value).Not() 185 | 186 | @staticmethod 187 | def gt(name, value): 188 | """Filter on metadata greater than the given value.""" 189 | return apifunction.ApiFunction.call_('Filter.greaterThan', name, value) 190 | 191 | @staticmethod 192 | def lte(name, value): 193 | """Filter on metadata less than or equal to the given value.""" 194 | return Filter.gt(name, value).Not() 195 | 196 | @staticmethod 197 | def And(*args): 198 | """Combine two or more filters using boolean AND.""" 199 | if len(args) == 1 and isinstance(args[0], (list, tuple)): 200 | args = args[0] 201 | return apifunction.ApiFunction.call_('Filter.and', args) 202 | 203 | @staticmethod 204 | def Or(*args): 205 | """Combine two or more filters using boolean OR.""" 206 | if len(args) == 1 and isinstance(args[0], (list, tuple)): 207 | args = args[0] 208 | return apifunction.ApiFunction.call_('Filter.or', args) 209 | 210 | @staticmethod 211 | def date(start, opt_end=None): 212 | """Filter images by date. 213 | 214 | The start and end may be a Date, numbers (interpreted as milliseconds since 215 | 1970-01-01T00:00:00Z), or strings (such as '1996-01-01T08:00'). 216 | 217 | Args: 218 | start: The inclusive start date. 219 | opt_end: The optional exclusive end date, If not specified, a 220 | 1-millisecond range starting at 'start' is created. 221 | 222 | Returns: 223 | The modified filter. 224 | """ 225 | date_range = apifunction.ApiFunction.call_('DateRange', start, opt_end) 226 | return apifunction.ApiFunction.apply_('Filter.dateRangeContains', { 227 | 'leftValue': date_range, 228 | 'rightField': 'system:time_start' 229 | }) 230 | 231 | @staticmethod 232 | def inList(opt_leftField=None, 233 | opt_rightValue=None, 234 | opt_rightField=None, 235 | opt_leftValue=None): 236 | """Filter on metadata contained in a list. 237 | 238 | Args: 239 | opt_leftField: A selector for the left operand. 240 | Should not be specified if leftValue is specified. 241 | opt_rightValue: The value of the right operand. 242 | Should not be specified if rightField is specified. 243 | opt_rightField: A selector for the right operand. 244 | Should not be specified if rightValue is specified. 245 | opt_leftValue: The value of the left operand. 246 | Should not be specified if leftField is specified. 247 | 248 | Returns: 249 | The constructed filter. 250 | """ 251 | # Implement this in terms of listContains, with the arguments switched. 252 | # In listContains the list is on the left side, while in inList it's on 253 | # the right. 254 | return apifunction.ApiFunction.apply_('Filter.listContains', { 255 | 'leftField': opt_rightField, 256 | 'rightValue': opt_leftValue, 257 | 'rightField': opt_leftField, 258 | 'leftValue': opt_rightValue 259 | }) 260 | 261 | @staticmethod 262 | def geometry(geometry, opt_errorMargin=None): 263 | """Filter on bounds. 264 | 265 | Items in the collection with a footprint that fails to intersect 266 | the bounds will be excluded when the collection is evaluated. 267 | 268 | Args: 269 | geometry: The geometry to filter to either as a GeoJSON geometry, 270 | or a FeatureCollection, from which a geometry will be extracted. 271 | opt_errorMargin: An optional error margin. If a number, interpreted as 272 | sphere surface meters. 273 | 274 | Returns: 275 | The modified filter. 276 | """ 277 | # Invoke geometry promotion then manually promote to a Feature. 278 | args = { 279 | 'leftField': '.all', 280 | 'rightValue': apifunction.ApiFunction.call_('Feature', geometry) 281 | } 282 | if opt_errorMargin is not None: 283 | args['maxError'] = opt_errorMargin 284 | return apifunction.ApiFunction.apply_('Filter.intersects', args) 285 | 286 | @staticmethod 287 | def name(): 288 | return 'Filter' 289 | -------------------------------------------------------------------------------- /ee/function.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A base class for EE Functions.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | import textwrap 10 | 11 | from . import computedobject 12 | from . import ee_exception 13 | from . import encodable 14 | from . import serializer 15 | 16 | 17 | class Function(encodable.EncodableFunction): 18 | """An abstract base class for functions callable by the EE API. 19 | 20 | Subclasses must implement encode_invocation() and getSignature(). 21 | """ 22 | 23 | # A function used to type-coerce arguments and return values. 24 | _promoter = staticmethod(lambda value, type_name: value) 25 | 26 | @staticmethod 27 | def _registerPromoter(promoter): 28 | """Registers a function used to type-coerce arguments and return values. 29 | 30 | Args: 31 | promoter: A function used to type-coerce arguments and return values. 32 | Passed a value as the first parameter and a type name as the second. 33 | Can be used, for example, promote numbers or strings to Images. 34 | Should return the input promoted if the type is recognized, 35 | otherwise the original input. 36 | """ 37 | Function._promoter = staticmethod(promoter) 38 | 39 | def getSignature(self): 40 | """Returns a description of the interface provided by this function. 41 | 42 | Returns: 43 | The function's signature, a dictionary containing: 44 | name: string 45 | returns: type name string 46 | args: list of argument dictionaries, each containing: 47 | name: string 48 | type: type name string 49 | optional: boolean 50 | default: an arbitrary primitive or encodable object 51 | """ 52 | raise NotImplementedError( 53 | 'Function subclasses must implement getSignature().') 54 | 55 | def call(self, *args, **kwargs): 56 | """Calls the function with the given positional and keyword arguments. 57 | 58 | Args: 59 | *args: The positional arguments to pass to the function. 60 | **kwargs: The named arguments to pass to the function. 61 | 62 | Returns: 63 | A ComputedObject representing the called function. If the signature 64 | specifies a recognized return type, the returned value will be cast 65 | to that type. 66 | """ 67 | return self.apply(self.nameArgs(args, kwargs)) 68 | 69 | def apply(self, named_args): 70 | """Calls the function with a dictionary of named arguments. 71 | 72 | Args: 73 | named_args: A dictionary of named arguments to pass to the function. 74 | 75 | Returns: 76 | A ComputedObject representing the called function. If the signature 77 | specifies a recognized return type, the returned value will be cast 78 | to that type. 79 | """ 80 | result = computedobject.ComputedObject(self, self.promoteArgs(named_args)) 81 | return Function._promoter(result, self.getReturnType()) 82 | 83 | def promoteArgs(self, args): 84 | """Promotes arguments to their types based on the function's signature. 85 | 86 | Verifies that all required arguments are provided and no unknown arguments 87 | are present. 88 | 89 | Args: 90 | args: A dictionary of keyword arguments to the function. 91 | 92 | Returns: 93 | A dictionary of promoted arguments. 94 | 95 | Raises: 96 | EEException: If unrecognized arguments are passed or required ones are 97 | missing. 98 | """ 99 | specs = self.getSignature()['args'] 100 | 101 | # Promote all recognized args. 102 | promoted_args = {} 103 | known = set() 104 | for spec in specs: 105 | name = spec['name'] 106 | if name in args: 107 | promoted_args[name] = Function._promoter(args[name], spec['type']) 108 | elif not spec.get('optional'): 109 | raise ee_exception.EEException( 110 | 'Required argument (%s) missing to function: %s' % (name, self)) 111 | known.add(name) 112 | 113 | # Check for unknown arguments. 114 | unknown = set(args.keys()).difference(known) 115 | if unknown: 116 | raise ee_exception.EEException( 117 | 'Unrecognized arguments %s to function: %s' % (unknown, self)) 118 | 119 | return promoted_args 120 | 121 | def nameArgs(self, args, extra_keyword_args=None): 122 | """Converts a list of positional arguments to a map of keyword arguments. 123 | 124 | Uses the function's signature for argument names. Note that this does not 125 | check whether the array contains enough arguments to satisfy the call. 126 | 127 | Args: 128 | args: Positional arguments to the function. 129 | extra_keyword_args: Optional named arguments to add. 130 | 131 | Returns: 132 | Keyword arguments to the function. 133 | 134 | Raises: 135 | EEException: If conflicting arguments or too many of them are supplied. 136 | """ 137 | specs = self.getSignature()['args'] 138 | 139 | # Handle positional arguments. 140 | if len(specs) < len(args): 141 | raise ee_exception.EEException( 142 | 'Too many (%d) arguments to function: %s' % (len(args), self)) 143 | named_args = dict([(spec['name'], value) 144 | for spec, value in zip(specs, args)]) 145 | 146 | # Handle keyword arguments. 147 | if extra_keyword_args: 148 | for name in extra_keyword_args: 149 | if name in named_args: 150 | raise ee_exception.EEException( 151 | 'Argument %s specified as both positional and ' 152 | 'keyword to function: %s' % (name, self)) 153 | named_args[name] = extra_keyword_args[name] 154 | # Unrecognized arguments are checked in promoteArgs(). 155 | 156 | return named_args 157 | 158 | def getReturnType(self): 159 | return self.getSignature()['returns'] 160 | 161 | def serialize(self, for_cloud_api=False): 162 | return serializer.toJSON( 163 | self, for_cloud_api=for_cloud_api 164 | ) 165 | 166 | def __str__(self): 167 | """Returns a user-readable docstring for this function.""" 168 | DOCSTRING_WIDTH = 75 169 | signature = self.getSignature() 170 | parts = [] 171 | if 'description' in signature: 172 | parts.append( 173 | textwrap.fill(signature['description'], width=DOCSTRING_WIDTH)) 174 | args = signature['args'] 175 | if args: 176 | parts.append('') 177 | parts.append('Args:') 178 | for arg in args: 179 | name_part = ' ' + arg['name'] 180 | if 'description' in arg: 181 | name_part += ': ' 182 | arg_header = name_part + arg['description'] 183 | else: 184 | arg_header = name_part 185 | arg_doc = textwrap.fill(arg_header, 186 | width=DOCSTRING_WIDTH - len(name_part), 187 | subsequent_indent=' ' * 6) 188 | parts.append(arg_doc) 189 | return '\n'.join(parts) 190 | -------------------------------------------------------------------------------- /ee/imagecollection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Representation for an Earth Engine ImageCollection.""" 3 | 4 | 5 | 6 | # Using lowercase function naming to match the JavaScript names. 7 | # pylint: disable=g-bad-name 8 | 9 | from . import apifunction 10 | from . import collection 11 | from . import computedobject 12 | from . import data 13 | from . import ee_exception 14 | from . import ee_list 15 | from . import ee_types 16 | from . import image 17 | 18 | 19 | class ImageCollection(collection.Collection): 20 | """Representation for an Earth Engine ImageCollection.""" 21 | 22 | _initialized = False 23 | 24 | def __init__(self, args): 25 | """ImageCollection constructor. 26 | 27 | Args: 28 | args: ImageCollections can be constructed from the following arguments: 29 | 1) A string: assumed to be the name of a collection, 30 | 2) An array of images, or anything that can be used to construct an 31 | image. 32 | 3) A single image. 33 | 5) A computed object - reinterpreted as a collection. 34 | 35 | Raises: 36 | EEException: if passed something other than the above. 37 | """ 38 | self.initialize() 39 | 40 | # Wrap single images in an array. 41 | if isinstance(args, image.Image): 42 | args = [args] 43 | 44 | if ee_types.isString(args): 45 | # An ID. 46 | super(ImageCollection, self).__init__( 47 | apifunction.ApiFunction.lookup('ImageCollection.load'), {'id': args}) 48 | elif isinstance(args, (list, tuple)): 49 | # A list of images. 50 | super(ImageCollection, self).__init__( 51 | apifunction.ApiFunction.lookup('ImageCollection.fromImages'), { 52 | 'images': [image.Image(i) for i in args] 53 | }) 54 | elif isinstance(args, ee_list.List): 55 | # A computed list of images. 56 | super(ImageCollection, self).__init__( 57 | apifunction.ApiFunction.lookup('ImageCollection.fromImages'), { 58 | 'images': args 59 | }) 60 | elif isinstance(args, computedobject.ComputedObject): 61 | # A custom object to reinterpret as a ImageCollection. 62 | super(ImageCollection, self).__init__(args.func, args.args, args.varName) 63 | else: 64 | raise ee_exception.EEException( 65 | 'Unrecognized argument type to convert to a ImageCollection: %s' % 66 | args) 67 | 68 | @classmethod 69 | def initialize(cls): 70 | """Imports API functions to this class.""" 71 | if not cls._initialized: 72 | super(ImageCollection, cls).initialize() 73 | apifunction.ApiFunction.importApi( 74 | cls, 'ImageCollection', 'ImageCollection') 75 | apifunction.ApiFunction.importApi( 76 | cls, 'reduce', 'ImageCollection') 77 | cls._initialized = True 78 | 79 | @classmethod 80 | def reset(cls): 81 | """Removes imported API functions from this class.""" 82 | apifunction.ApiFunction.clearApi(cls) 83 | cls._initialized = False 84 | 85 | def getMapId(self, vis_params=None): 86 | """Fetch and return a MapID. 87 | 88 | This mosaics the collection to a single image and return a mapid suitable 89 | for building a Google Maps overlay. 90 | 91 | Args: 92 | vis_params: The visualization parameters. 93 | 94 | Returns: 95 | A mapid and token. 96 | """ 97 | mosaic = apifunction.ApiFunction.call_('ImageCollection.mosaic', self) 98 | return mosaic.getMapId(vis_params) 99 | 100 | def select(self, selectors, opt_names=None, *args): 101 | """Select bands from each image in a collection. 102 | 103 | Args: 104 | selectors: An array of names, regexes or numeric indices specifying 105 | the bands to select. 106 | opt_names: An array of strings specifying the new names for the 107 | selected bands. If supplied, the length must match the number 108 | of bands selected. 109 | *args: Selector elements as varargs. 110 | 111 | Returns: 112 | The image collection with selected bands. 113 | """ 114 | return self.map(lambda img: img.select(selectors, opt_names, *args)) 115 | 116 | def first(self): 117 | """Returns the first entry from a given collection. 118 | 119 | Returns: 120 | The first entry from the collection. 121 | """ 122 | return image.Image(apifunction.ApiFunction.call_('Collection.first', self)) 123 | 124 | @staticmethod 125 | def name(): 126 | return 'ImageCollection' 127 | 128 | @staticmethod 129 | def elementType(): 130 | return image.Image 131 | 132 | 133 | def getFilmstripThumbURL(self, params=None): 134 | """Get the URL for a "filmstrip" thumbnail of the given collection. 135 | 136 | Args: 137 | params: Parameters identical to getMapId, plus, optionally: 138 | dimensions - 139 | (a number or pair of numbers in format WIDTHxHEIGHT) Max dimensions of 140 | the thumbnail to render, in pixels. If only one number is passed, it is 141 | used as the maximum, and the other dimension is computed by proportional 142 | scaling. 143 | crs - a CRS string specifying the projection of the output. 144 | crs_transform - the affine transform to use for the output pixel grid. 145 | scale - a scale to determine the output pixel grid; ignored if both crs 146 | and crs_transform are specified. 147 | region - (E,S,W,N or GeoJSON) Geospatial region of the result. By default, 148 | the whole image. 149 | format - (string) The output format (e.g., "png", "jpg"). 150 | 151 | Returns: 152 | A URL to download a thumbnail of the specified ImageCollection. 153 | 154 | Raises: 155 | EEException: If the region parameter is not an array or GeoJSON object. 156 | """ 157 | return self._getThumbURL(['png', 'jpg'], params) 158 | 159 | def _getThumbURL(self, valid_formats, params=None): 160 | """Get the URL for a thumbnail of this collection. 161 | 162 | Args: 163 | valid_formats: A list of supported formats, the first of which is used as 164 | a default if no format is supplied in 'params'. 165 | params: Parameters identical to getMapId, plus, optionally: 166 | dimensions - 167 | (a number or pair of numbers in format WIDTHxHEIGHT) Max dimensions of 168 | the thumbnail to render, in pixels. If only one number is passed, it is 169 | used as the maximum, and the other dimension is computed by proportional 170 | scaling. 171 | crs - a CRS string specifying the projection of the output. 172 | crs_transform - the affine transform to use for the output pixel grid. 173 | scale - a scale to determine the output pixel grid; ignored if both crs 174 | and crs_transform are specified. 175 | region - (E,S,W,N or GeoJSON) Geospatial region of the result. By default, 176 | the whole image. 177 | format - (string) The output format 178 | 179 | Returns: 180 | A URL to download a thumbnail of the specified ImageCollection. 181 | 182 | Raises: 183 | EEException: If the region parameter is not an array or GeoJSON object. 184 | """ 185 | def map_function(input_image, input_params): 186 | output_image, request = input_image._apply_crs_and_affine(input_params) # pylint: disable=protected-access 187 | output_image, request = output_image._apply_selection_and_scale(request) # pylint: disable=protected-access 188 | output_image, request = output_image._apply_visualization(request) # pylint: disable=protected-access 189 | return output_image, request 190 | 191 | clipped_collection, request = self._apply_preparation_function( 192 | map_function, params) 193 | 194 | request['format'] = params.get('format', valid_formats[0]) 195 | if request['format'] not in valid_formats: 196 | raise ee_exception.EEException( 197 | 'Invalid format specified for thumbnail. ' + str(params['format'])) 198 | 199 | request['image'] = clipped_collection 200 | if params and params.get('dimensions') is not None: 201 | request['dimensions'] = params.get('dimensions') 202 | 203 | return data.makeThumbUrl(data.getThumbId(request)) 204 | 205 | def _apply_preparation_function(self, preparation_function, params): 206 | """Applies a preparation function to an ImageCollection. 207 | 208 | Args: 209 | preparation_function: The preparation function. Takes an image and a 210 | parameter dict; returns the modified image and a subset of the 211 | parameter dict, with the parameters it used removed. 212 | params: The parameters to the preparation function. 213 | 214 | Returns: 215 | A tuple containing: 216 | - an ImageCollection that has had many of the parameters applied 217 | to it 218 | - any remaining parameters. 219 | """ 220 | # The preparation function operates only on a single image and returns a 221 | # modified parameter set; we need to apply across all the images in this 222 | # collection via self.map, and also return a modified parameter set, which 223 | # we can't easily get out of self.map. So we invoke it in two ways: once on 224 | # a dummy Image to get a modified parameter set, and once via self.map. 225 | _, remaining_params = preparation_function(self.first(), params) 226 | 227 | if remaining_params == params: 228 | # Nothing in params affects us; omit the map. 229 | return self, params 230 | 231 | # Copy params defensively in case it's modified after we return but before 232 | # the map operation is serialised. 233 | params = params.copy() 234 | def apply_params(img): 235 | prepared_img, _ = preparation_function(img, params) 236 | return prepared_img 237 | return self.map(apply_params), remaining_params 238 | 239 | def prepare_for_export(self, params): 240 | """Applies all relevant export parameters to an ImageCollection. 241 | 242 | Args: 243 | params: The export request parameters. 244 | 245 | Returns: 246 | A tuple containing: 247 | - an ImageCollection that has had many of the request parameters applied 248 | to it 249 | - any remaining parameters. 250 | """ 251 | # If the Cloud API is enabled, we can do cleaner handling of the parameters. 252 | # If it isn't enabled, we have to be bug-for-bug compatible with current 253 | # behaviour, so we do nothing. 254 | if data._use_cloud_api: # pylint: disable=protected-access 255 | return self._apply_preparation_function(image.Image.prepare_for_export, 256 | params) 257 | return self, params 258 | -------------------------------------------------------------------------------- /ee/oauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Earth Engine OAuth2 helper functions for generating client tokens. 3 | 4 | Typical use-case consists of: 5 | 1. Calling 'get_authorization_url' 6 | 2. Using a browser to access the output URL and copy the generated OAuth2 code 7 | 3. Calling 'request_token' to request a token using that code and the OAuth API 8 | 4. Calling 'write_token' to save the token at the path given by 9 | 'get_credentials_path' 10 | """ 11 | 12 | 13 | import datetime 14 | import errno 15 | import json 16 | import os 17 | from six.moves.urllib import parse 18 | from six.moves.urllib import request 19 | from six.moves.urllib.error import HTTPError 20 | 21 | 22 | CLIENT_ID = ('517222506229-vsmmajv00ul0bs7p89v5m89qs8eb9359.' 23 | 'apps.googleusercontent.com') 24 | CLIENT_SECRET = 'RUP0RZ6e0pPhDzsqIJ7KlNd1' 25 | SCOPES = [ 26 | 'https://www.googleapis.com/auth/earthengine', 27 | 'https://www.googleapis.com/auth/devstorage.full_control' 28 | ] 29 | REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' # Prompts user to copy-paste code 30 | TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' 31 | 32 | 33 | def get_credentials_path(): 34 | cred_path = os.path.expanduser('~/.config/earthengine/credentials') 35 | return cred_path 36 | 37 | 38 | def get_authorization_url(): 39 | """Returns a URL to generate an auth code.""" 40 | 41 | return 'https://accounts.google.com/o/oauth2/auth?' + parse.urlencode({ 42 | 'client_id': CLIENT_ID, 43 | 'scope': ' '.join(SCOPES), 44 | 'redirect_uri': REDIRECT_URI, 45 | 'response_type': 'code', 46 | }) 47 | 48 | 49 | def request_token(auth_code): 50 | """Uses authorization code to request tokens.""" 51 | 52 | request_args = { 53 | 'code': auth_code, 54 | 'client_id': CLIENT_ID, 55 | 'client_secret': CLIENT_SECRET, 56 | 'redirect_uri': REDIRECT_URI, 57 | 'grant_type': 'authorization_code', 58 | } 59 | 60 | refresh_token = None 61 | 62 | try: 63 | response = request.urlopen( 64 | TOKEN_URI, 65 | parse.urlencode(request_args).encode()).read().decode() 66 | refresh_token = json.loads(response)['refresh_token'] 67 | except HTTPError as e: 68 | raise Exception('Problem requesting tokens. Please try again. %s %s' % 69 | (e, e.read())) 70 | 71 | return refresh_token 72 | 73 | 74 | def write_token(refresh_token): 75 | """Attempts to write the passed token to the given user directory.""" 76 | 77 | credentials_path = get_credentials_path() 78 | dirname = os.path.dirname(credentials_path) 79 | try: 80 | os.makedirs(dirname) 81 | except OSError as e: 82 | if e.errno != errno.EEXIST: 83 | raise Exception('Error creating directory %s: %s' % (dirname, e)) 84 | 85 | json.dump({'refresh_token': refresh_token}, open(credentials_path, 'w')) 86 | -------------------------------------------------------------------------------- /ee/terrain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A namespace for Terrain.""" 3 | 4 | 5 | 6 | from . import apifunction 7 | 8 | # Using lowercase function naming to match the JavaScript names. 9 | # pylint: disable=g-bad-name 10 | 11 | 12 | class Terrain(object): 13 | """An namespace for Terrain Algorithms.""" 14 | 15 | _initialized = False 16 | 17 | @classmethod 18 | def initialize(cls): 19 | """Imports API functions to this class.""" 20 | if not cls._initialized: 21 | apifunction.ApiFunction.importApi(cls, 'Terrain', 'Terrain') 22 | cls._initialized = True 23 | 24 | @classmethod 25 | def reset(cls): 26 | """Removes imported API functions from this class.""" 27 | apifunction.ApiFunction.clearApi(cls) 28 | cls._initialized = False 29 | -------------------------------------------------------------------------------- /ee/tests/_helpers_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | import six 5 | 6 | import unittest 7 | import ee 8 | from ee import apitestcase 9 | from ee import deserializer 10 | from ee.apifunction import ApiFunction 11 | from ee.computedobject import ComputedObject 12 | 13 | 14 | class ProfilingTest(apitestcase.ApiTestCase): 15 | 16 | def MockSend(self, path, params, *args): 17 | """Overridden to check for profiling-related data.""" 18 | if path == '/value': 19 | value = deserializer.fromJSON(params['json']) 20 | hooked = ee.data._thread_locals.profile_hook is not None 21 | is_get_profiles = (isinstance(value, ComputedObject) and value.func == 22 | ApiFunction.lookup('Profile.getProfiles')) 23 | return 'hooked=%s getProfiles=%s' % (hooked, is_get_profiles) 24 | else: 25 | return super(ProfilingTest, self).MockSend(path, params, *args) 26 | 27 | def testProfilePrinting(self): 28 | out = six.StringIO() 29 | with ee.profilePrinting(destination=out): 30 | self.assertEqual('hooked=True getProfiles=False', ee.Number(1).getInfo()) 31 | self.assertEqual('hooked=False getProfiles=True', out.getvalue()) 32 | 33 | def testProfilePrintingDefaultSmoke(self): 34 | # This will print to sys.stderr, so we can't make any assertions about the 35 | # output. But we can check that it doesn't fail. 36 | with ee.profilePrinting(): 37 | self.assertEqual('hooked=True getProfiles=False', ee.Number(1).getInfo()) 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /ee/tests/apifunction_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Tests for the ee.apifunction module.""" 3 | 4 | 5 | 6 | import types 7 | 8 | import unittest 9 | 10 | import ee 11 | 12 | from ee import apitestcase 13 | 14 | 15 | class ApiFunctionTest(apitestcase.ApiTestCase): 16 | 17 | def testAddFunctions(self): 18 | """Verifies that addition of static and instance API functions.""" 19 | 20 | # Check instance vs static functions, and trampling of 21 | # existing functions. 22 | class TestClass(object): 23 | 24 | def pre_addBands(self): # pylint: disable=g-bad-name 25 | pass 26 | 27 | self.assertFalse(hasattr(TestClass, 'pre_load')) 28 | self.assertFalse(hasattr(TestClass, 'select')) 29 | self.assertFalse(hasattr(TestClass, 'pre_select')) 30 | self.assertTrue(hasattr(TestClass, 'pre_addBands')) 31 | self.assertFalse(hasattr(TestClass, '_pre_addBands')) 32 | 33 | ee.ApiFunction.importApi(TestClass, 'Image', 'Image', 'pre_') 34 | self.assertFalse(isinstance(TestClass.pre_load, types.MethodType)) 35 | self.assertFalse(hasattr(TestClass, 'select')) 36 | # Unbound methods are just functions in Python 3. Check both to maintain 37 | # backward compatibility. 38 | self.assertTrue(isinstance(TestClass.pre_select, 39 | (types.FunctionType, types.MethodType))) 40 | self.assertTrue(isinstance(TestClass.pre_addBands, 41 | (types.FunctionType, types.MethodType))) 42 | self.assertFalse(hasattr(TestClass, '_pre_addBands')) 43 | 44 | ee.ApiFunction.clearApi(TestClass) 45 | self.assertFalse(hasattr(TestClass, 'pre_load')) 46 | self.assertFalse(hasattr(TestClass, 'select')) 47 | self.assertFalse(hasattr(TestClass, 'pre_select')) 48 | self.assertTrue(hasattr(TestClass, 'pre_addBands')) 49 | self.assertFalse(hasattr(TestClass, '_pre_addBands')) 50 | 51 | def testAddFunctions_Inherited(self): 52 | """Verifies that inherited non-client functions can be overridden.""" 53 | 54 | class Base(object): 55 | 56 | def ClientOverride(self): 57 | pass 58 | 59 | class Child(Base): 60 | pass 61 | 62 | ee.ApiFunction.importApi(Base, 'Image', 'Image') 63 | ee.ApiFunction.importApi(Child, 'Image', 'Image') 64 | self.assertEqual(Base.ClientOverride, Child.ClientOverride) 65 | self.assertNotEqual(Base.addBands, Child.addBands) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /ee/tests/collection_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.collection module.""" 3 | 4 | 5 | 6 | import datetime 7 | 8 | import unittest 9 | 10 | import ee 11 | from ee import apitestcase 12 | 13 | 14 | class CollectionTestCase(apitestcase.ApiTestCase): 15 | 16 | def testSortAndLimit(self): 17 | """Verifies the behavior of the sort() and limit() methods.""" 18 | collection = ee.Collection(ee.Function(), {}) 19 | 20 | limited = collection.limit(10) 21 | self.assertEqual(ee.ApiFunction.lookup('Collection.limit'), limited.func) 22 | self.assertEqual({'collection': collection, 'limit': 10}, limited.args) 23 | 24 | sorted_collection = collection.sort('bar', True) 25 | self.assertEqual( 26 | ee.ApiFunction.lookup('Collection.limit'), sorted_collection.func) 27 | self.assertEqual({ 28 | 'collection': collection, 29 | 'key': ee.String('bar'), 30 | 'ascending': True 31 | }, sorted_collection.args) 32 | 33 | reverse_sorted_collection = collection.sort('bar', False) 34 | self.assertEqual( 35 | ee.ApiFunction.lookup('Collection.limit'), 36 | reverse_sorted_collection.func) 37 | self.assertEqual({ 38 | 'collection': collection, 39 | 'key': ee.String('bar'), 40 | 'ascending': False 41 | }, reverse_sorted_collection.args) 42 | 43 | def testFilter(self): 44 | """Verifies the behavior of filter() method.""" 45 | collection = ee.Collection(ee.Function(), {}) 46 | 47 | # We don't allow empty filters. 48 | self.assertRaises(Exception, collection.filter) 49 | 50 | filtered = collection.filter(ee.Filter.eq('foo', 1)) 51 | self.assertEqual(ee.ApiFunction.lookup('Collection.filter'), filtered.func) 52 | self.assertEqual({ 53 | 'collection': collection, 54 | 'filter': ee.Filter.eq('foo', 1) 55 | }, filtered.args) 56 | self.assertTrue(isinstance(filtered, ee.Collection)) 57 | 58 | def testFilterShortcuts(self): 59 | """Verifies the behavior of the various filtering shortcut methods.""" 60 | collection = ee.Collection(ee.Function(), {}) 61 | geom = {'type': 'Polygon', 'coordinates': [[[1, 2], [3, 4]]]} 62 | d1 = datetime.datetime.strptime('1/1/2000', '%m/%d/%Y') 63 | d2 = datetime.datetime.strptime('1/1/2001', '%m/%d/%Y') 64 | 65 | self.assertEqual( 66 | collection.filter(ee.Filter.geometry(geom)), 67 | collection.filterBounds(geom)) 68 | self.assertEqual( 69 | collection.filter(ee.Filter.date(d1)), collection.filterDate(d1)) 70 | self.assertEqual( 71 | collection.filter(ee.Filter.date(d1, d2)), collection.filterDate( 72 | d1, d2)) 73 | self.assertEqual( 74 | collection.filter(ee.Filter.eq('foo', 13)), 75 | collection.filterMetadata('foo', 'equals', 13)) 76 | 77 | def testMapping(self): 78 | """Verifies the behavior of the map() method.""" 79 | collection = ee.ImageCollection('foo') 80 | algorithm = lambda img: img.select('bar') 81 | mapped = collection.map(algorithm) 82 | 83 | self.assertTrue(isinstance(mapped, ee.ImageCollection)) 84 | self.assertEqual(ee.ApiFunction.lookup('Collection.map'), mapped.func) 85 | self.assertEqual(collection, mapped.args['collection']) 86 | 87 | # Need to do a serialized comparison for the function body because 88 | # variables returned from CustomFunction.variable() do not implement 89 | # __eq__. 90 | sig = { 91 | 'returns': 'Image', 92 | 'args': [{'name': '_MAPPING_VAR_0_0', 'type': 'Image'}] 93 | } 94 | expected_function = ee.CustomFunction(sig, algorithm) 95 | self.assertEqual(expected_function.serialize(), 96 | mapped.args['baseAlgorithm'].serialize()) 97 | 98 | def testNestedMapping(self): 99 | """Verifies that nested map() calls produce distinct variables.""" 100 | collection = ee.FeatureCollection('foo') 101 | result = collection.map(lambda x: collection.map(lambda y: [x, y])) 102 | 103 | # Verify the signatures. 104 | self.assertEqual('_MAPPING_VAR_1_0', 105 | result.args['baseAlgorithm']._signature['args'][0]['name']) 106 | inner_result = result.args['baseAlgorithm']._body 107 | self.assertEqual( 108 | '_MAPPING_VAR_0_0', 109 | inner_result.args['baseAlgorithm']._signature['args'][0]['name']) 110 | 111 | # Verify the references. 112 | self.assertEqual('_MAPPING_VAR_1_0', 113 | inner_result.args['baseAlgorithm']._body[0].varName) 114 | self.assertEqual('_MAPPING_VAR_0_0', 115 | inner_result.args['baseAlgorithm']._body[1].varName) 116 | 117 | def testIteration(self): 118 | """Verifies the behavior of the iterate() method.""" 119 | collection = ee.ImageCollection('foo') 120 | first = ee.Image(0) 121 | algorithm = lambda img, prev: img.addBands(ee.Image(prev)) 122 | result = collection.iterate(algorithm, first) 123 | 124 | self.assertEqual(ee.ApiFunction.lookup('Collection.iterate'), result.func) 125 | self.assertEqual(collection, result.args['collection']) 126 | self.assertEqual(first, result.args['first']) 127 | 128 | # Need to do a serialized comparison for the function body because 129 | # variables returned from CustomFunction.variable() do not implement 130 | # __eq__. 131 | sig = { 132 | 'returns': 'Object', 133 | 'args': [ 134 | {'name': '_MAPPING_VAR_0_0', 'type': 'Image'}, 135 | {'name': '_MAPPING_VAR_0_1', 'type': 'Object'} 136 | ] 137 | } 138 | expected_function = ee.CustomFunction(sig, algorithm) 139 | self.assertEqual(expected_function.serialize(), 140 | result.args['function'].serialize()) 141 | 142 | 143 | if __name__ == '__main__': 144 | unittest.main() 145 | -------------------------------------------------------------------------------- /ee/tests/computedobject_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Tests for the ee.computedobject module.""" 3 | 4 | 5 | 6 | import six # For Python 2/3 compatibility 7 | 8 | import unittest 9 | import ee 10 | from ee import apitestcase 11 | 12 | 13 | class ComputedObjectTest(apitestcase.ApiTestCase): 14 | 15 | def testComputedObject(self): 16 | """Verifies that untyped calls wrap the result in a ComputedObject.""" 17 | 18 | result = ee.ApiFunction.call_('DateRange', 1, 2) 19 | self.assertTrue(isinstance(result.serialize(), six.string_types)) 20 | self.assertEqual({'value': 'fakeValue'}, result.getInfo()) 21 | 22 | def testInternals(self): 23 | """Test eq(), ne() and hash().""" 24 | a = ee.ApiFunction.call_('DateRange', 1, 2) 25 | b = ee.ApiFunction.call_('DateRange', 2, 3) 26 | c = ee.ApiFunction.call_('DateRange', 1, 2) 27 | 28 | self.assertEqual(a, a) 29 | self.assertNotEqual(a, b) 30 | self.assertEqual(a, c) 31 | self.assertNotEqual(b, c) 32 | self.assertNotEqual(hash(a), hash(b)) 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /ee/tests/data_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | import httplib2 5 | import mock 6 | from six.moves import urllib 7 | import unittest 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class DataTest(unittest.TestCase): 13 | 14 | def testGetTaskList(self): 15 | 16 | def Request(unused_self, url, method, body, headers): 17 | _ = method, body, headers # Unused kwargs. 18 | 19 | parse_result = urllib.parse.urlparse(url) 20 | if parse_result.path != '/api/tasklist': 21 | return httplib2.Response({'status': 404}), 'not found' 22 | 23 | resp_body = '{}' 24 | query_args = urllib.parse.parse_qs(parse_result.query) 25 | if query_args == {'pagesize': ['500']}: 26 | resp_body = ('{"data": {"tasks": [{"id": "1"}],' 27 | ' "next_page_token": "foo"}}') 28 | elif query_args == {'pagesize': ['500'], 'pagetoken': ['foo']}: 29 | resp_body = '{"data": {"tasks": [{"id": "2"}]}}' 30 | 31 | response = httplib2.Response({ 32 | 'status': 200, 33 | 'content-type': 'application/json', 34 | }) 35 | return response, resp_body 36 | 37 | with mock.patch('httplib2.Http.request', new=Request): 38 | self.assertEqual([{'id': '1'}, {'id': '2'}], ee.data.getTaskList()) 39 | 40 | def testListOperations(self): 41 | mock_http = mock.MagicMock(httplib2.Http) 42 | # Return in three groups. 43 | mock_http.request.side_effect = [ 44 | (httplib2.Response({ 45 | 'status': 200 46 | }), b'{"operations": [{"name": "name1"}], "nextPageToken": "t1"}'), 47 | (httplib2.Response({ 48 | 'status': 200 49 | }), b'{"operations": [{"name": "name2"}], "nextPageToken": "t2"}'), 50 | (httplib2.Response({ 51 | 'status': 200 52 | }), b'{"operations": [{"name": "name3"}]}'), 53 | ] 54 | with apitestcase.UsingCloudApi(mock_http=mock_http): 55 | self.assertEqual([{ 56 | 'name': 'name1' 57 | }, { 58 | 'name': 'name2' 59 | }, { 60 | 'name': 'name3' 61 | }], ee.data.listOperations()) 62 | 63 | def testListOperationsEmptyList(self): 64 | # Empty lists don't appear at all in the result. 65 | mock_http = mock.MagicMock(httplib2.Http) 66 | mock_http.request.return_value = (httplib2.Response({'status': 200}), b'{}') 67 | with apitestcase.UsingCloudApi(mock_http=mock_http): 68 | self.assertEqual([], ee.data.listOperations()) 69 | 70 | @mock.patch('time.sleep') 71 | def testSuccess(self, mock_sleep): 72 | with DoStubHttp(200, 'application/json', '{"data": "bar"}'): 73 | self.assertEqual('bar', ee.data.send_('/foo', {})) 74 | self.assertEqual(False, mock_sleep.called) 75 | 76 | @mock.patch('time.sleep') 77 | def testRetry(self, mock_sleep): 78 | with DoStubHttp(429, 'application/json', '{"data": "bar"}'): 79 | with self.assertRaises(ee.ee_exception.EEException): 80 | ee.data.send_('/foo', {}) 81 | self.assertEqual(5, mock_sleep.call_count) 82 | 83 | def testNon200Success(self): 84 | with DoStubHttp(202, 'application/json', '{"data": "bar"}'): 85 | self.assertEqual('bar', ee.data.send_('/foo', {})) 86 | 87 | def testJsonSyntaxError(self): 88 | with DoStubHttp(200, 'application/json', '{"data"}'): 89 | with self.assertRaises(ee.ee_exception.EEException) as cm: 90 | ee.data.send_('/foo', {}) 91 | self.assertEqual('Invalid JSON: {"data"}', str(cm.exception)) 92 | 93 | def testJsonStructureError(self): 94 | with DoStubHttp(200, 'application/json', '{}'): 95 | with self.assertRaises(ee.ee_exception.EEException) as cm: 96 | ee.data.send_('/foo', {}) 97 | self.assertEqual('Malformed response: {}', str(cm.exception)) 98 | 99 | def testUnexpectedStatus(self): 100 | with DoStubHttp(418, 'text/html', ''): 101 | with self.assertRaises(ee.ee_exception.EEException) as cm: 102 | ee.data.send_('/foo', {}) 103 | self.assertEqual('Server returned HTTP code: 418', str(cm.exception)) 104 | 105 | def testJson200Error(self): 106 | with DoStubHttp(200, 'application/json', 107 | '{"error": {"code": 500, "message": "bar"}}'): 108 | with self.assertRaises(ee.ee_exception.EEException) as cm: 109 | ee.data.send_('/foo', {}) 110 | self.assertEqual(u'bar', str(cm.exception)) 111 | 112 | def testJsonNon2xxError(self): 113 | with DoStubHttp(400, 'application/json', 114 | '{"error": {"code": 400, "message": "bar"}}'): 115 | with self.assertRaises(ee.ee_exception.EEException) as cm: 116 | ee.data.send_('/foo', {}) 117 | self.assertEqual(u'bar', str(cm.exception)) 118 | 119 | def testWrongContentType(self): 120 | with DoStubHttp(200, 'text/html', '{"data": "bar"}'): 121 | with self.assertRaises(ee.ee_exception.EEException) as cm: 122 | ee.data.send_('/foo', {}) 123 | self.assertEqual(u'Response was unexpectedly not JSON, but text/html', 124 | str(cm.exception)) 125 | 126 | def testNoContentType(self): 127 | with DoStubHttp(200, None, '{"data": "bar"}'): 128 | self.assertEqual('bar', ee.data.send_('/foo', {})) 129 | 130 | def testContentTypeParameterAllowed(self): 131 | with DoStubHttp(200, 'application/json; charset=utf-8', '{"data": ""}'): 132 | self.assertEqual('', ee.data.send_('/foo', {})) 133 | 134 | def testRawSuccess(self): 135 | with DoStubHttp(200, 'image/png', 'FAKEDATA'): 136 | self.assertEqual('FAKEDATA', ee.data.send_('/foo', {}, opt_raw=True)) 137 | 138 | def testRawError(self): 139 | with DoStubHttp(400, 'application/json', 140 | '{"error": {"code": 400, "message": "bar"}}'): 141 | with self.assertRaises(ee.ee_exception.EEException) as cm: 142 | ee.data.send_('/foo', {}, opt_raw=True) 143 | self.assertEqual(u'Server returned HTTP code: 400', str(cm.exception)) 144 | 145 | def testRaw200Error(self): 146 | """Raw shouldn't be parsed, so the error-in-200 shouldn't be noticed. 147 | 148 | (This is an edge case we do not expect to see.) 149 | """ 150 | with DoStubHttp(200, 'application/json', 151 | '{"error": {"code": 400, "message": "bar"}}'): 152 | self.assertEqual('{"error": {"code": 400, "message": "bar"}}', 153 | ee.data.send_('/foo', {}, opt_raw=True)) 154 | 155 | def testNotProfiling(self): 156 | # Test that we do not request profiling. 157 | with DoProfileStubHttp(self, False): 158 | ee.data.send_('/foo', {}) 159 | 160 | def testProfiling(self): 161 | with DoProfileStubHttp(self, True): 162 | seen = [] 163 | def ProfileHook(profile_id): 164 | seen.append(profile_id) 165 | 166 | with ee.data.profiling(ProfileHook): 167 | ee.data.send_('/foo', {}) 168 | self.assertEqual(['someProfileId'], seen) 169 | 170 | def testProfilingCleanup(self): 171 | with DoProfileStubHttp(self, True): 172 | try: 173 | with ee.data.profiling(lambda _: None): 174 | raise ExceptionForTest() 175 | except ExceptionForTest: 176 | pass 177 | 178 | # Should not have profiling enabled after exiting the context by raising. 179 | with DoProfileStubHttp(self, False): 180 | ee.data.send_('/foo', {}) 181 | 182 | def testListAssets(self): 183 | cloud_api_resource = mock.MagicMock() 184 | with apitestcase.UsingCloudApi(cloud_api_resource=cloud_api_resource): 185 | mock_result = {'assets': [{'path': 'id1', 'type': 'type1'}]} 186 | cloud_api_resource.projects().assets().listAssets( 187 | ).execute.return_value = mock_result 188 | actual_result = ee.data.listAssets({'p': 'q'}) 189 | cloud_api_resource.projects().assets().listAssets().\ 190 | execute.assert_called_once() 191 | self.assertEqual(mock_result, actual_result) 192 | 193 | def testListImages(self): 194 | cloud_api_resource = mock.MagicMock() 195 | with apitestcase.UsingCloudApi(cloud_api_resource=cloud_api_resource): 196 | mock_result = {'assets': [{'path': 'id1', 'type': 'type1'}]} 197 | cloud_api_resource.projects().assets().listImages( 198 | ).execute.return_value = mock_result 199 | actual_result = ee.data.listImages({'p': 'q'}) 200 | cloud_api_resource.projects().assets().listImages( 201 | ).execute.assert_called_once() 202 | self.assertEqual(mock_result, actual_result) 203 | 204 | def testListBuckets(self): 205 | cloud_api_resource = mock.MagicMock() 206 | with apitestcase.UsingCloudApi(cloud_api_resource=cloud_api_resource): 207 | mock_result = {'assets': [{'name': 'id1', 'type': 'FOLDER'}]} 208 | cloud_api_resource.projects().listAssets( 209 | ).execute.return_value = mock_result 210 | actual_result = ee.data.listBuckets() 211 | cloud_api_resource.projects().listAssets( 212 | ).execute.assert_called_once() 213 | self.assertEqual(mock_result, actual_result) 214 | 215 | def testSimpleGetListViaCloudApi(self): 216 | cloud_api_resource = mock.MagicMock() 217 | with apitestcase.UsingCloudApi(cloud_api_resource=cloud_api_resource): 218 | mock_result = {'assets': [{'name': 'id1', 'type': 'IMAGE_COLLECTION'}]} 219 | cloud_api_resource.projects().assets().listAssets( 220 | ).execute.return_value = mock_result 221 | actual_result = ee.data.getList({'id': 'glam', 'num': 3}) 222 | expected_params = { 223 | 'parent': 'projects/earthengine-public/assets/glam', 224 | 'pageSize': 3 225 | } 226 | expected_result = [{'id': 'id1', 'type': 'ImageCollection'}] 227 | cloud_api_resource.projects().assets().listAssets.assert_called_with( 228 | **expected_params) 229 | self.assertEqual(expected_result, actual_result) 230 | 231 | def testComplexGetListViaCloudApi(self): 232 | cloud_api_resource = mock.MagicMock() 233 | with apitestcase.UsingCloudApi(cloud_api_resource=cloud_api_resource): 234 | mock_result = { 235 | 'images': [{ 236 | 'name': 'id1', 237 | 'size_bytes': 1234 238 | }] 239 | } 240 | cloud_api_resource.projects().assets().listImages( 241 | ).execute.return_value = mock_result 242 | actual_result = ee.data.getList({ 243 | 'id': 'glam', 244 | 'num': 3, 245 | 'starttime': 3612345 246 | }) 247 | expected_params = { 248 | 'parent': 'projects/earthengine-public/assets/glam', 249 | 'pageSize': 3, 250 | 'startTime': '1970-01-01T01:00:12.345000Z', 251 | 'fields': 'images(name)' 252 | } 253 | expected_result = [{'id': 'id1', 'type': 'Image'}] 254 | cloud_api_resource.projects().assets().listImages.assert_called_with( 255 | **expected_params) 256 | self.assertEqual(expected_result, actual_result) 257 | 258 | def testCloudProfilingEnabled(self): 259 | seen = [] 260 | 261 | def ProfileHook(profile_id): 262 | seen.append(profile_id) 263 | 264 | with ee.data.profiling(ProfileHook): 265 | with apitestcase.UsingCloudApi(), DoCloudProfileStubHttp(self, True): 266 | ee.data.listImages({'parent': 'projects/earthengine-public/assets/q'}) 267 | self.assertEqual(['someProfileId'], seen) 268 | 269 | def testCloudProfilingDisabled(self): 270 | with apitestcase.UsingCloudApi(), DoCloudProfileStubHttp(self, False): 271 | ee.data.listImages({'parent': 'projects/earthengine-public/assets/q'}) 272 | 273 | def testCloudErrorTranslation(self): 274 | mock_http = mock.MagicMock(httplib2.Http) 275 | mock_http.request.return_value = (httplib2.Response({'status': 400}), 276 | b'{"error": {"message": "errorly"} }') 277 | with apitestcase.UsingCloudApi(mock_http=mock_http): 278 | with self.assertRaisesRegexp(ee.ee_exception.EEException, '^errorly$'): 279 | ee.data.listImages({'parent': 'projects/earthengine-public/assets/q'}) 280 | 281 | 282 | def DoStubHttp(status, mime, resp_body): 283 | """Context manager for temporarily overriding Http.""" 284 | def Request(unused_self, unused_url, method, body, headers): 285 | _ = method, body, headers # Unused kwargs. 286 | response = httplib2.Response({ 287 | 'status': status, 288 | 'content-type': mime, 289 | }) 290 | return response, resp_body 291 | return mock.patch('httplib2.Http.request', new=Request) 292 | 293 | 294 | def DoProfileStubHttp(test, expect_profiling): 295 | def Request(unused_self, unused_url, method, body, headers): 296 | _ = method, headers # Unused kwargs. 297 | test.assertEqual(expect_profiling, 'profiling=1' in body, msg=body) 298 | response_dict = { 299 | 'status': 200, 300 | 'content-type': 'application/json' 301 | } 302 | if expect_profiling: 303 | response_dict[ 304 | ee.data._PROFILE_RESPONSE_HEADER_LOWERCASE] = 'someProfileId' 305 | response = httplib2.Response(response_dict) 306 | return response, '{"data": "dummy_data"}' 307 | return mock.patch('httplib2.Http.request', new=Request) 308 | 309 | 310 | def DoCloudProfileStubHttp(test, expect_profiling): 311 | 312 | def Request(unused_self, unused_url, method, body, headers): 313 | _ = method, body # Unused kwargs. 314 | test.assertEqual(expect_profiling, 315 | ee.data._PROFILE_REQUEST_HEADER in headers) 316 | response_dict = { 317 | 'status': 200, 318 | 'content-type': 'application/json' 319 | } 320 | if expect_profiling: 321 | response_dict[ 322 | ee.data._PROFILE_RESPONSE_HEADER_LOWERCASE] = 'someProfileId' 323 | response = httplib2.Response(response_dict) 324 | return response, '{"data": "dummy_data"}' 325 | return mock.patch('httplib2.Http.request', new=Request) 326 | 327 | 328 | class ExceptionForTest(Exception): 329 | pass 330 | 331 | 332 | if __name__ == '__main__': 333 | unittest.main() 334 | -------------------------------------------------------------------------------- /ee/tests/date_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.date module.""" 3 | 4 | 5 | 6 | import datetime 7 | 8 | import unittest 9 | 10 | import ee 11 | from ee import apitestcase 12 | 13 | 14 | class DateTest(apitestcase.ApiTestCase): 15 | def testDate(self): 16 | """Verifies date constructors.""" 17 | 18 | datefunc = ee.ApiFunction.lookup('Date') 19 | 20 | d1 = ee.Date('2000-01-01') 21 | d2 = ee.Date(946684800000) 22 | d3 = ee.Date(datetime.datetime(2000, 1, 1)) 23 | d4 = ee.Date(d3) 24 | dates = [d1, d2, d3, d4] 25 | 26 | for d in dates: 27 | self.assertTrue(isinstance(d, ee.Date)) 28 | self.assertEqual(datefunc, d.func) 29 | 30 | self.assertEqual(d1.args, {'value': '2000-01-01'}) 31 | for d in dates[1:]: 32 | self.assertEqual(d.args['value'], 946684800000) 33 | 34 | d5 = ee.Date(ee.CustomFunction.variable('Date', 'foo')) 35 | self.assertTrue(isinstance(d5, ee.Date)) 36 | self.assertTrue(d5.isVariable()) 37 | self.assertEqual('foo', d5.varName) 38 | 39 | # A non-date variable. 40 | v = ee.CustomFunction.variable('Number', 'bar') 41 | d6 = ee.Date(v) 42 | self.assertTrue(isinstance(d6, ee.Date)) 43 | self.assertFalse(d6.isVariable()) 44 | self.assertEqual(datefunc, d6.func) 45 | self.assertEqual({'value': v}, d6.args) 46 | 47 | # A non-date ComputedObject, promotion and casting. 48 | obj = ee.ApiFunction.call_('DateRange', 1, 2) 49 | d7 = ee.Date(obj) 50 | self.assertTrue(isinstance(d7, ee.Date)) 51 | self.assertEqual(datefunc, d7.func) 52 | self.assertEqual({'value': obj}, d7.args) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /ee/tests/deserializer_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Tests for the ee.deserializer module.""" 3 | 4 | 5 | 6 | import json 7 | 8 | import unittest 9 | 10 | import ee 11 | from ee import apitestcase 12 | from ee import deserializer 13 | from ee import serializer 14 | 15 | 16 | class DeserializerTest(apitestcase.ApiTestCase): 17 | 18 | def testRoundTrip(self): 19 | """Verifies a round trip of a comprehensive serialization case.""" 20 | encoded = apitestcase.ENCODED_JSON_SAMPLE 21 | decoded = deserializer.decode(encoded) 22 | re_encoded = json.loads(serializer.toJSON(decoded)) 23 | self.assertEqual(encoded, re_encoded) 24 | 25 | def testCast(self): 26 | """Verifies that decoding casts the result to the right class.""" 27 | input_image = ee.Image(13).addBands(42) 28 | output = deserializer.fromJSON(serializer.toJSON(input_image)) 29 | self.assertTrue(isinstance(output, ee.Image)) 30 | 31 | def testReuse(self): 32 | """Verifies that decoding results can be used and re-encoded.""" 33 | input_image = ee.Image(13) 34 | output = deserializer.fromJSON(serializer.toJSON(input_image)) 35 | self.assertEqual( 36 | output.addBands(42).serialize(), 37 | input_image.addBands(42).serialize()) 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /ee/tests/dictionary_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.dictionary module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class DictionaryTest(apitestcase.ApiTestCase): 13 | 14 | def testDictionary(self): 15 | """Verifies basic behavior of ee.Dictionary.""" 16 | src = {'a': 1, 'b': 2, 'c': 'three'} 17 | dictionary = ee.Dictionary(src) 18 | self.assertEqual({ 19 | 'type': 'Dictionary', 20 | 'value': src 21 | }, 22 | ee.Serializer(False)._encode(dictionary)) 23 | self.assertEqual({'constantValue': { 24 | 'a': 1, 25 | 'b': 2, 26 | 'c': 'three' 27 | }}, 28 | ee.Serializer(False, 29 | for_cloud_api=True)._encode(dictionary)) 30 | 31 | f = ee.Feature(None, {'properties': src}) 32 | computed = ee.Dictionary(f.get('properties')) 33 | self.assertTrue(isinstance(computed, ee.Dictionary)) 34 | 35 | # The 4 types of arguments we expect 36 | cons = (ee.Dictionary(src), 37 | ee.Dictionary(f.get('properties')), 38 | ee.Dictionary(), 39 | ee.Dictionary(('one', 1))) 40 | 41 | for d in cons: 42 | self.assertTrue(isinstance(d, ee.ComputedObject)) 43 | 44 | def testInternals(self): 45 | """Test eq(), ne() and hash().""" 46 | a = ee.Dictionary({'one': 1}) 47 | b = ee.Dictionary({'two': 2}) 48 | c = ee.Dictionary({'one': 1}) 49 | 50 | self.assertEqual(a, a) 51 | self.assertNotEqual(a, b) 52 | self.assertEqual(a, c) 53 | self.assertNotEqual(b, c) 54 | self.assertNotEqual(hash(a), hash(b)) 55 | 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /ee/tests/element_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.element module.""" 3 | 4 | 5 | 6 | import six 7 | 8 | import unittest 9 | import ee 10 | from ee import apitestcase 11 | 12 | 13 | class ElementTestCase(apitestcase.ApiTestCase): 14 | 15 | def testSet(self): 16 | """Verifies Element.set() keyword argument interpretation.""" 17 | image = ee.Image(1) 18 | 19 | # Constant dictionary. 20 | def AssertProperties(expected, image): 21 | properties = {} 22 | while image.func == ee.ApiFunction.lookup('Element.set'): 23 | key = image.args['key'] 24 | if not isinstance(key, six.string_types): 25 | key = key.encode() 26 | properties[key] = image.args['value'] 27 | image = image.args['object'] 28 | self.assertEqual(ee.Image(1), image) 29 | self.assertEqual(expected, properties) 30 | 31 | AssertProperties({'foo': 'bar'}, image.set({'foo': 'bar'})) 32 | AssertProperties({'foo': 'bar'}, image.set({'properties': {'foo': 'bar'}})) 33 | AssertProperties({'properties': 5}, image.set({'properties': 5})) 34 | AssertProperties({'properties': {'foo': 'bar'}, 'baz': 'quux'}, 35 | image.set({'properties': {'foo': 'bar'}, 'baz': 'quux'})) 36 | AssertProperties({'foo': 'bar', 'baz': 'quux'}, 37 | image.set('foo', 'bar', 'baz', 'quux')) 38 | 39 | # Computed dictionary. 40 | computed_arg = ee.ComputedObject(None, None, 'foo') 41 | 42 | def CheckMultiProperties(result): 43 | self.assertEqual(ee.ApiFunction.lookup('Element.setMulti'), result.func) 44 | self.assertEqual({ 45 | 'object': image, 46 | 'properties': ee.Dictionary(computed_arg) 47 | }, result.args) 48 | CheckMultiProperties(image.set(computed_arg)) 49 | CheckMultiProperties(image.set({'properties': computed_arg})) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /ee/tests/feature_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.feature module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class FeatureTest(apitestcase.ApiTestCase): 13 | 14 | def testConstructors(self): 15 | """Verifies that constructors understand valid parameters.""" 16 | point = ee.Geometry.Point(1, 2) 17 | from_geometry = ee.Feature(point) 18 | self.assertEqual(ee.ApiFunction('Feature'), from_geometry.func) 19 | self.assertEqual({'geometry': point, 'metadata': None}, from_geometry.args) 20 | 21 | from_null_geometry = ee.Feature(None, {'x': 2}) 22 | self.assertEqual(ee.ApiFunction('Feature'), from_null_geometry.func) 23 | self.assertEqual({ 24 | 'geometry': None, 25 | 'metadata': { 26 | 'x': 2 27 | } 28 | }, from_null_geometry.args) 29 | 30 | computed_geometry = ee.Geometry(ee.ComputedObject(ee.Function(), {'a': 1})) 31 | computed_properties = ee.ComputedObject(ee.Function(), {'b': 2}) 32 | from_computed_one = ee.Feature(computed_geometry) 33 | from_computed_both = ee.Feature(computed_geometry, computed_properties) 34 | self.assertEqual(ee.ApiFunction('Feature'), from_computed_one.func) 35 | self.assertEqual({ 36 | 'geometry': computed_geometry, 37 | 'metadata': None 38 | }, from_computed_one.args) 39 | self.assertEqual(ee.ApiFunction('Feature'), from_computed_both.func) 40 | self.assertEqual({ 41 | 'geometry': computed_geometry, 42 | 'metadata': computed_properties 43 | }, from_computed_both.args) 44 | 45 | from_variable = ee.Feature(ee.CustomFunction.variable(None, 'foo')) 46 | self.assertTrue(isinstance(from_variable, ee.Feature)) 47 | self.assertEqual({ 48 | 'type': 'ArgumentRef', 49 | 'value': 'foo' 50 | }, from_variable.encode(None)) 51 | 52 | from_geo_json_feature = ee.Feature({ 53 | 'type': 'Feature', 54 | 'id': 'bar', 55 | 'geometry': point.toGeoJSON(), 56 | 'properties': {'foo': 42} 57 | }) 58 | self.assertEqual(ee.ApiFunction('Feature'), from_geo_json_feature.func) 59 | self.assertEqual(point, from_geo_json_feature.args['geometry']) 60 | self.assertEqual({ 61 | 'foo': 42, 62 | 'system:index': 'bar' 63 | }, from_geo_json_feature.args['metadata']) 64 | 65 | def testGetMap(self): 66 | """Verifies that getMap() uses Collection.draw to rasterize Features.""" 67 | feature = ee.Feature(None) 68 | mapid = feature.getMapId({'color': 'ABCDEF'}) 69 | manual = ee.ApiFunction.apply_('Collection.draw', { 70 | 'collection': ee.FeatureCollection([feature]), 71 | 'color': 'ABCDEF'}) 72 | 73 | self.assertEqual('fakeMapId', mapid['mapid']) 74 | self.assertEqual(manual.serialize(), mapid['image'].serialize()) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /ee/tests/featurecollection_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.featurecollection module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class FeatureCollectionTestCase(apitestcase.ApiTestCase): 13 | 14 | def testConstructors(self): 15 | """Verifies that constructors understand valid parameters.""" 16 | from_id = ee.FeatureCollection('abcd') 17 | self.assertEqual( 18 | ee.ApiFunction.lookup('Collection.loadTable'), from_id.func) 19 | self.assertEqual({'tableId': 'abcd'}, from_id.args) 20 | 21 | from_id_and_geom_column = ee.FeatureCollection('abcd', 'xyz') 22 | self.assertEqual( 23 | ee.ApiFunction.lookup('Collection.loadTable'), 24 | from_id_and_geom_column.func) 25 | self.assertEqual({ 26 | 'tableId': 'abcd', 27 | 'geometryColumn': 'xyz' 28 | }, from_id_and_geom_column.args) 29 | 30 | geometry = ee.Geometry.Point(1, 2) 31 | feature = ee.Feature(geometry) 32 | from_geometries = ee.FeatureCollection([geometry]) 33 | from_single_geometry = ee.FeatureCollection(geometry) 34 | from_features = ee.FeatureCollection([feature]) 35 | from_single_feature = ee.FeatureCollection(feature) 36 | self.assertEqual(from_geometries, from_single_geometry) 37 | self.assertEqual(from_geometries, from_features) 38 | self.assertEqual(from_geometries, from_single_feature) 39 | self.assertEqual(ee.ApiFunction.lookup('Collection'), from_geometries.func) 40 | self.assertEqual({'features': [feature]}, from_geometries.args) 41 | 42 | # Test a computed list object. 43 | l = ee.List([feature]).slice(0) 44 | from_list = ee.FeatureCollection(l) 45 | self.assertEqual({'features': l}, from_list.args) 46 | 47 | from_computed_object = ee.FeatureCollection( 48 | ee.ComputedObject(None, {'x': 'y'})) 49 | self.assertEqual({'x': 'y'}, from_computed_object.args) 50 | 51 | def testGetMapId(self): 52 | """Verifies that getMap() uses Collection.draw to draw.""" 53 | collection = ee.FeatureCollection('test5') 54 | mapid = collection.getMapId({'color': 'ABCDEF'}) 55 | manual = ee.ApiFunction.call_('Collection.draw', collection, 'ABCDEF') 56 | 57 | self.assertEqual('fakeMapId', mapid['mapid']) 58 | self.assertEqual(manual, mapid['image']) 59 | 60 | def testDownload(self): 61 | """Verifies that Download ID and URL generation.""" 62 | csv_url = ee.FeatureCollection('test7').getDownloadURL('csv') 63 | 64 | self.assertEqual('/table', self.last_table_call['url']) 65 | self.assertEqual({ 66 | 'table': ee.FeatureCollection('test7').serialize(), 67 | 'json_format': 'v2', 68 | 'format': 'CSV' 69 | }, self.last_table_call['data']) 70 | self.assertEqual('/api/table?docid=5&token=6', csv_url) 71 | 72 | everything_url = ee.FeatureCollection('test8').getDownloadURL( 73 | 'json', 'bar, baz', 'qux') 74 | self.assertEqual({ 75 | 'table': ee.FeatureCollection('test8').serialize(), 76 | 'json_format': 'v2', 77 | 'format': 'JSON', 78 | 'selectors': 'bar, baz', 79 | 'filename': 'qux' 80 | }, self.last_table_call['data']) 81 | self.assertEqual('/api/table?docid=5&token=6', everything_url) 82 | 83 | self.assertEqual( 84 | ee.FeatureCollection('test7').getDownloadUrl('csv'), 85 | ee.FeatureCollection('test7').getDownloadURL('csv')) 86 | 87 | def testSelect(self): 88 | def equals(c1, c2): 89 | self.assertEqual(c1.serialize(), c2.serialize()) 90 | 91 | fc = ee.FeatureCollection(ee.Feature(ee.Geometry.Point(0, 0), {'a': 5})) 92 | equals(fc.select('a'), fc.select(['a'])) 93 | equals(fc.select('a', 'b'), fc.select(['a', 'b'])) 94 | equals(fc.select('a', 'b', 'c'), fc.select(['a', 'b', 'c'])) 95 | equals(fc.select('a', 'b', 'c', 'd'), fc.select(['a', 'b', 'c', 'd'])) 96 | 97 | equals(fc.select(['a']), fc.select(['a'], None, True)) 98 | equals(fc.select(['a'], None, False), 99 | fc.select(propertySelectors=['a'], retainGeometry=False)) 100 | 101 | if __name__ == '__main__': 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /ee/tests/filter_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.filter module.""" 3 | 4 | 5 | 6 | import datetime 7 | 8 | import unittest 9 | 10 | import ee 11 | from ee import apitestcase 12 | 13 | 14 | class FilterTest(apitestcase.ApiTestCase): 15 | 16 | def testConstructors(self): 17 | """Verifies that constructors understand valid parameters.""" 18 | from_static_method = ee.Filter.gt('foo', 1) 19 | from_computed_object = ee.Filter( 20 | ee.ApiFunction.call_('Filter.greaterThan', 'foo', 1)) 21 | self.assertEqual(from_static_method, from_computed_object) 22 | 23 | copy = ee.Filter(from_static_method) 24 | self.assertEqual(from_static_method, copy) 25 | 26 | def testMetadata(self): 27 | """Verifies that the metadata_() method works.""" 28 | self.assertEqual( 29 | ee.ApiFunction.call_('Filter.equals', 'x', 1), 30 | ee.Filter.metadata_('x', 'equals', 1)) 31 | self.assertEqual( 32 | ee.Filter.metadata_('x', 'equals', 1), ee.Filter.eq('x', 1)) 33 | self.assertEqual( 34 | ee.Filter.metadata_('x', 'EQUALS', 1), ee.Filter.eq('x', 1)) 35 | self.assertEqual( 36 | ee.Filter.metadata_('x', 'not_equals', 1), ee.Filter.neq('x', 1)) 37 | self.assertEqual( 38 | ee.Filter.metadata_('x', 'less_than', 1), ee.Filter.lt('x', 1)) 39 | self.assertEqual( 40 | ee.Filter.metadata_('x', 'not_greater_than', 1), ee.Filter.lte('x', 1)) 41 | self.assertEqual( 42 | ee.Filter.metadata_('x', 'greater_than', 1), ee.Filter.gt('x', 1)) 43 | self.assertEqual( 44 | ee.Filter.metadata_('x', 'not_less_than', 1), ee.Filter.gte('x', 1)) 45 | 46 | def testLogicalCombinations(self): 47 | """Verifies that the and() and or() methods work.""" 48 | f1 = ee.Filter.eq('x', 1) 49 | f2 = ee.Filter.eq('x', 2) 50 | 51 | or_filter = ee.Filter.Or(f1, f2) 52 | self.assertEqual(ee.ApiFunction.call_('Filter.or', (f1, f2)), or_filter) 53 | 54 | and_filter = ee.Filter.And(f1, f2) 55 | self.assertEqual(ee.ApiFunction.call_('Filter.and', (f1, f2)), and_filter) 56 | 57 | self.assertEqual( 58 | ee.ApiFunction.call_('Filter.or', (or_filter, and_filter)), 59 | ee.Filter.Or(or_filter, and_filter)) 60 | 61 | def testDate(self): 62 | """Verifies that date filters work.""" 63 | d1 = datetime.datetime.strptime('1/1/2000', '%m/%d/%Y') 64 | d2 = datetime.datetime.strptime('1/1/2001', '%m/%d/%Y') 65 | instant_range = ee.ApiFunction.call_('DateRange', d1, None) 66 | long_range = ee.ApiFunction.call_('DateRange', d1, d2) 67 | 68 | instant_filter = ee.Filter.date(d1) 69 | self.assertEqual( 70 | ee.ApiFunction.lookup('Filter.dateRangeContains'), instant_filter.func) 71 | self.assertEqual({ 72 | 'leftValue': instant_range, 73 | 'rightField': ee.String('system:time_start') 74 | }, instant_filter.args) 75 | 76 | long_filter = ee.Filter.date(d1, d2) 77 | self.assertEqual( 78 | ee.ApiFunction.lookup('Filter.dateRangeContains'), long_filter.func) 79 | self.assertEqual({ 80 | 'leftValue': long_range, 81 | 'rightField': ee.String('system:time_start') 82 | }, long_filter.args) 83 | 84 | def testBounds(self): 85 | """Verifies that geometry intersection filters work.""" 86 | polygon = ee.Geometry.Polygon(1, 2, 3, 4, 5, 6) 87 | self.assertEqual( 88 | ee.ApiFunction.call_('Filter.intersects', '.all', 89 | ee.ApiFunction.call_('Feature', polygon)), 90 | ee.Filter.geometry(polygon)) 91 | 92 | # Collection-to-geometry promotion. 93 | collection = ee.FeatureCollection('foo') 94 | feature = ee.ApiFunction.call_( 95 | 'Feature', ee.ApiFunction.call_('Collection.geometry', collection)) 96 | self.assertEqual( 97 | ee.ApiFunction.call_('Filter.intersects', '.all', feature), 98 | ee.Filter.geometry(collection)) 99 | 100 | def testInList(self): 101 | """Verifies that list membership filters work.""" 102 | self.assertEqual( 103 | ee.Filter.listContains(None, None, 'foo', [1, 2]), 104 | ee.Filter.inList('foo', [1, 2])) 105 | 106 | def testInternals(self): 107 | """Test eq(), ne() and hash().""" 108 | a = ee.Filter.eq('x', 1) 109 | b = ee.Filter.eq('x', 2) 110 | c = ee.Filter.eq('x', 1) 111 | 112 | self.assertEqual(a, a) 113 | self.assertNotEqual(a, b) 114 | self.assertEqual(a, c) 115 | self.assertNotEqual(b, c) 116 | self.assertNotEqual(hash(a), hash(b)) 117 | 118 | 119 | if __name__ == '__main__': 120 | unittest.main() 121 | -------------------------------------------------------------------------------- /ee/tests/function_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Tests for the ee.function module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | 10 | # A function to experiment on. 11 | TEST_FUNC = ee.Function() 12 | TEST_FUNC.getSignature = lambda: { # pylint: disable-msg=g-long-lambda 13 | 'description': 'Method description.', 14 | 'returns': 'Image', 15 | 'args': [ 16 | { 17 | 'type': 'Image', 18 | 'name': 'a', 19 | 'description': 'Arg A doc.'}, 20 | { 21 | 'type': 'Image', 22 | 'name': 'b', 23 | 'description': 'Arg B doc.', 24 | 'optional': True 25 | } 26 | ] 27 | } 28 | 29 | EXPECTED_DOC = """Method description. 30 | 31 | Args: 32 | a: Arg A doc. 33 | b: Arg B doc.""" 34 | 35 | 36 | class FunctionTest(unittest.TestCase): 37 | 38 | def testNameArgs(self): 39 | """Verifies that Functions can convert positional to named arguments.""" 40 | self.assertEqual({}, TEST_FUNC.nameArgs([])) 41 | self.assertEqual({'a': 42}, TEST_FUNC.nameArgs([42])) 42 | self.assertEqual({'a': 42, 'b': 13}, TEST_FUNC.nameArgs([42, 13])) 43 | self.assertEqual({'a': 3, 'b': 5}, TEST_FUNC.nameArgs([3], {'b': 5})) 44 | 45 | self.assertRaisesWithRegexpMatch('Too many', TEST_FUNC.nameArgs, [1, 2, 3]) 46 | 47 | def testPromoteArgs(self): 48 | """Verifies that Functions can promote and verify their arguments.""" 49 | old_promoter = ee.Function._promoter 50 | ee.Function._registerPromoter(lambda obj, type_name: [type_name, obj]) 51 | 52 | # Regular call. 53 | self.assertEqual({ 54 | 'a': ['Image', 42], 55 | 'b': ['Image', 13] 56 | }, TEST_FUNC.promoteArgs({ 57 | 'a': 42, 58 | 'b': 13 59 | })) 60 | 61 | # Allow missing optional argument. 62 | self.assertEqual({'a': ['Image', 42]}, TEST_FUNC.promoteArgs({'a': 42})) 63 | 64 | # Disallow unknown arguments. 65 | self.assertRaisesWithRegexpMatch( 66 | 'Required argument', TEST_FUNC.promoteArgs, {}) 67 | 68 | # Disallow unknown arguments. 69 | self.assertRaisesWithRegexpMatch( 70 | 'Unrecognized', TEST_FUNC.promoteArgs, {'a': 42, 'c': 13}) 71 | 72 | # Clean up. 73 | ee.Function._registerPromoter(old_promoter) 74 | 75 | def testCall(self): 76 | """Verifies the full function invocation flow.""" 77 | old_promoter = ee.Function._promoter 78 | ee.Function._registerPromoter(lambda obj, type_name: [type_name, obj]) 79 | 80 | return_type, return_value = TEST_FUNC.call(42, 13) 81 | self.assertEqual('Image', return_type) 82 | self.assertEqual(TEST_FUNC, return_value.func) 83 | self.assertEqual({ 84 | 'a': ['Image', 42], 85 | 'b': ['Image', 13] 86 | }, return_value.args) 87 | 88 | # Clean up. 89 | ee.Function._registerPromoter(old_promoter) 90 | 91 | def testToString(self): 92 | """Verifies function docstring generation.""" 93 | self.assertEqual(EXPECTED_DOC, str(TEST_FUNC)) 94 | 95 | def assertRaisesWithRegexpMatch(self, msg, func, *args): 96 | try: 97 | func(*args) 98 | except ee.EEException as e: 99 | self.assertTrue(msg in str(e)) 100 | else: 101 | self.fail('Expected an exception.') 102 | 103 | 104 | if __name__ == '__main__': 105 | unittest.main() 106 | -------------------------------------------------------------------------------- /ee/tests/geometry_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.geometry module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class GeometryTest(apitestcase.ApiTestCase): 13 | 14 | def testValid_Point(self): 15 | """Verifies Point constructor behavior with valid arguments.""" 16 | self.assertValid(1, ee.Geometry.Point, [1, 2]) 17 | self.assertValid(1, ee.Geometry.Point, 1, 2) 18 | 19 | def testValid_MultiPoint(self): 20 | """Verifies MultiPoint constructor behavior with valid arguments.""" 21 | self.assertValid(2, ee.Geometry.MultiPoint, 1, 2, 3, 4, 5, 6) 22 | self.assertValid(1, ee.Geometry.MultiPoint) 23 | 24 | def testValid_LineString(self): 25 | """Verifies LineString constructor behavior with valid arguments.""" 26 | self.assertValid(2, ee.Geometry.LineString, 1, 2, 3, 4, 5, 6) 27 | 28 | def testValid_LinearRing(self): 29 | """Verifies LinearRing constructor behavior with valid arguments.""" 30 | self.assertValid(2, ee.Geometry.LinearRing, 1, 2, 3, 4, 5, 6) 31 | 32 | def testValid_MultiLineString(self): 33 | """Verifies MultiLineString constructor behavior with valid arguments.""" 34 | self.assertValid(3, ee.Geometry.MultiLineString, 1, 2, 3, 4, 5, 6) 35 | self.assertValid(1, ee.Geometry.MultiLineString) 36 | 37 | def testValid_Polygon(self): 38 | """Verifies Polygon constructor behavior with valid arguments.""" 39 | self.assertValid(3, ee.Geometry.Polygon, 1, 2, 3, 4, 5, 6) 40 | 41 | def testValid_Rectangle(self): 42 | """Verifies Rectangle constructor behavior with valid arguments.""" 43 | self.assertValid(3, ee.Geometry.Rectangle, 1, 2, 5, 6) 44 | 45 | def testValid_MultiPolygon(self): 46 | """Verifies MultiPolygon constructor behavior with valid arguments.""" 47 | self.assertValid(4, ee.Geometry.MultiPolygon, 1, 2, 3, 4, 5, 6) 48 | self.assertValid(1, ee.Geometry.MultiPolygon) 49 | 50 | def testValid_GeometryCollection(self): 51 | """Verifies GeometryCollection constructor behavior with valid arguments.""" 52 | geometry = ee.Geometry({ 53 | 'type': 54 | 'GeometryCollection', 55 | 'geometries': [{ 56 | 'type': 'Polygon', 57 | 'coordinates': [[[-1, -1], [0, 1], [1, -1]]], 58 | 'geodesic': True, 59 | 'evenOdd': True 60 | }, { 61 | 'type': 'Point', 62 | 'coordinates': [0, 0] 63 | }, { 64 | 'type': 65 | 'GeometryCollection', 66 | 'geometries': [{ 67 | 'type': 'Point', 68 | 'coordinates': [1, 2] 69 | }, { 70 | 'type': 'Point', 71 | 'coordinates': [2, 1] 72 | }] 73 | }], 74 | 'coordinates': [] 75 | }) 76 | self.assertTrue(isinstance(geometry, ee.Geometry)) 77 | 78 | def testInvalid_Point(self): 79 | """Verifies Point constructor behavior with invalid arguments.""" 80 | f = ee.Geometry.Point 81 | self.assertInvalid(f, 'Invalid geometry', ['-78.204948', '40.966539']) 82 | 83 | def testInvalid_MultiPoint(self): 84 | """Verifies MultiPoint constructor behavior with invalid arguments.""" 85 | f = ee.Geometry.MultiPoint 86 | self.assertInvalid( 87 | f, 'Invalid number of coordinates: 5', 1, 2, 3, 4, 5) 88 | self.assertInvalid(f, 'Invalid number of coordinates: 5', [1, 2, 3, 4, 5]) 89 | self.assertInvalid(f, 'Invalid geometry', [[1, 2], [3, 4], 5]) 90 | # Too many nesting levels. 91 | self.assertInvalid(f, 'Invalid geometry', [[[1, 2], [3, 4]]]) 92 | 93 | def testInvalid_LineString(self): 94 | """Verifies LineString constructor behavior with invalid arguments.""" 95 | f = ee.Geometry.LineString 96 | self.assertInvalid( 97 | f, 'Invalid number of coordinates: 5', 1, 2, 3, 4, 5) 98 | self.assertInvalid(f, 'Invalid number of coordinates: 5', [1, 2, 3, 4, 5]) 99 | self.assertInvalid(f, 'Invalid geometry', [[1, 2], [3, 4], 5]) 100 | # Too many nesting levels. 101 | self.assertInvalid(f, 'Invalid geometry', [[[1, 2], [3, 4]]]) 102 | 103 | def testInvalid_LinearRing(self): 104 | """Verifies LinearRing constructor behavior with invalid arguments.""" 105 | f = ee.Geometry.LinearRing 106 | self.assertInvalid( 107 | f, 'Invalid number of coordinates: 5', 1, 2, 3, 4, 5) 108 | self.assertInvalid(f, 'Invalid number of coordinates: 5', [1, 2, 3, 4, 5]) 109 | self.assertInvalid(f, 'Invalid geometry', [[1, 2], [3, 4], 5]) 110 | # Too many nesting levels. 111 | self.assertInvalid(f, 'Invalid geometry', [[[1, 2], [3, 4]]]) 112 | 113 | def testInvalid_MultiLineString(self): 114 | """Verifies MultiLineString constructor behavior with invalid arguments.""" 115 | f = ee.Geometry.MultiLineString 116 | self.assertInvalid( 117 | f, 'Invalid number of coordinates: 5', 1, 2, 3, 4, 5) 118 | self.assertInvalid(f, 'Invalid number of coordinates: 5', [1, 2, 3, 4, 5]) 119 | self.assertInvalid(f, 'Invalid geometry', [[1, 2], [3, 4], 5]) 120 | # Too many nesting levels. 121 | self.assertInvalid(f, 'Invalid geometry', [[[[1, 2], [3, 4]]]]) 122 | # Bad nesting 123 | self.assertInvalid(f, 'Invalid geometry', [[[1, 2], [3, 4]], [1, 2]]) 124 | 125 | def testInvalid_Polygon(self): 126 | """Verifies Polygon constructor behavior with invalid arguments.""" 127 | f = ee.Geometry.Polygon 128 | self.assertInvalid( 129 | f, 'Invalid number of coordinates: 5', 1, 2, 3, 4, 5) 130 | self.assertInvalid(f, 'Invalid number of coordinates: 5', [1, 2, 3, 4, 5]) 131 | self.assertInvalid(f, 'Invalid geometry', [[1, 2], [3, 4], 5]) 132 | # Too many nesting levels. 133 | self.assertInvalid(f, 'Invalid geometry', [[[[1, 2], [3, 4], [5, 6]]]]) 134 | # Bad nesting 135 | self.assertInvalid(f, 'Invalid geometry', [[[1, 2], [3, 4]], [1, 2]]) 136 | 137 | def testInvalid_MultiPolygon(self): 138 | """Verifies MultiPolygon constructor behavior with invalid arguments.""" 139 | f = ee.Geometry.MultiPolygon 140 | self.assertInvalid(f, 'Invalid number of coordinates: 5', 1, 2, 3, 4, 5) 141 | self.assertInvalid(f, 'Invalid number of coordinates: 5', [1, 2, 3, 4, 5]) 142 | self.assertInvalid(f, 'Invalid geometry', [[1, 2], [3, 4], 5]) 143 | # Too many nesting levels. 144 | self.assertInvalid(f, 'Invalid geometry', [[[[[1, 2], [3, 4], [5, 6]]]]]) 145 | # Bad nesting 146 | self.assertInvalid(f, 'Invalid geometry', [[[[1, 2], [3, 4]], [1, 2]]]) 147 | 148 | def testEvenOddPolygon(self): 149 | poly1 = ee.Geometry.Polygon([0, 0, 0, 5, 5, 0]) 150 | self.assertTrue(poly1.toGeoJSON()['evenOdd']) 151 | poly2 = ee.Geometry.Polygon([0, 0, 0, 5, 5, 0], None, None, None, False) 152 | self.assertFalse(poly2.toGeoJSON()['evenOdd']) 153 | 154 | def testArrayConstructors(self): 155 | """Verifies that constructors that take arrays fix nesting.""" 156 | get_coordinates_count = lambda g: len(g.toGeoJSON()['coordinates']) 157 | 158 | point = ee.Geometry.Point([1, 2]) 159 | self.assertEqual(2, get_coordinates_count(point)) 160 | 161 | multipoint = ee.Geometry.MultiPoint([[1, 2], [3, 4], [5, 6]]) 162 | self.assertEqual(3, get_coordinates_count(multipoint)) 163 | 164 | line = ee.Geometry.LineString([[1, 2], [3, 4], [5, 6]]) 165 | self.assertEqual(3, get_coordinates_count(line)) 166 | 167 | ring = ee.Geometry.LinearRing([[1, 2], [3, 4], [5, 6]]) 168 | self.assertEqual(3, get_coordinates_count(ring)) 169 | 170 | multiline = ee.Geometry.MultiLineString( 171 | [[[1, 2], [3, 4]], 172 | [[5, 6], [7, 8]]]) 173 | self.assertEqual(2, get_coordinates_count(multiline)) 174 | 175 | polygon = ee.Geometry.Polygon([[[1, 2], [3, 4], [5, 6]]]) 176 | self.assertEqual(1, get_coordinates_count(polygon)) 177 | 178 | mpolygon = ee.Geometry.MultiPolygon( 179 | [[[[1, 2], [3, 4], [5, 6]]], 180 | [[[1, 2], [3, 4], [5, 6]]]]) 181 | self.assertEqual(2, get_coordinates_count(mpolygon)) 182 | 183 | def testGeodesicFlag(self): 184 | """Verifies that JSON parsing and generation preserves the geodesic flag.""" 185 | geodesic = ee.Geometry({ 186 | 'type': 'LineString', 187 | 'coordinates': [[1, 2], [3, 4]], 188 | 'geodesic': True 189 | }) 190 | projected = ee.Geometry({ 191 | 'type': 'LineString', 192 | 'coordinates': [[1, 2], [3, 4]], 193 | 'geodesic': False 194 | }) 195 | self.assertTrue(geodesic.toGeoJSON()['geodesic']) 196 | self.assertFalse(projected.toGeoJSON()['geodesic']) 197 | 198 | def testConstructor(self): 199 | """Check the behavior of the Geometry constructor. 200 | 201 | There are 5 options: 202 | 1) A geoJSON object. 203 | 2) A not-computed geometry. 204 | 3) A not-computed geometry with overrides. 205 | 4) A computed geometry. 206 | 5) something to cast to geometry. 207 | """ 208 | line = ee.Geometry.LineString(1, 2, 3, 4) 209 | 210 | # GeoJSON. 211 | from_json = ee.Geometry(line.toGeoJSON()) 212 | self.assertEqual(from_json.func, None) 213 | self.assertEqual(from_json._type, 'LineString') 214 | self.assertEqual(from_json._coordinates, [[1, 2], [3, 4]]) 215 | 216 | # GeoJSON with a CRS specified. 217 | json_with_crs = line.toGeoJSON() 218 | json_with_crs['crs'] = { 219 | 'type': 'name', 220 | 'properties': { 221 | 'name': 'SR-ORG:6974' 222 | } 223 | } 224 | from_json_with_crs = ee.Geometry(json_with_crs) 225 | self.assertEqual(from_json_with_crs.func, None) 226 | self.assertEqual(from_json_with_crs._type, 'LineString') 227 | self.assertEqual(from_json_with_crs._proj, 'SR-ORG:6974') 228 | 229 | # A not-computed geometry. 230 | self.assertEqual(ee.Geometry(line), line) 231 | 232 | # A not-computed geometry with an override. 233 | with_override = ee.Geometry(line, 'SR-ORG:6974') 234 | self.assertEqual(with_override._proj, 'SR-ORG:6974') 235 | 236 | # A computed geometry. 237 | self.assertEqual(ee.Geometry(line.bounds()), line.bounds()) 238 | 239 | # Something to cast to a geometry. 240 | computed = ee.ComputedObject(ee.Function(), {'a': 1}) 241 | geom = ee.Geometry(computed) 242 | self.assertEqual(computed.func, geom.func) 243 | self.assertEqual(computed.args, geom.args) 244 | 245 | def testComputedGeometries(self): 246 | """Verifies the computed object behavior of the Geometry constructor.""" 247 | line = ee.Geometry.LineString(1, 2, 3, 4) 248 | bounds = line.bounds() 249 | 250 | self.assertTrue(isinstance(bounds, ee.Geometry)) 251 | self.assertEqual(ee.ApiFunction.lookup('Geometry.bounds'), bounds.func) 252 | self.assertEqual(line, bounds.args['geometry']) 253 | self.assertTrue(hasattr(bounds, 'bounds')) 254 | 255 | def testComputedCoordinate(self): 256 | """Verifies that a computed coordinate produces a computed geometry.""" 257 | coords = [1, ee.Number(1).add(1)] 258 | p = ee.Geometry.Point(coords) 259 | 260 | self.assertTrue(isinstance(p, ee.Geometry)) 261 | self.assertEqual( 262 | ee.ApiFunction.lookup('GeometryConstructors.Point'), p.func) 263 | self.assertEqual({'coordinates': ee.List(coords)}, p.args) 264 | 265 | def testComputedList(self): 266 | """Verifies that a computed coordinate produces a computed geometry.""" 267 | lst = ee.List([1, 2, 3, 4]).slice(0, 2) 268 | p = ee.Geometry.Point(lst) 269 | 270 | self.assertTrue(isinstance(p, ee.Geometry)) 271 | self.assertEqual( 272 | ee.ApiFunction.lookup('GeometryConstructors.Point'), p.func) 273 | self.assertEqual({'coordinates': lst}, p.args) 274 | 275 | def testComputedProjection(self): 276 | """Verifies that a geometry with a projection can be constructed.""" 277 | p = ee.Geometry.Point([1, 2], 'epsg:4326') 278 | 279 | self.assertTrue(isinstance(p, ee.Geometry)) 280 | self.assertEqual( 281 | ee.ApiFunction.lookup('GeometryConstructors.Point'), p.func) 282 | expected_args = { 283 | 'coordinates': ee.List([1, 2]), 284 | 'crs': ee.ApiFunction.lookup('Projection').call('epsg:4326') 285 | } 286 | self.assertEqual(expected_args, p.args) 287 | 288 | def testGeometryInputs(self): 289 | """Verifies that a geometry with geometry inputs can be constructed.""" 290 | p1 = ee.Geometry.Point([1, 2]) 291 | p2 = ee.Geometry.Point([3, 4]) 292 | line = ee.Geometry.LineString([p1, p2]) 293 | 294 | self.assertTrue(isinstance(line, ee.Geometry)) 295 | self.assertEqual( 296 | ee.ApiFunction.lookup('GeometryConstructors.LineString'), line.func) 297 | self.assertEqual({'coordinates': ee.List([p1, p2])}, line.args) 298 | 299 | def testOldPointKeywordArgs(self): 300 | """Verifies that Points still allow keyword lon/lat args.""" 301 | self.assertEqual(ee.Geometry.Point(1, 2), ee.Geometry.Point(lon=1, lat=2)) 302 | self.assertEqual(ee.Geometry.Point(1, 2), ee.Geometry.Point(1, lat=2)) 303 | 304 | def testOldRectangleKeywordArgs(self): 305 | """Verifies that Rectangles still allow keyword xlo/ylo/xhi/yhi args.""" 306 | self.assertEqual( 307 | ee.Geometry.Rectangle(1, 2, 3, 4), 308 | ee.Geometry.Rectangle(xlo=1, ylo=2, xhi=3, yhi=4)) 309 | self.assertEqual( 310 | ee.Geometry.Rectangle(1, 2, 3, 4), 311 | ee.Geometry.Rectangle(1, 2, xhi=3, yhi=4)) 312 | 313 | def assertValid(self, nesting, ctor, *coords): 314 | """Checks that geometry is valid and has the expected nesting level. 315 | 316 | Args: 317 | nesting: The expected coordinate nesting level. 318 | ctor: The geometry constructor function, e.g. ee.Geometry.MultiPoint. 319 | *coords: The coordinates of the geometry. 320 | """ 321 | # The constructor already does a validity check. 322 | geometry = ctor(*coords) 323 | self.assertTrue(isinstance(geometry, ee.Geometry)) 324 | self.assertTrue(isinstance(geometry.toGeoJSON(), dict)) 325 | final_coords = geometry.toGeoJSON()['coordinates'] 326 | self.assertEqual(nesting, ee.Geometry._isValidCoordinates(final_coords)) 327 | 328 | def assertInvalid(self, ctor, msg, *coords): 329 | """Verifies that geometry is invalid. 330 | 331 | Calls the given constructor with whatever arguments have been passed, 332 | and verifies that the given error message is thrown. 333 | 334 | Args: 335 | ctor: The geometry constructor function, e.g. ee.Geometry.MultiPoint. 336 | msg: The expected error message in the thrown exception. 337 | *coords: The coordinates of the geometry. 338 | """ 339 | with self.assertRaisesRegexp(ee.EEException, msg): 340 | ctor(*coords) 341 | 342 | def testInternals(self): 343 | """Test eq(), ne() and hash().""" 344 | a = ee.Geometry.Point(1, 2) 345 | b = ee.Geometry.Point(2, 1) 346 | c = ee.Geometry.Point(1, 2) 347 | 348 | self.assertEqual(a, a) 349 | self.assertNotEqual(a, b) 350 | self.assertEqual(a, c) 351 | self.assertNotEqual(b, c) 352 | self.assertNotEqual(hash(a), hash(b)) 353 | 354 | 355 | if __name__ == '__main__': 356 | unittest.main() 357 | -------------------------------------------------------------------------------- /ee/tests/imagecollection_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.imagecollection module.""" 3 | 4 | 5 | 6 | import mock 7 | 8 | import unittest 9 | import ee 10 | from ee import apitestcase 11 | 12 | 13 | class ImageCollectionTestCase(apitestcase.ApiTestCase): 14 | 15 | def testImageCollectionConstructors(self): 16 | """Verifies that constructors understand valid parameters.""" 17 | from_id = ee.ImageCollection('abcd') 18 | self.assertEqual( 19 | ee.ApiFunction.lookup('ImageCollection.load'), from_id.func) 20 | self.assertEqual({'id': 'abcd'}, from_id.args) 21 | 22 | from_images = ee.ImageCollection([ee.Image(1), ee.Image(2)]) 23 | self.assertEqual( 24 | ee.ApiFunction.lookup('ImageCollection.fromImages'), from_images.func) 25 | self.assertEqual({'images': [ee.Image(1), ee.Image(2)]}, from_images.args) 26 | 27 | self.assertEqual( 28 | ee.ImageCollection([ee.Image(1)]), ee.ImageCollection(ee.Image(1))) 29 | 30 | original = ee.ImageCollection('foo') 31 | from_other_image_collection = ee.ImageCollection(original) 32 | self.assertEqual(from_other_image_collection, original) 33 | 34 | l = ee.List([ee.Image(1)]).slice(0) 35 | from_list = ee.ImageCollection(l) 36 | self.assertEqual({'images': l}, from_list.args) 37 | 38 | from_computed_object = ee.ImageCollection( 39 | ee.ComputedObject(None, {'x': 'y'})) 40 | self.assertEqual({'x': 'y'}, from_computed_object.args) 41 | 42 | def testImperativeFunctions(self): 43 | """Verifies that imperative functions return ready values.""" 44 | image_collection = ee.ImageCollection(ee.Image(1)) 45 | self.assertEqual({'value': 'fakeValue'}, image_collection.getInfo()) 46 | self.assertEqual('fakeMapId', image_collection.getMapId()['mapid']) 47 | 48 | def testFilter(self): 49 | """Verifies that filtering an ImageCollection wraps the result.""" 50 | collection = ee.ImageCollection(ee.Image(1)) 51 | noop_filter = ee.Filter() 52 | filtered = collection.filter(noop_filter) 53 | self.assertTrue(isinstance(filtered, ee.ImageCollection)) 54 | self.assertEqual(ee.ApiFunction.lookup('Collection.filter'), filtered.func) 55 | self.assertEqual({ 56 | 'collection': collection, 57 | 'filter': noop_filter 58 | }, filtered.args) 59 | 60 | def testFirst(self): 61 | """Verifies that first gets promoted properly.""" 62 | first = ee.ImageCollection(ee.Image(1)).first() 63 | self.assertTrue(isinstance(first, ee.Image)) 64 | self.assertEqual(ee.ApiFunction.lookup('Collection.first'), first.func) 65 | 66 | def testPrepareForExport(self): 67 | """Verifies proper handling of export-related parameters.""" 68 | with apitestcase.UsingCloudApi(): 69 | base_collection = ee.ImageCollection(ee.Image(1)) 70 | 71 | collection, params = base_collection.prepare_for_export( 72 | {'something': 'else'}) 73 | self.assertEqual(base_collection, collection) 74 | self.assertEqual({'something': 'else'}, params) 75 | 76 | collection, params = base_collection.prepare_for_export({ 77 | 'crs': 'ABCD', 78 | 'crs_transform': '1,2,3,4,5,6' 79 | }) 80 | 81 | # Need to do a serialized comparison for the collection because 82 | # custom functions don't implement equality comparison. 83 | def expected_preparation_function(img): 84 | return img.reproject( 85 | crs='ABCD', crsTransform=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) 86 | 87 | expected_collection = base_collection.map(expected_preparation_function) 88 | self.assertEqual( 89 | expected_collection.serialize(for_cloud_api=True), 90 | collection.serialize(for_cloud_api=True)) 91 | self.assertEqual({}, params) 92 | 93 | 94 | if __name__ == '__main__': 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /ee/tests/list_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.lber module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class ListTest(apitestcase.ApiTestCase): 13 | 14 | def testList(self): 15 | """Verifies basic behavior of ee.List.""" 16 | l = ee.List([1, 2, 3]) 17 | self.assertEqual([1, 2, 3], ee.Serializer(False)._encode(l)) 18 | 19 | computed = ee.List([1, 2, 3]).slice(0) # pylint: disable=no-member 20 | self.assertTrue(isinstance(computed, ee.List)) 21 | self.assertEqual(ee.ApiFunction.lookup('List.slice'), computed.func) 22 | self.assertEqual({ 23 | 'list': ee.List([1, 2, 3]), 24 | 'start': ee.Number(0) 25 | }, computed.args) 26 | 27 | def testMapping(self): 28 | lst = ee.List(['foo', 'bar']) 29 | body = lambda s: ee.String(s).cat('bar') 30 | mapped = lst.map(body) 31 | 32 | self.assertTrue(isinstance(mapped, ee.List)) 33 | self.assertEqual(ee.ApiFunction.lookup('List.map'), mapped.func) 34 | self.assertEqual(lst, mapped.args['list']) 35 | 36 | # Need to do a serialized comparison for the function body because 37 | # variables returned from CustomFunction.variable() do not implement 38 | # __eq__. 39 | sig = { 40 | 'returns': 'Object', 41 | 'args': [{'name': '_MAPPING_VAR_0_0', 'type': 'Object'}] 42 | } 43 | expected_function = ee.CustomFunction(sig, body) 44 | self.assertEqual(expected_function.serialize(), 45 | mapped.args['baseAlgorithm'].serialize()) 46 | self.assertEqual( 47 | expected_function.serialize(for_cloud_api=True), 48 | mapped.args['baseAlgorithm'].serialize(for_cloud_api=True)) 49 | 50 | def testInternals(self): 51 | """Test eq(), ne() and hash().""" 52 | a = ee.List([1, 2]) 53 | b = ee.List([2, 1]) 54 | c = ee.List([1, 2]) 55 | 56 | self.assertTrue(a.__eq__(a)) 57 | self.assertFalse(a.__eq__(b)) 58 | self.assertTrue(a.__eq__(c)) 59 | self.assertTrue(b.__ne__(c)) 60 | self.assertNotEqual(a.__hash__(), b.__hash__()) 61 | self.assertEqual(a.__hash__(), c.__hash__()) 62 | 63 | 64 | if __name__ == '__main__': 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /ee/tests/number_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.number module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class NumberTest(apitestcase.ApiTestCase): 13 | 14 | def testNumber(self): 15 | """Verifies basic behavior of ee.Number.""" 16 | num = ee.Number(1) 17 | self.assertEqual(1, num.encode()) 18 | 19 | computed = ee.Number(1).add(2) 20 | self.assertTrue(isinstance(computed, ee.Number)) 21 | self.assertEqual(ee.ApiFunction.lookup('Number.add'), computed.func) 22 | self.assertEqual({ 23 | 'left': ee.Number(1), 24 | 'right': ee.Number(2) 25 | }, computed.args) 26 | 27 | def testInternals(self): 28 | """Test eq(), ne() and hash().""" 29 | a = ee.Number(1) 30 | b = ee.Number(2.1) 31 | c = ee.Number(1) 32 | 33 | self.assertEqual(a, a) 34 | self.assertNotEqual(a, b) 35 | self.assertEqual(a, c) 36 | self.assertNotEqual(b, c) 37 | self.assertNotEqual(hash(a), hash(b)) 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /ee/tests/oauth_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test class for oauth.""" 3 | 4 | 5 | import json 6 | import mock 7 | 8 | from six.moves.urllib import parse 9 | 10 | import tempfile 11 | import unittest 12 | 13 | import ee 14 | 15 | 16 | class OAuthTest(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.test_tmpdir = tempfile.mkdtemp() 20 | 21 | def testRequestToken(self): 22 | 23 | class MockResponse(object): 24 | 25 | def __init__(self, code): 26 | self.code = code.decode() 27 | 28 | def read(self): 29 | return ('{"refresh_token": "' + self.code + '456"}').encode() 30 | 31 | def mock_urlopen(unused_url, param): 32 | return MockResponse(parse.parse_qs(param)[b'code'][0]) 33 | 34 | with mock.patch('six.moves.urllib.request.urlopen', new=mock_urlopen): 35 | auth_code = '123' 36 | refresh_token = ee.oauth.request_token(auth_code) 37 | self.assertEqual('123456', refresh_token) 38 | 39 | def testWriteToken(self): 40 | 41 | def mock_credentials_path(): 42 | return self.test_tmpdir+'/tempfile' 43 | 44 | oauth_pkg = 'ee.oauth' 45 | with mock.patch(oauth_pkg+'.get_credentials_path', 46 | new=mock_credentials_path): 47 | refresh_token = '123' 48 | ee.oauth.write_token(refresh_token) 49 | 50 | with open(mock_credentials_path()) as f: 51 | token = json.load(f) 52 | self.assertEqual({'refresh_token': '123'}, token) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /ee/tests/serializer_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Tests for the ee.serializer module.""" 3 | 4 | 5 | 6 | import json 7 | 8 | import unittest 9 | 10 | import ee 11 | from ee import apitestcase 12 | from ee import serializer 13 | 14 | 15 | class SerializerTest(apitestcase.ApiTestCase): 16 | 17 | def testSerialization(self): 18 | """Verifies a complex serialization case.""" 19 | 20 | class ByteString(ee.Encodable): 21 | """A custom Encodable class that does not use invocations. 22 | 23 | This one is actually supported by the EE API encoding. 24 | """ 25 | 26 | def __init__(self, value): 27 | """Creates a bytestring with a given string value.""" 28 | self._value = value 29 | 30 | def encode(self, unused_encoder): # pylint: disable-msg=g-bad-name 31 | return {'type': 'Bytes', 'value': self._value} 32 | 33 | def encode_cloud_value(self, unused_encoder): 34 | # Proto3 JSON embedding of "bytes" values uses base64 encoding, which 35 | # this already is. 36 | return {'bytesValue': self._value} 37 | 38 | call = ee.ComputedObject('String.cat', {'string1': 'x', 'string2': 'y'}) 39 | body = lambda x, y: ee.CustomFunction.variable(None, 'y') 40 | sig = {'returns': 'Object', 41 | 'args': [ 42 | {'name': 'x', 'type': 'Object'}, 43 | {'name': 'y', 'type': 'Object'}]} 44 | custom_function = ee.CustomFunction(sig, body) 45 | to_encode = [ 46 | None, 47 | True, 48 | 5, 49 | 7, 50 | 3.4, 51 | 112233445566778899, 52 | 'hello', 53 | ee.Date(1234567890000), 54 | ee.Geometry(ee.Geometry.LineString(1, 2, 3, 4), 'SR-ORG:6974'), 55 | ee.Geometry.Polygon([ 56 | [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], 57 | [[5, 6], [7, 6], [7, 8], [5, 8]], 58 | [[1, 1], [2, 1], [2, 2], [1, 2]] 59 | ]), 60 | ByteString('aGVsbG8='), 61 | { 62 | 'foo': 'bar', 63 | 'baz': call 64 | }, 65 | call, 66 | custom_function 67 | ] 68 | 69 | self.assertEqual(apitestcase.ENCODED_JSON_SAMPLE, 70 | json.loads(serializer.toJSON(to_encode))) 71 | encoded = serializer.encode(to_encode, for_cloud_api=True) 72 | self.assertEqual(apitestcase.ENCODED_CLOUD_API_JSON_SAMPLE, encoded) 73 | pretty_encoded = serializer.encode( 74 | to_encode, is_compound=False, for_cloud_api=True) 75 | self.assertEqual(apitestcase.ENCODED_CLOUD_API_JSON_SAMPLE_PRETTY, 76 | pretty_encoded) 77 | 78 | encoded_json = serializer.toJSON(to_encode, for_cloud_api=True) 79 | decoded_encoded_json = json.loads(encoded_json) 80 | self.assertEqual(encoded, decoded_encoded_json) 81 | 82 | def testRepeats(self): 83 | """Verifies serialization finds and removes repeated values.""" 84 | test1 = ee.Image(5).mask(ee.Image(5)) # pylint: disable-msg=no-member 85 | expected1 = { 86 | 'type': 'CompoundValue', 87 | 'scope': [ 88 | ['0', { 89 | 'type': 'Invocation', 90 | 'arguments': { 91 | 'value': 5 92 | }, 93 | 'functionName': 'Image.constant' 94 | }], 95 | ['1', { 96 | 'type': 'Invocation', 97 | 'arguments': { 98 | 'image': { 99 | 'type': 'ValueRef', 100 | 'value': '0' 101 | }, 102 | 'mask': { 103 | 'type': 'ValueRef', 104 | 'value': '0' 105 | } 106 | }, 107 | 'functionName': 'Image.mask' 108 | }] 109 | ], 110 | 'value': { 111 | 'type': 'ValueRef', 112 | 'value': '1' 113 | } 114 | } 115 | self.assertEqual(expected1, json.loads(serializer.toJSON(test1))) 116 | expected_cloud = { 117 | 'values': { 118 | '0': { 119 | 'functionInvocationValue': { 120 | 'arguments': { 121 | 'image': { 122 | 'valueReference': '1' 123 | }, 124 | 'mask': { 125 | 'valueReference': '1' 126 | } 127 | }, 128 | 'functionName': 'Image.mask' 129 | } 130 | }, 131 | '1': { 132 | 'functionInvocationValue': { 133 | 'arguments': { 134 | 'value': { 135 | 'constantValue': 5 136 | } 137 | }, 138 | 'functionName': 'Image.constant' 139 | } 140 | } 141 | }, 142 | 'result': '0' 143 | } 144 | expected_cloud_pretty = { 145 | 'functionInvocationValue': { 146 | 'arguments': { 147 | 'image': { 148 | 'functionInvocationValue': { 149 | 'arguments': { 150 | 'value': { 151 | 'constantValue': 5 152 | } 153 | }, 154 | 'functionName': 'Image.constant' 155 | } 156 | }, 157 | 'mask': { 158 | 'functionInvocationValue': { 159 | 'arguments': { 160 | 'value': { 161 | 'constantValue': 5 162 | } 163 | }, 164 | 'functionName': 'Image.constant' 165 | } 166 | } 167 | }, 168 | 'functionName': 'Image.mask' 169 | } 170 | } 171 | self.assertEqual(expected_cloud, serializer.encode( 172 | test1, for_cloud_api=True)) 173 | self.assertEqual( 174 | expected_cloud_pretty, 175 | serializer.encode(test1, is_compound=False, for_cloud_api=True)) 176 | 177 | 178 | if __name__ == '__main__': 179 | unittest.main() 180 | -------------------------------------------------------------------------------- /ee/tests/string_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test for the ee.string module.""" 3 | 4 | 5 | 6 | import unittest 7 | 8 | import ee 9 | from ee import apitestcase 10 | 11 | 12 | class StringTest(apitestcase.ApiTestCase): 13 | 14 | def testString(self): 15 | """Verifies basic behavior of ee.String.""" 16 | bare_string = ee.String('foo') 17 | self.assertEqual('foo', bare_string.encode()) 18 | 19 | computed = ee.String('foo').cat('bar') 20 | self.assertTrue(isinstance(computed, ee.String)) 21 | self.assertEqual(ee.ApiFunction.lookup('String.cat'), computed.func) 22 | self.assertEqual({ 23 | 'string1': ee.String('foo'), 24 | 'string2': ee.String('bar') 25 | }, computed.args) 26 | 27 | # Casting a non-string ComputedObject. 28 | obj = ee.Number(1).add(1) 29 | s = ee.String(obj) 30 | self.assertTrue(isinstance(s, ee.String)) 31 | self.assertEqual(ee.ApiFunction.lookup('String'), s.func) 32 | self.assertEqual({'input': obj}, s.args) 33 | 34 | def testInternals(self): 35 | """Test eq(), ne() and hash().""" 36 | a = ee.String('one') 37 | b = ee.String('two') 38 | c = ee.String('one') 39 | 40 | self.assertEqual(a, a) 41 | self.assertNotEqual(a, b) 42 | self.assertEqual(a, c) 43 | self.assertNotEqual(b, c) 44 | self.assertNotEqual(hash(a), hash(b)) 45 | 46 | 47 | if __name__ == '__main__': 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httplib2==0.12.0 2 | google-auth-httplib2==0.0.3 3 | google-api-python-client==1.7.9 4 | google-auth==1.6.2 5 | --------------------------------------------------------------------------------