data.py 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930
  1. #!/usr/bin/env python
  2. """Singleton for the library's communication with the Earth Engine API."""
  3. # Using lowercase function naming to match the JavaScript names.
  4. # pylint: disable=g-bad-name
  5. # pylint: disable=g-bad-import-order
  6. import contextlib
  7. import json
  8. import platform
  9. import re
  10. import threading
  11. import uuid
  12. import sys
  13. from google_auth_httplib2 import AuthorizedHttp
  14. from . import __version__
  15. from . import _cloud_api_utils
  16. from . import deprecation
  17. from . import encodable
  18. from . import oauth
  19. from . import serializer
  20. import googleapiclient
  21. from . import ee_exception
  22. from google.oauth2.credentials import Credentials
  23. # OAuth2 credentials object. This may be set by ee.Initialize().
  24. _credentials = None
  25. # The base URL for all data calls. This is set by ee.Initialize().
  26. _api_base_url = None
  27. # The base URL for map tiles. This is set by ee.Initialize().
  28. _tile_base_url = None
  29. # The base URL for all Cloud API calls. This is set by ee.Initialize().
  30. _cloud_api_base_url = None
  31. # Google Cloud API key. This may be set by ee.Initialize().
  32. _cloud_api_key = None
  33. # A resource object for making Cloud API calls.
  34. _cloud_api_resource = None
  35. # A resource object for making Cloud API calls and receiving raw return types.
  36. _cloud_api_resource_raw = None
  37. # The default user project to use when making Cloud API calls.
  38. _cloud_api_user_project = None
  39. # The API client version number to send when making requests.
  40. _cloud_api_client_version = None
  41. # The http_transport to use.
  42. _http_transport = None
  43. # Whether the module has been initialized.
  44. _initialized = False
  45. # Sets the number of milliseconds to wait for a request before considering
  46. # it timed out. 0 means no limit.
  47. _deadline_ms = 0
  48. class _ThreadLocals(threading.local):
  49. def __init__(self):
  50. # pylint: disable=super-init-not-called
  51. # A function called when profile results are received from the server. Takes
  52. # the profile ID as an argument. None if profiling is disabled.
  53. #
  54. # This is a thread-local variable because the alternative is to add a
  55. # parameter to ee.data.send_, which would then have to be propagated from
  56. # the assorted API call functions (ee.data.getInfo, ee.data.getMapId, etc.),
  57. # and the user would have to modify each call to profile, rather than
  58. # enabling profiling as a wrapper around the entire program (with
  59. # ee.data.profiling, defined below).
  60. self.profile_hook = None
  61. _thread_locals = _ThreadLocals()
  62. # The HTTP header through which profile results are returned.
  63. # Lowercase because that's how httplib2 does things.
  64. _PROFILE_RESPONSE_HEADER_LOWERCASE = 'x-earth-engine-computation-profile'
  65. # The HTTP header through which profiling is requested when using the Cloud API.
  66. _PROFILE_REQUEST_HEADER = 'X-Earth-Engine-Computation-Profiling'
  67. # The HTTP header through which a user project override is provided.
  68. _USER_PROJECT_OVERRIDE_HEADER = 'X-Goog-User-Project'
  69. # The HTTP header used to indicate the version of the client library used.
  70. _API_CLIENT_VERSION_HEADER = 'X-Goog-Api-Client'
  71. # Optional HTTP header returned to display initialization-time messages.
  72. _INIT_MESSAGE_HEADER = 'x-earth-engine-init-message' # lowercase for httplib2
  73. # Maximum number of times to retry a rate-limited request.
  74. MAX_RETRIES = 5
  75. # Maximum time to wait before retrying a rate-limited request (in milliseconds).
  76. MAX_RETRY_WAIT = 120000
  77. # Base time (in ms) to wait when performing exponential backoff in request
  78. # retries.
  79. BASE_RETRY_WAIT = 1000
  80. # The default base URL for API calls.
  81. DEFAULT_API_BASE_URL = 'https://earthengine.googleapis.com/api'
  82. # The default base URL for media/tile calls.
  83. DEFAULT_TILE_BASE_URL = 'https://earthengine.googleapis.com'
  84. # The default base URL for Cloud API calls.
  85. DEFAULT_CLOUD_API_BASE_URL = 'https://earthengine.googleapis.com'
  86. # The default project to use for Cloud API calls.
  87. DEFAULT_CLOUD_API_USER_PROJECT = 'earthengine-legacy'
  88. # Asset types recognized by create_assets().
  89. ASSET_TYPE_FOLDER = 'Folder'
  90. ASSET_TYPE_IMAGE_COLL = 'ImageCollection'
  91. # Cloud API versions of the asset types.
  92. ASSET_TYPE_FOLDER_CLOUD = 'FOLDER'
  93. ASSET_TYPE_IMAGE_COLL_CLOUD = 'IMAGE_COLLECTION'
  94. # Max length of the above type names
  95. MAX_TYPE_LENGTH = len(ASSET_TYPE_IMAGE_COLL_CLOUD)
  96. # The maximum number of tasks to retrieve in each request to "/tasklist".
  97. _TASKLIST_PAGE_SIZE = 500
  98. def initialize(credentials=None,
  99. api_base_url=None,
  100. tile_base_url=None,
  101. cloud_api_base_url=None,
  102. cloud_api_key=None,
  103. project=None,
  104. http_transport=None):
  105. """Initializes the data module, setting credentials and base URLs.
  106. If any of the arguments are unspecified, they will keep their old values;
  107. the defaults if initialize() has never been called before.
  108. At least one of "credentials" and "cloud_api_key" must be provided. If both
  109. are provided, both will be used; in this case, the API key's project must
  110. match the credentials' project.
  111. Args:
  112. credentials: The OAuth2 credentials.
  113. api_base_url: The EarthEngine REST API endpoint.
  114. tile_base_url: The EarthEngine REST tile endpoint.
  115. cloud_api_base_url: The EarthEngine Cloud API endpoint.
  116. cloud_api_key: The API key to use with the Cloud API.
  117. project: The client project ID or number to use when making API calls.
  118. http_transport: The http transport to use
  119. """
  120. global _api_base_url, _tile_base_url, _credentials, _initialized
  121. global _cloud_api_base_url
  122. global _cloud_api_resource, _cloud_api_resource_raw, _cloud_api_key
  123. global _cloud_api_user_project, _http_transport
  124. global _cloud_api_client_version
  125. # If already initialized, only replace the explicitly specified parts.
  126. if credentials is not None:
  127. _credentials = credentials
  128. if api_base_url is not None:
  129. _api_base_url = api_base_url
  130. elif not _initialized:
  131. _api_base_url = DEFAULT_API_BASE_URL
  132. if tile_base_url is not None:
  133. _tile_base_url = tile_base_url
  134. elif not _initialized:
  135. _tile_base_url = DEFAULT_TILE_BASE_URL
  136. if cloud_api_key is not None:
  137. _cloud_api_key = cloud_api_key
  138. if cloud_api_base_url is not None:
  139. _cloud_api_base_url = cloud_api_base_url
  140. elif not _initialized:
  141. _cloud_api_base_url = DEFAULT_CLOUD_API_BASE_URL
  142. if __version__ is not None:
  143. version = __version__
  144. _cloud_api_client_version = version
  145. _http_transport = http_transport
  146. _install_cloud_api_resource()
  147. if project is not None:
  148. _cloud_api_user_project = project
  149. _cloud_api_utils.set_cloud_api_user_project(project)
  150. else:
  151. _cloud_api_utils.set_cloud_api_user_project(DEFAULT_CLOUD_API_USER_PROJECT)
  152. _initialized = True
  153. def get_persistent_credentials():
  154. """Read persistent credentials from ~/.config/earthengine.
  155. Raises EEException with helpful explanation if credentials don't exist.
  156. Returns:
  157. OAuth2Credentials built from persistently stored refresh_token
  158. """
  159. try:
  160. return Credentials(None, **oauth.get_credentials_arguments())
  161. except IOError:
  162. raise ee_exception.EEException(
  163. 'Please authorize access to your Earth Engine account by '
  164. 'running\n\nearthengine authenticate\n\n'
  165. 'in your command line, and then retry.')
  166. def reset():
  167. """Resets the data module, clearing credentials and custom base URLs."""
  168. global _api_base_url, _tile_base_url, _credentials, _initialized
  169. global _cloud_api_base_url
  170. global _cloud_api_resource, _cloud_api_resource_raw
  171. global _cloud_api_key, _http_transport
  172. _credentials = None
  173. _api_base_url = None
  174. _tile_base_url = None
  175. _cloud_api_base_url = None
  176. _cloud_api_key = None
  177. _cloud_api_resource = None
  178. _cloud_api_resource_raw = None
  179. _http_transport = None
  180. _initialized = False
  181. def _get_projects_path():
  182. """Returns the projects path to use for constructing a request."""
  183. if _cloud_api_user_project is not None:
  184. return 'projects/' + _cloud_api_user_project
  185. else:
  186. return 'projects/' + DEFAULT_CLOUD_API_USER_PROJECT
  187. def _install_cloud_api_resource():
  188. """Builds or rebuilds the Cloud API resource object, if needed."""
  189. global _cloud_api_resource, _cloud_api_resource_raw
  190. global _http_transport
  191. timeout = (_deadline_ms / 1000.0) or None
  192. _cloud_api_resource = _cloud_api_utils.build_cloud_resource(
  193. _cloud_api_base_url,
  194. credentials=_credentials,
  195. api_key=_cloud_api_key,
  196. timeout=timeout,
  197. headers_supplier=_make_request_headers,
  198. response_inspector=_handle_profiling_response,
  199. http_transport=_http_transport)
  200. _cloud_api_resource_raw = _cloud_api_utils.build_cloud_resource(
  201. _cloud_api_base_url,
  202. credentials=_credentials,
  203. api_key=_cloud_api_key,
  204. timeout=timeout,
  205. headers_supplier=_make_request_headers,
  206. response_inspector=_handle_profiling_response,
  207. http_transport=_http_transport,
  208. raw=True)
  209. def _get_cloud_api_resource():
  210. if _cloud_api_resource is None:
  211. raise ee_exception.EEException(
  212. 'Earth Engine client library not initialized. Run `ee.Initialize()`')
  213. return _cloud_api_resource
  214. def _make_request_headers():
  215. """Adds headers based on client context."""
  216. headers = {}
  217. client_version_header_values = []
  218. if _cloud_api_client_version is not None:
  219. client_version_header_values.append('ee-py/' + _cloud_api_client_version)
  220. client_version_header_values.append('python/' + platform.python_version())
  221. headers[_API_CLIENT_VERSION_HEADER] = ' '.join(client_version_header_values)
  222. if _thread_locals.profile_hook:
  223. headers[_PROFILE_REQUEST_HEADER] = '1'
  224. if _cloud_api_user_project is not None:
  225. headers[_USER_PROJECT_OVERRIDE_HEADER] = _cloud_api_user_project
  226. if headers:
  227. return headers
  228. return None
  229. def _handle_profiling_response(response):
  230. """Handles profiling annotations on Cloud API responses."""
  231. # Call the profile hook if present. Note that this is done before we handle
  232. # the content, so that profiles are reported even if the response is an error.
  233. if (_thread_locals.profile_hook and
  234. _PROFILE_RESPONSE_HEADER_LOWERCASE in response):
  235. _thread_locals.profile_hook(response[_PROFILE_RESPONSE_HEADER_LOWERCASE])
  236. def _execute_cloud_call(call, num_retries=MAX_RETRIES):
  237. """Executes a Cloud API call and translates errors to EEExceptions.
  238. Args:
  239. call: The Cloud API call, with all parameters set, ready to have execute()
  240. called on it.
  241. num_retries: How many times retryable failures should be retried.
  242. Returns:
  243. The value returned by executing that call.
  244. Raises:
  245. EEException if the call fails.
  246. """
  247. try:
  248. return call.execute(num_retries=num_retries)
  249. except googleapiclient.errors.HttpError as e:
  250. raise _translate_cloud_exception(e)
  251. def _translate_cloud_exception(http_error):
  252. """Translates a Cloud API exception into an EEException.
  253. Args:
  254. http_error: A googleapiclient.errors.HttpError.
  255. Returns:
  256. An EEException bearing the error message from http_error.
  257. """
  258. # The only sane way to get a message out of an HttpError is to use a protected
  259. # method.
  260. return ee_exception.EEException(http_error._get_reason()) # pylint: disable=protected-access
  261. def _maybe_populate_workload_tag(body):
  262. """Populates the workload tag on the request body passed in if applicable.
  263. Defaults to the workload tag set by ee.data.setWorkloadTag() or related
  264. methods. A workload tag already set on the body takes precedence. The workload
  265. tag will not be set if it's an empty string.
  266. Args:
  267. body: The request body.
  268. """
  269. if 'workloadTag' not in body:
  270. workload_tag = getWorkloadTag()
  271. if workload_tag:
  272. body['workloadTag'] = workload_tag
  273. elif not body['workloadTag']:
  274. del body['workloadTag']
  275. def setCloudApiKey(cloud_api_key):
  276. """Sets the Cloud API key parameter ("api_key") for all requests."""
  277. global _cloud_api_key
  278. _cloud_api_key = cloud_api_key
  279. _install_cloud_api_resource()
  280. def setCloudApiUserProject(cloud_api_user_project):
  281. global _cloud_api_user_project
  282. _cloud_api_user_project = cloud_api_user_project
  283. _cloud_api_utils.set_cloud_api_user_project(_cloud_api_user_project)
  284. def setDeadline(milliseconds):
  285. """Sets the timeout length for API requests.
  286. Args:
  287. milliseconds: The number of milliseconds to wait for a request
  288. before considering it timed out. 0 means no limit.
  289. """
  290. global _deadline_ms
  291. _deadline_ms = milliseconds
  292. _install_cloud_api_resource()
  293. @contextlib.contextmanager
  294. def profiling(hook):
  295. # pylint: disable=g-doc-return-or-yield
  296. """Returns a context manager which enables or disables profiling.
  297. If hook is not None, enables profiling for all API calls in its scope and
  298. calls the hook function with all resulting profile IDs. If hook is null,
  299. disables profiling (or leaves it disabled).
  300. Args:
  301. hook: A function of one argument which is called with each profile
  302. ID obtained from API calls, just before the API call returns.
  303. """
  304. saved_hook = _thread_locals.profile_hook
  305. _thread_locals.profile_hook = hook
  306. try:
  307. yield
  308. finally:
  309. _thread_locals.profile_hook = saved_hook
  310. @deprecation.Deprecated('Use getAsset')
  311. def getInfo(asset_id):
  312. """Load info for an asset, given an asset id.
  313. Args:
  314. asset_id: The asset to be retrieved.
  315. Returns:
  316. The value call results, or None if the asset does not exist.
  317. """
  318. # Don't use getAsset as it will translate the exception, and we need
  319. # to handle 404s specially.
  320. try:
  321. return _get_cloud_api_resource().projects().assets().get(
  322. name=_cloud_api_utils.convert_asset_id_to_asset_name(asset_id),
  323. prettyPrint=False).execute(num_retries=MAX_RETRIES)
  324. except googleapiclient.errors.HttpError as e:
  325. if e.resp.status == 404:
  326. return None
  327. else:
  328. raise _translate_cloud_exception(e)
  329. def getAsset(asset_id):
  330. """Loads info for an asset, given an asset id.
  331. Args:
  332. asset_id: The asset to be retrieved.
  333. Returns:
  334. The asset's information, as an EarthEngineAsset.
  335. """
  336. return _execute_cloud_call(_get_cloud_api_resource().projects().assets().get(
  337. name=_cloud_api_utils.convert_asset_id_to_asset_name(asset_id),
  338. prettyPrint=False))
  339. @deprecation.Deprecated('Use listAssets or listImages')
  340. def getList(params):
  341. """Get a list of contents for a collection asset.
  342. Args:
  343. params: An object containing request parameters with the possible values:
  344. id - (string) The asset id of the collection to list, required.
  345. starttime - (number) Start time, in msec since the epoch.
  346. endtime - (number) End time, in msec since the epoch.
  347. Returns:
  348. The list call results.
  349. """
  350. result = listAssets(
  351. _cloud_api_utils.convert_get_list_params_to_list_assets_params(params))
  352. result = _cloud_api_utils.convert_list_assets_result_to_get_list_result(
  353. result)
  354. return result
  355. def listImages(params):
  356. """Returns the images in an image collection or folder.
  357. Args:
  358. params: An object containing request parameters with the following possible
  359. values, all but 'parent` are optional:
  360. parent - (string) The ID of the image collection to list, required.
  361. pageSize - (string) The number of results to return. Defaults to 1000.
  362. pageToken - (string) The token page of results to return.
  363. startTime - (ISO 8601 string): The minimum start time (inclusive).
  364. endTime - (ISO 8601 string): The maximum end time (exclusive).
  365. region - (GeoJSON or WKT string): A region to filter on.
  366. properties - (list of strings): A list of property filters to apply, for
  367. example, ["classification=urban", "size>=2"].
  368. filter - (string) An additional filter query to apply. Example query:
  369. `properties.my_property>=1 AND properties.my_property<2 AND
  370. startTime >= "2019-01-01T00:00:00.000Z" AND
  371. endTime < "2020-01-01T00:00:00.000Z" AND
  372. intersects("{'type':'Point','coordinates':[0,0]}")`
  373. See https://google.aip.dev/160 for how to construct a query.
  374. view - (string) Specifies how much detail is returned in the list. Either
  375. "FULL" (default) for all image properties or "BASIC".
  376. """
  377. images = {'images': []}
  378. assets = listAssets(
  379. _cloud_api_utils.convert_list_images_params_to_list_assets_params(params))
  380. images['images'].extend(assets.get('assets', []))
  381. return images
  382. def listAssets(params):
  383. """Returns the assets in a folder.
  384. Args:
  385. params: An object containing request parameters with the following possible
  386. values, all but 'parent` are optional:
  387. parent - (string) The ID of the collection or folder to list, required.
  388. pageSize - (string) The number of results to return. Defaults to 1000.
  389. pageToken - (string) The token page of results to return.
  390. filter - (string) An additional filter query to apply. Example query:
  391. '''properties.my_property>=1 AND properties.my_property<2 AND
  392. startTime >= "2019-01-01T00:00:00.000Z" AND
  393. endTime < "2020-01-01T00:00:00.000Z" AND
  394. intersects("{'type':'Point','coordinates':[0,0]}")'''
  395. See https://google.aip.dev/160 for how to construct a query.
  396. view - (string) Specifies how much detail is returned in the list. Either
  397. "FULL" (default) for all image properties or "BASIC".
  398. """
  399. assets = {'assets': []}
  400. if 'parent' in params:
  401. params['parent'] = _cloud_api_utils.convert_asset_id_to_asset_name(
  402. params['parent'])
  403. if 'parent' in params and _cloud_api_utils.is_asset_root(params['parent']):
  404. # If the asset name is 'projects/my-project/assets' we assume a user
  405. # wants to list their cloud assets, to do this we call the alternative
  406. # listAssets method and remove the trailing '/assets/?'
  407. params['parent'] = re.sub('/assets/?$', '', params['parent'])
  408. cloud_resource_root = _get_cloud_api_resource().projects()
  409. else:
  410. cloud_resource_root = _get_cloud_api_resource().projects().assets()
  411. request = cloud_resource_root.listAssets(**params)
  412. while request is not None:
  413. response = _execute_cloud_call(request)
  414. assets['assets'].extend(response.get('assets', []))
  415. request = cloud_resource_root.listAssets_next(request, response)
  416. # We currently treat pageSize as a cap on the results, if this param was
  417. # provided we should break fast and not return more than the asked for
  418. # amount.
  419. if 'pageSize' in params:
  420. break
  421. return assets
  422. def listBuckets(project=None):
  423. if project is None:
  424. project = _get_projects_path()
  425. return _execute_cloud_call(
  426. _get_cloud_api_resource().projects().listAssets(parent=project))
  427. def getMapId(params):
  428. """Get a Map ID for a given asset.
  429. Args:
  430. params: An object containing visualization options with the
  431. following possible values:
  432. image - The image to render, as an Image or a JSON string.
  433. The JSON string format is deprecated.
  434. version - (number) Version number of image (or latest).
  435. bands - (comma-separated strings) Comma-delimited list of
  436. band names to be mapped to RGB.
  437. min - (comma-separated numbers) Value (or one per band)
  438. to map onto 00.
  439. max - (comma-separated numbers) Value (or one per band)
  440. to map onto FF.
  441. gain - (comma-separated numbers) Gain (or one per band)
  442. to map onto 00-FF.
  443. bias - (comma-separated numbers) Offset (or one per band)
  444. to map onto 00-FF.
  445. gamma - (comma-separated numbers) Gamma correction
  446. factor (or one per band).
  447. palette - (comma-separated strings) A string of comma-separated
  448. CSS-style color strings (single-band previews only). For example,
  449. 'FF0000,000000'.
  450. format - (string) The desired map tile image format. If omitted, one is
  451. chosen automatically. Can be 'jpg' (does not support transparency)
  452. or 'png' (supports transparency).
  453. Returns:
  454. A map ID dictionary containing:
  455. - "mapid" and optional "token" strings: these identify the map.
  456. - "tile_fetcher": a TileFetcher which can be used to fetch the tile
  457. images, or to get a format for the tile URLs.
  458. """
  459. if isinstance(params['image'], str):
  460. raise ee_exception.EEException('Image as JSON string not supported.')
  461. if 'version' in params:
  462. raise ee_exception.EEException(
  463. 'Image version specification not supported.')
  464. request = {
  465. 'expression':
  466. serializer.encode(params['image'], for_cloud_api=True),
  467. 'fileFormat':
  468. _cloud_api_utils.convert_to_image_file_format(params.get('format')),
  469. 'bandIds':
  470. _cloud_api_utils.convert_to_band_list(params.get('bands')),
  471. }
  472. # Only add visualizationOptions to the request if it's non-empty, as
  473. # specifying it affects server behaviour.
  474. visualizationOptions = _cloud_api_utils.convert_to_visualization_options(
  475. params)
  476. if visualizationOptions:
  477. request['visualizationOptions'] = visualizationOptions
  478. # Returns only the `name` field, otherwise it echoes the entire request, which
  479. # might be large.
  480. queryParams = {
  481. 'fields': 'name',
  482. 'body': request,
  483. }
  484. _maybe_populate_workload_tag(queryParams)
  485. result = _execute_cloud_call(
  486. _get_cloud_api_resource().projects().maps().create(
  487. parent=_get_projects_path(), **queryParams))
  488. map_name = result['name']
  489. url_format = '%s/%s/%s/tiles/{z}/{x}/{y}' % (
  490. _tile_base_url, _cloud_api_utils.VERSION, map_name)
  491. if _cloud_api_key:
  492. url_format += '?key=%s' % _cloud_api_key
  493. return {'mapid': map_name, 'token': '',
  494. 'tile_fetcher': TileFetcher(url_format, map_name=map_name)}
  495. def getFeatureViewTilesKey(params):
  496. """Get a tiles key for a given map or asset.
  497. Args:
  498. params: An object containing parameters with the following possible values:
  499. assetId - The asset ID for which to obtain a tiles key.
  500. visParams - The visualization parameters for this layer.
  501. Returns:
  502. A dictionary containing:
  503. - "token" string: this identifies the FeatureView.
  504. """
  505. request = {
  506. 'asset':
  507. _cloud_api_utils.convert_asset_id_to_asset_name(
  508. params.get('assetId'))
  509. }
  510. # Only include visParams if it's non-empty.
  511. if params.get('visParams'):
  512. request['visualizationExpression'] = serializer.encode(
  513. params.get('visParams'), for_cloud_api=True)
  514. # Returns only the `name` field, otherwise it echoes the entire request, which
  515. # might be large.
  516. result = _execute_cloud_call(
  517. _get_cloud_api_resource().projects().featureView().create(
  518. parent=_get_projects_path(), fields='name', body=request))
  519. name = result['name']
  520. version = _cloud_api_utils.VERSION
  521. format_tile_url = (
  522. lambda x, y, z: f'{_tile_base_url}/{version}/{name}/tiles/{z}/{x}/{y}')
  523. token = name.rsplit('/', 1).pop()
  524. return {
  525. 'token': token,
  526. 'formatTileUrl': format_tile_url,
  527. }
  528. def listFeatures(params):
  529. """List features for a given table or FeatureView asset.
  530. Args:
  531. params: An object containing parameters with the following possible values:
  532. assetId - The asset ID for which to list features.
  533. pageSize - An optional max number of results per page, default is 1000.
  534. pageToken - An optional token identifying a new page of results the server
  535. should return, usually taken from the response object.
  536. region - If present, a geometry defining a query region, specified as a
  537. GeoJSON geometry string (see RFC 7946).
  538. filter - If present, specifies additional simple property filters
  539. (see https://google.aip.dev/160).
  540. Returns:
  541. A dictionary containing:
  542. - "type": always "FeatureCollection" marking this object as a GeoJSON
  543. feature collection.
  544. - "features": a list of GeoJSON features.
  545. - "next_page_token": A token to retrieve the next page of results in a
  546. subsequent call to this function.
  547. """
  548. params = params.copy()
  549. params['asset'] = _cloud_api_utils.convert_asset_id_to_asset_name(
  550. params.get('assetId'))
  551. del params['assetId']
  552. return _execute_cloud_call(
  553. _get_cloud_api_resource().projects().assets().listFeatures(**params))
  554. def getTileUrl(mapid, x, y, z):
  555. """Generate a URL for map tiles from a Map ID and coordinates.
  556. Args:
  557. mapid: The Map ID to generate tiles for, a dictionary returned
  558. by getMapId.
  559. x: The tile x coordinate.
  560. y: The tile y coordinate.
  561. z: The tile zoom level.
  562. Returns:
  563. The tile URL.
  564. """
  565. return mapid['tile_fetcher'].format_tile_url(x, y, z)
  566. class TileFetcher(object):
  567. """A helper class to fetch image tiles."""
  568. def __init__(self, url_format, map_name=None):
  569. self._url_format = url_format
  570. self._map_name = map_name
  571. @property
  572. def url_format(self):
  573. """Gets the URL format for this tile fetcher.
  574. Returns:
  575. A format string with {x}, {y}, and {z} placeholders.
  576. If you are using the Cloud API, and have not provided an API
  577. key, then this URL will require authorization. Use the credentials
  578. provided to ee.Initialize() to provide this authorization. Alternatively,
  579. use "fetch_tile" to fetch the tile data, which will handle the
  580. authorization for you.
  581. """
  582. return self._url_format
  583. def format_tile_url(self, x, y, z):
  584. """Generates the URL for a particular tile.
  585. Args:
  586. x: The tile x coordinate.
  587. y: The tile y coordinate.
  588. z: The tile zoom level.
  589. Returns:
  590. The tile's URL.
  591. """
  592. width = 2**z
  593. x %= width
  594. if x < 0:
  595. x += width
  596. return self.url_format.format(x=x, y=y, z=z)
  597. def fetch_tile(self, x, y, z):
  598. """Fetches the map tile specified by (x, y, z).
  599. This method uses any credentials that were specified to ee.Initialize().
  600. Args:
  601. x: The tile x coordinate.
  602. y: The tile y coordinate.
  603. z: The tile zoom level.
  604. Returns:
  605. The map tile image data bytes.
  606. Raises:
  607. EEException if the fetch fails.
  608. """
  609. return _execute_cloud_call(
  610. _cloud_api_resource_raw.projects().maps().tiles().get(
  611. parent=self._map_name, x=x, y=y, zoom=z,
  612. ), num_retries=MAX_RETRIES
  613. )
  614. def computeValue(obj):
  615. """Sends a request to compute a value.
  616. Args:
  617. obj: A ComputedObject whose value is desired.
  618. Returns:
  619. The result of evaluating that object on the server.
  620. """
  621. body = {'expression': serializer.encode(obj, for_cloud_api=True)}
  622. _maybe_populate_workload_tag(body)
  623. return _execute_cloud_call(
  624. _get_cloud_api_resource().projects().value().compute(
  625. body=body,
  626. project=_get_projects_path(),
  627. prettyPrint=False))['result']
  628. @deprecation.Deprecated('Use getThumbId and makeThumbUrl')
  629. def getThumbnail(params, thumbType=None):
  630. """Get a Thumbnail for a given asset.
  631. Args:
  632. params: Parameters identical to getMapId, plus:
  633. size - (a number or pair of numbers in format WIDTHxHEIGHT) Maximum
  634. dimensions of the thumbnail to render, in pixels. If only one number
  635. is passed, it is used as the maximum, and the other dimension is
  636. computed by proportional scaling.
  637. region - (E,S,W,N or GeoJSON) Geospatial region of the image
  638. to render. By default, the whole image.
  639. format - (string) Either 'png' (default) or 'jpg'.
  640. thumbType: Thumbnail type to get. Only valid values are
  641. 'video' or 'filmstrip' otherwise the request is treated as a
  642. regular thumbnail.
  643. Returns:
  644. A thumbnail image as raw PNG data.
  645. """
  646. thumbid = params['image'].getThumbId(params)['thumbid']
  647. if thumbType == 'video':
  648. return _execute_cloud_call(
  649. _cloud_api_resource_raw.projects().videoThumbnails().getPixels(
  650. name=thumbid
  651. ), num_retries=MAX_RETRIES
  652. )
  653. elif thumbType == 'filmstrip':
  654. return _execute_cloud_call(
  655. _cloud_api_resource_raw.projects().filmstripThumbnails().getPixels(
  656. name=thumbid
  657. ), num_retries=MAX_RETRIES
  658. )
  659. else:
  660. return _execute_cloud_call(
  661. _cloud_api_resource_raw.projects().thumbnails().getPixels(
  662. name=thumbid
  663. ), num_retries=MAX_RETRIES
  664. )
  665. def getThumbId(params, thumbType=None):
  666. """Get a Thumbnail ID for a given asset.
  667. Args:
  668. params: Parameters identical to getMapId, plus:
  669. size - (a number or pair of numbers in format WIDTHxHEIGHT) Maximum
  670. dimensions of the thumbnail to render, in pixels. If only one number
  671. is passed, it is used as the maximum, and the other dimension is
  672. computed by proportional scaling.
  673. region - (E,S,W,N or GeoJSON) Geospatial region of the image
  674. to render. By default, the whole image.
  675. format - (string) Either 'png' (default) or 'jpg'.
  676. thumbType: Type of thumbnail to create an ID for, the values
  677. 'video' or 'filmstrip' will create filmstrip/video ids.
  678. Returns:
  679. A dictionary containing "thumbid" and "token" strings, which identify the
  680. thumbnail.
  681. """
  682. # We only really support accessing this method via ee.Image.getThumbURL,
  683. # which folds almost all the parameters into the Image itself.
  684. if isinstance(params['image'], str):
  685. raise ee_exception.EEException('Image as JSON string not supported.')
  686. if 'version' in params:
  687. raise ee_exception.EEException(
  688. 'Image version specification not supported.')
  689. if 'size' in params:
  690. raise ee_exception.EEException(
  691. '"size" not supported. Use "dimensions" and ee.Image.getThumbURL.')
  692. if 'region' in params:
  693. raise ee_exception.EEException(
  694. '"region" not supported in call to ee.data.getThumbId. Use '
  695. 'ee.Image.getThumbURL.')
  696. request = {
  697. 'expression':
  698. serializer.encode(params['image'], for_cloud_api=True),
  699. 'fileFormat':
  700. _cloud_api_utils.convert_to_image_file_format(params.get('format')),
  701. }
  702. # Only add visualizationOptions to the request if it's non-empty, as
  703. # specifying it affects server behaviour.
  704. visualizationOptions = _cloud_api_utils.convert_to_visualization_options(
  705. params)
  706. if visualizationOptions:
  707. request['visualizationOptions'] = visualizationOptions
  708. # Returns only the `name` field, otherwise it echoes the entire request, which
  709. # might be large.
  710. queryParams = {
  711. 'fields': 'name',
  712. 'body': request,
  713. }
  714. _maybe_populate_workload_tag(queryParams)
  715. if thumbType == 'video':
  716. if 'framesPerSecond' in params:
  717. request['videoOptions'] = {
  718. 'framesPerSecond': params.get('framesPerSecond')
  719. }
  720. result = _execute_cloud_call(
  721. _get_cloud_api_resource().projects().videoThumbnails().create(
  722. parent=_get_projects_path(), **queryParams))
  723. elif thumbType == 'filmstrip':
  724. # Currently only 'VERTICAL' thumbnails are supported.
  725. request['orientation'] = 'VERTICAL'
  726. result = _execute_cloud_call(
  727. _get_cloud_api_resource().projects().filmstripThumbnails().create(
  728. parent=_get_projects_path(), **queryParams))
  729. else:
  730. request['filenamePrefix'] = params.get('name')
  731. request['bandIds'] = _cloud_api_utils.convert_to_band_list(
  732. params.get('bands'))
  733. result = _execute_cloud_call(
  734. _get_cloud_api_resource().projects().thumbnails().create(
  735. parent=_get_projects_path(), **queryParams))
  736. return {'thumbid': result['name'], 'token': ''}
  737. def makeThumbUrl(thumbId):
  738. """Create a thumbnail URL from the given thumbid and token.
  739. Args:
  740. thumbId: An object containing a thumbnail thumbid and token.
  741. Returns:
  742. A URL from which the thumbnail can be obtained.
  743. """
  744. url = '%s/%s/%s:getPixels' % (_tile_base_url, _cloud_api_utils.VERSION,
  745. thumbId['thumbid'])
  746. if _cloud_api_key:
  747. url += '?key=%s' % _cloud_api_key
  748. return url
  749. def getDownloadId(params):
  750. """Get a Download ID.
  751. Args:
  752. params: An object containing visualization options with the following
  753. possible values:
  754. image - The image to download.
  755. - name: a base name to use when constructing filenames. Only applicable
  756. when format is "ZIPPED_GEO_TIFF" (default) or filePerBand is true.
  757. Defaults to the image id (or "download" for computed images) when
  758. format is "ZIPPED_GEO_TIFF" or filePerBand is true, otherwise a
  759. random character string is generated. Band names are appended when
  760. filePerBand is true.
  761. - bands: a description of the bands to download. Must be an array of
  762. band names or an array of dictionaries, each with the
  763. following keys:
  764. + id: the name of the band, a string, required.
  765. + crs: an optional CRS string defining the band projection.
  766. + crs_transform: an optional array of 6 numbers specifying an affine
  767. transform from the specified CRS, in the order:
  768. [xScale, yShearing, xShearing, yScale, xTranslation, yTranslation]
  769. + dimensions: an optional array of two integers defining the width and
  770. height to which the band is cropped.
  771. + scale: an optional number, specifying the scale in meters of the
  772. band; ignored if crs and crs_transform are specified.
  773. - crs: a default CRS string to use for any bands that do not explicitly
  774. specify one.
  775. - crs_transform: a default affine transform to use for any bands that do
  776. not specify one, of the same format as the crs_transform of bands.
  777. - dimensions: default image cropping dimensions to use for any bands
  778. that do not specify them.
  779. - scale: a default scale to use for any bands that do not specify one;
  780. ignored if crs and crs_transform is specified.
  781. - region: a polygon specifying a region to download; ignored if crs
  782. and crs_transform are specified.
  783. - filePerBand: whether to produce a separate GeoTIFF per band (boolean).
  784. Defaults to true. If false, a single GeoTIFF is produced and all
  785. band-level transformations will be ignored.
  786. - format: the download format. One of:
  787. "ZIPPED_GEO_TIFF" (GeoTIFF file(s) wrapped in a zip file, default),
  788. "GEO_TIFF" (GeoTIFF file), "NPY" (NumPy binary format).
  789. If "GEO_TIFF" or "NPY", filePerBand and all band-level
  790. transformations will be ignored. Loading a NumPy output results in
  791. a structured array.
  792. - id: deprecated, use image parameter.
  793. Returns:
  794. A dict containing a docid and token.
  795. """
  796. params = params.copy()
  797. # Previously, the docs required an image ID parameter that was changed
  798. # to image. Due to the circular dependency, we raise an error and ask the
  799. # user to supply an ee.Image directly.
  800. if 'id' in params:
  801. raise ee_exception.EEException('Image ID string is not supported. '
  802. 'Construct an image with the ID '
  803. '(e.g. ee.Image(id)) and use '
  804. 'ee.Image.getDownloadURL instead.')
  805. if 'image' not in params:
  806. raise ee_exception.EEException('Missing image parameter.')
  807. if isinstance(params['image'], str):
  808. raise ee_exception.EEException('Image as JSON string not supported.')
  809. params.setdefault('filePerBand', True)
  810. params.setdefault(
  811. 'format', 'ZIPPED_GEO_TIFF_PER_BAND'
  812. if params['filePerBand'] else 'ZIPPED_GEO_TIFF')
  813. if 'region' in params and ('scale' in params or 'crs_transform' in params
  814. ) and 'dimensions' in params:
  815. raise ee_exception.EEException(
  816. 'Cannot specify (bounding region, crs_transform/scale, dimensions) '
  817. 'simultaneously.'
  818. )
  819. bands = None
  820. if 'bands' in params:
  821. bands = params['bands']
  822. if isinstance(bands, str):
  823. bands = _cloud_api_utils.convert_to_band_list(bands)
  824. if not isinstance(bands, list):
  825. raise ee_exception.EEException('Bands parameter must be a list.')
  826. if all(isinstance(band, str) for band in bands):
  827. # Support expressing the bands list as a list of strings.
  828. bands = [{'id': band} for band in bands]
  829. if not all('id' in band for band in bands):
  830. raise ee_exception.EEException('Each band dictionary must have an id.')
  831. params['bands'] = bands
  832. request = {
  833. 'expression':
  834. serializer.encode(
  835. params['image']._build_download_id_image(params), # pylint: disable=protected-access
  836. for_cloud_api=True),
  837. 'fileFormat':
  838. _cloud_api_utils.convert_to_image_file_format(params.get('format')),
  839. }
  840. request['filenamePrefix'] = params.get('name')
  841. if bands:
  842. request['bandIds'] = _cloud_api_utils.convert_to_band_list(
  843. [band['id'] for band in bands])
  844. # Returns only the `name` field, otherwise it echoes the entire request, which
  845. # might be large.
  846. queryParams = {
  847. 'fields': 'name',
  848. 'body': request,
  849. }
  850. _maybe_populate_workload_tag(queryParams)
  851. result = _execute_cloud_call(
  852. _get_cloud_api_resource().projects().thumbnails().create(
  853. parent=_get_projects_path(), **queryParams))
  854. return {'docid': result['name'], 'token': ''}
  855. def makeDownloadUrl(downloadId):
  856. """Create a download URL from the given docid and token.
  857. Args:
  858. downloadId: An object containing a download docid and token.
  859. Returns:
  860. A URL from which the download can be obtained.
  861. """
  862. return '%s/%s/%s:getPixels' % (_tile_base_url, _cloud_api_utils.VERSION,
  863. downloadId['docid'])
  864. def getTableDownloadId(params):
  865. """Get a Download ID.
  866. Args:
  867. params: An object containing table download options with the following
  868. possible values:
  869. table - The feature collection to download.
  870. format - The download format, CSV, JSON, KML, KMZ, or TF_RECORD.
  871. selectors - Comma separated string of selectors that can be used to
  872. determine which attributes will be downloaded.
  873. filename - The name of the file that will be downloaded.
  874. Returns:
  875. A dict containing a docid and token.
  876. Raises:
  877. KeyError: if "table" is not specified.
  878. """
  879. if 'table' not in params:
  880. raise KeyError('"table" must be specified.')
  881. table = params['table']
  882. selectors = None
  883. if 'selectors' in params:
  884. selectors = params['selectors']
  885. if isinstance(selectors, str):
  886. selectors = selectors.split(',')
  887. filename = None
  888. if 'filename' in params:
  889. filename = params['filename']
  890. request = {
  891. 'expression': serializer.encode(table, for_cloud_api=True),
  892. 'fileFormat':
  893. _cloud_api_utils.convert_to_table_file_format(params.get('format')),
  894. 'selectors': selectors,
  895. 'filename': filename,
  896. }
  897. # Returns only the `name` field, otherwise it echoes the entire request, which
  898. # might be large.
  899. queryParams = {
  900. 'fields': 'name',
  901. 'body': request,
  902. }
  903. _maybe_populate_workload_tag(queryParams)
  904. result = _execute_cloud_call(
  905. _get_cloud_api_resource().projects().tables().create(
  906. parent=_get_projects_path(), **queryParams))
  907. return {'docid': result['name'], 'token': ''}
  908. def makeTableDownloadUrl(downloadId):
  909. """Create a table download URL from a docid and token.
  910. Args:
  911. downloadId: A table download id and token.
  912. Returns:
  913. A Url from which the download can be obtained.
  914. """
  915. return '%s/%s/%s:getFeatures' % (
  916. _tile_base_url, _cloud_api_utils.VERSION, downloadId['docid'])
  917. def getAlgorithms():
  918. """Get the list of algorithms.
  919. Returns:
  920. The dictionary of algorithms. Each algorithm is a dictionary containing
  921. the following fields:
  922. "description" - (string) A text description of the algorithm.
  923. "returns" - (string) The return type of the algorithm.
  924. "args" - An array of arguments. Each argument specifies the following:
  925. "name" - (string) The name of the argument.
  926. "description" - (string) A text description of the argument.
  927. "type" - (string) The type of the argument.
  928. "optional" - (boolean) Whether the argument is optional or not.
  929. "default" - A representation of the default value if the argument
  930. is not specified.
  931. """
  932. try:
  933. call = _get_cloud_api_resource().projects().algorithms().list(
  934. parent=_get_projects_path(), prettyPrint=False)
  935. except TypeError:
  936. call = _get_cloud_api_resource().projects().algorithms().list(
  937. project=_get_projects_path(), prettyPrint=False)
  938. def inspect(response):
  939. if _INIT_MESSAGE_HEADER in response:
  940. print(
  941. '*** Earth Engine ***',
  942. response[_INIT_MESSAGE_HEADER],
  943. file=sys.stderr)
  944. call.add_response_callback(inspect)
  945. return _cloud_api_utils.convert_algorithms(_execute_cloud_call(call))
  946. def createAsset(
  947. value,
  948. opt_path=None,
  949. opt_properties=None):
  950. """Creates an asset from a JSON value.
  951. To create an empty image collection or folder, pass in a "value" object
  952. with a "type" key whose value is "ImageCollection" or "Folder".
  953. If you are using the Cloud API, use "IMAGE_COLLECTION" or "FOLDER".
  954. Args:
  955. value: An object describing the asset to create or a JSON string
  956. with the already-serialized value for the new asset.
  957. opt_path: An optional desired ID, including full path.
  958. opt_properties: The keys and values of the properties to set
  959. on the created asset.
  960. Returns:
  961. A description of the saved asset, including a generated ID.
  962. """
  963. if not isinstance(value, dict):
  964. raise ee_exception.EEException('Asset cannot be specified as string.')
  965. asset = value.copy()
  966. if 'name' not in asset:
  967. if not opt_path:
  968. raise ee_exception.EEException(
  969. 'Either asset name or opt_path must be specified.')
  970. asset['name'] = _cloud_api_utils.convert_asset_id_to_asset_name(opt_path)
  971. if 'properties' not in asset and opt_properties:
  972. asset['properties'] = opt_properties
  973. asset['type'] = _cloud_api_utils.convert_asset_type_for_create_asset(
  974. asset['type'])
  975. parent, asset_id = _cloud_api_utils.split_asset_name(asset.pop('name'))
  976. return _execute_cloud_call(
  977. _get_cloud_api_resource().projects().assets().create(
  978. parent=parent,
  979. assetId=asset_id,
  980. body=asset,
  981. prettyPrint=False))
  982. def copyAsset(sourceId, destinationId, allowOverwrite=False
  983. ):
  984. """Copies the asset from sourceId into destinationId.
  985. Args:
  986. sourceId: The ID of the asset to copy.
  987. destinationId: The ID of the new asset created by copying.
  988. allowOverwrite: If True, allows overwriting an existing asset.
  989. """
  990. request = {
  991. 'destinationName':
  992. _cloud_api_utils.convert_asset_id_to_asset_name(destinationId),
  993. 'overwrite':
  994. allowOverwrite
  995. }
  996. _execute_cloud_call(_get_cloud_api_resource().projects().assets().copy(
  997. sourceName=_cloud_api_utils.convert_asset_id_to_asset_name(sourceId),
  998. body=request))
  999. return
  1000. def renameAsset(sourceId, destinationId):
  1001. """Renames the asset from sourceId to destinationId.
  1002. Args:
  1003. sourceId: The ID of the asset to rename.
  1004. destinationId: The new ID of the asset.
  1005. """
  1006. _execute_cloud_call(_get_cloud_api_resource().projects().assets().move(
  1007. sourceName=_cloud_api_utils.convert_asset_id_to_asset_name(sourceId),
  1008. body={
  1009. 'destinationName':
  1010. _cloud_api_utils.convert_asset_id_to_asset_name(destinationId)
  1011. }))
  1012. return
  1013. def deleteAsset(assetId):
  1014. """Deletes the asset with the given id.
  1015. Args:
  1016. assetId: The ID of the asset to delete.
  1017. """
  1018. _execute_cloud_call(_get_cloud_api_resource().projects().assets().delete(
  1019. name=_cloud_api_utils.convert_asset_id_to_asset_name(assetId)))
  1020. return
  1021. def newTaskId(count=1):
  1022. """Generate an ID for a long-running task.
  1023. Args:
  1024. count: Optional count of IDs to generate, one by default.
  1025. Returns:
  1026. A list containing generated ID strings.
  1027. """
  1028. return [str(uuid.uuid4()) for _ in range(count)]
  1029. @deprecation.Deprecated('Use listOperations')
  1030. def getTaskList():
  1031. """Retrieves a list of the user's tasks.
  1032. Returns:
  1033. A list of task status dictionaries, one for each task submitted to EE by
  1034. the current user. These include currently running tasks as well as recently
  1035. canceled or failed tasks.
  1036. """
  1037. return [_cloud_api_utils.convert_operation_to_task(o)
  1038. for o in listOperations()]
  1039. def listOperations(project=None):
  1040. """Retrieves a list of the user's tasks.
  1041. Args:
  1042. project: The project to list operations for, uses the default set project
  1043. if none is provided.
  1044. Returns:
  1045. A list of Operation status dictionaries, one for each task submitted to EE
  1046. by the current user. These include currently running tasks as well as
  1047. recently canceled or failed tasks.
  1048. """
  1049. if project is None:
  1050. project = _get_projects_path()
  1051. operations = []
  1052. request = _get_cloud_api_resource().projects().operations().list(
  1053. pageSize=_TASKLIST_PAGE_SIZE, name=project)
  1054. while request is not None:
  1055. try:
  1056. response = request.execute(num_retries=MAX_RETRIES)
  1057. operations += response.get('operations', [])
  1058. request = _cloud_api_resource.projects().operations().list_next(
  1059. request, response)
  1060. except googleapiclient.errors.HttpError as e:
  1061. raise _translate_cloud_exception(e)
  1062. return operations
  1063. @deprecation.Deprecated('Use getOperation')
  1064. def getTaskStatus(taskId):
  1065. """Retrieve status of one or more long-running tasks.
  1066. Args:
  1067. taskId: ID of the task or a list of multiple IDs.
  1068. Returns:
  1069. List containing one object for each queried task, in the same order as
  1070. the input array, each object containing the following values:
  1071. id (string) ID of the task.
  1072. state (string) State of the task, one of READY, RUNNING, COMPLETED,
  1073. FAILED, CANCELLED; or UNKNOWN if the task with the specified ID
  1074. doesn't exist.
  1075. error_message (string) For a FAILED task, a description of the error.
  1076. """
  1077. if isinstance(taskId, str):
  1078. taskId = [taskId]
  1079. result = []
  1080. for one_id in taskId:
  1081. try:
  1082. # Don't use getOperation as it will translate the exception, and we need
  1083. # to handle 404s specially.
  1084. operation = _get_cloud_api_resource().projects().operations().get(
  1085. name=_cloud_api_utils.convert_task_id_to_operation_name(
  1086. one_id)).execute(num_retries=MAX_RETRIES)
  1087. result.append(_cloud_api_utils.convert_operation_to_task(operation))
  1088. except googleapiclient.errors.HttpError as e:
  1089. if e.resp.status == 404:
  1090. result.append({'id': one_id, 'state': 'UNKNOWN'})
  1091. else:
  1092. raise _translate_cloud_exception(e)
  1093. return result
  1094. def getOperation(operation_name):
  1095. """Retrieves the status of a long-running operation.
  1096. Args:
  1097. operation_name: The name of the operation to retrieve, in the format
  1098. operations/AAAABBBBCCCCDDDDEEEEFFFF.
  1099. Returns:
  1100. An Operation status dictionary for the requested operation.
  1101. """
  1102. return _execute_cloud_call(
  1103. _get_cloud_api_resource().projects().operations().get(
  1104. name=operation_name))
  1105. @deprecation.Deprecated('Use cancelOperation')
  1106. def cancelTask(taskId):
  1107. """Cancels a batch task."""
  1108. cancelOperation(_cloud_api_utils.convert_task_id_to_operation_name(taskId))
  1109. return
  1110. def cancelOperation(operation_name):
  1111. _execute_cloud_call(_get_cloud_api_resource().projects().operations().cancel(
  1112. name=operation_name, body={}))
  1113. def exportImage(request_id, params):
  1114. """Starts an image export task running.
  1115. This is a low-level method. The higher-level ee.batch.Export.image object
  1116. is generally preferred for initiating image exports.
  1117. Args:
  1118. request_id (string): A unique ID for the task, from newTaskId.
  1119. If you are using the cloud API, this does not need to be from newTaskId,
  1120. (though that's a good idea, as it's a good source of unique strings).
  1121. It can also be empty, but in that case the request is more likely to
  1122. fail as it cannot be safely retried.
  1123. params: The object that describes the export task.
  1124. If you are using the cloud API, this should be an ExportImageRequest.
  1125. However, the "expression" parameter can be the actual Image to be
  1126. exported, not its serialized form.
  1127. Returns:
  1128. A dict with information about the created task.
  1129. If you are using the cloud API, this will be an Operation.
  1130. """
  1131. params = params.copy()
  1132. return _prepare_and_run_export(
  1133. request_id, params,
  1134. _get_cloud_api_resource().projects().image().export)
  1135. def exportTable(request_id, params):
  1136. """Starts a table export task running.
  1137. This is a low-level method. The higher-level ee.batch.Export.table object
  1138. is generally preferred for initiating table exports.
  1139. Args:
  1140. request_id (string): A unique ID for the task, from newTaskId. If you are
  1141. using the cloud API, this does not need to be from newTaskId, (though
  1142. that's a good idea, as it's a good source of unique strings). It can also
  1143. be empty, but in that case the request is more likely to fail as it cannot
  1144. be safely retried.
  1145. params: The object that describes the export task. If you are using the
  1146. cloud API, this should be an ExportTableRequest. However, the "expression"
  1147. parameter can be the actual FeatureCollection to be exported, not its
  1148. serialized form.
  1149. Returns:
  1150. A dict with information about the created task.
  1151. If you are using the cloud API, this will be an Operation.
  1152. """
  1153. params = params.copy()
  1154. return _prepare_and_run_export(
  1155. request_id, params,
  1156. _get_cloud_api_resource().projects().table().export)
  1157. def exportVideo(request_id, params):
  1158. """Starts a video export task running.
  1159. This is a low-level method. The higher-level ee.batch.Export.video object
  1160. is generally preferred for initiating video exports.
  1161. Args:
  1162. request_id (string): A unique ID for the task, from newTaskId.
  1163. If you are using the cloud API, this does not need to be from newTaskId,
  1164. (though that's a good idea, as it's a good source of unique strings).
  1165. It can also be empty, but in that case the request is more likely to
  1166. fail as it cannot be safely retried.
  1167. params: The object that describes the export task.
  1168. If you are using the cloud API, this should be an ExportVideoRequest.
  1169. However, the "expression" parameter can be the actual ImageCollection
  1170. to be exported, not its serialized form.
  1171. Returns:
  1172. A dict with information about the created task.
  1173. If you are using the cloud API, this will be an Operation.
  1174. """
  1175. params = params.copy()
  1176. return _prepare_and_run_export(
  1177. request_id, params,
  1178. _get_cloud_api_resource().projects().video().export)
  1179. def exportMap(request_id, params):
  1180. """Starts a map export task running.
  1181. This is a low-level method. The higher-level ee.batch.Export.map object
  1182. is generally preferred for initiating map tile exports.
  1183. Args:
  1184. request_id (string): A unique ID for the task, from newTaskId.
  1185. If you are using the cloud API, this does not need to be from newTaskId,
  1186. (though that's a good idea, as it's a good source of unique strings).
  1187. It can also be empty, but in that case the request is more likely to
  1188. fail as it cannot be safely retried.
  1189. params: The object that describes the export task.
  1190. If you are using the cloud API, this should be an ExportMapRequest.
  1191. However, the "expression" parameter can be the actual Image to be
  1192. exported, not its serialized form.
  1193. Returns:
  1194. A dict with information about the created task.
  1195. If you are using the cloud API, this will be an Operation.
  1196. """
  1197. params = params.copy()
  1198. return _prepare_and_run_export(
  1199. request_id, params,
  1200. _get_cloud_api_resource().projects().map().export)
  1201. def _prepare_and_run_export(request_id, params, export_endpoint):
  1202. """Starts an export task running.
  1203. Args:
  1204. request_id (string): An optional unique ID for the task.
  1205. params: The object that describes the export task. The "expression"
  1206. parameter can be the actual object to be exported, not its serialized
  1207. form. This may be modified.
  1208. export_endpoint: A callable representing the export endpoint to invoke
  1209. (e.g., _cloud_api_resource.image().export).
  1210. Returns:
  1211. An Operation with information about the created task.
  1212. """
  1213. _maybe_populate_workload_tag(params)
  1214. if request_id:
  1215. if isinstance(request_id, str):
  1216. params['requestId'] = request_id
  1217. # If someone passes request_id via newTaskId() (which returns a list)
  1218. # try to do the right thing and use the first entry as a request ID.
  1219. elif (isinstance(request_id, list) and len(request_id) == 1 and
  1220. isinstance(request_id[0], str)):
  1221. params['requestId'] = request_id[0]
  1222. else:
  1223. raise ValueError('"requestId" must be a string.')
  1224. if isinstance(params['expression'], encodable.Encodable):
  1225. params['expression'] = serializer.encode(
  1226. params['expression'], for_cloud_api=True)
  1227. num_retries = MAX_RETRIES if request_id else 0
  1228. return _execute_cloud_call(
  1229. export_endpoint(project=_get_projects_path(), body=params),
  1230. num_retries=num_retries)
  1231. def startIngestion(request_id, params, allow_overwrite=False):
  1232. """Creates an image asset import task.
  1233. Args:
  1234. request_id (string): A unique ID for the ingestion, from newTaskId.
  1235. If you are using the Cloud API, this does not need to be from newTaskId,
  1236. (though that's a good idea, as it's a good source of unique strings).
  1237. It can also be empty, but in that case the request is more likely to
  1238. fail as it cannot be safely retried.
  1239. params: The object that describes the import task, which can
  1240. have these fields:
  1241. name (string) The destination asset id (e.g.,
  1242. "projects/earthengine-legacy/assets/users/foo/bar").
  1243. tilesets (array) A list of Google Cloud Storage source file paths
  1244. formatted like:
  1245. [{'sources': [
  1246. {'uris': ['foo.tif', 'foo.prj']},
  1247. {'uris': ['bar.tif', 'bar.prj']},
  1248. ]}]
  1249. Where path values correspond to source files' Google Cloud Storage
  1250. object names, e.g. 'gs://bucketname/filename.tif'
  1251. bands (array) An optional list of band names formatted like:
  1252. [{'id': 'R'}, {'id': 'G'}, {'id': 'B'}]
  1253. In general, this is a dict representation of an ImageManifest.
  1254. allow_overwrite: Whether the ingested image can overwrite an
  1255. existing version.
  1256. Returns:
  1257. A dict with notes about the created task. This will include the ID for the
  1258. import task (under 'id'), which may be different from request_id.
  1259. """
  1260. request = {
  1261. 'imageManifest':
  1262. _cloud_api_utils.convert_params_to_image_manifest(params),
  1263. 'requestId':
  1264. request_id,
  1265. 'overwrite':
  1266. allow_overwrite
  1267. }
  1268. # It's only safe to retry the request if there's a unique ID to make it
  1269. # idempotent.
  1270. num_retries = MAX_RETRIES if request_id else 0
  1271. operation = _execute_cloud_call(
  1272. _get_cloud_api_resource().projects().image().import_(
  1273. project=_get_projects_path(), body=request),
  1274. num_retries=num_retries)
  1275. return {
  1276. 'id':
  1277. _cloud_api_utils.convert_operation_name_to_task_id(
  1278. operation['name']),
  1279. 'name': operation['name'],
  1280. 'started': 'OK',
  1281. }
  1282. def startTableIngestion(request_id, params, allow_overwrite=False):
  1283. """Creates a table asset import task.
  1284. Args:
  1285. request_id (string): A unique ID for the ingestion, from newTaskId.
  1286. If you are using the Cloud API, this does not need to be from newTaskId,
  1287. (though that's a good idea, as it's a good source of unique strings).
  1288. It can also be empty, but in that case the request is more likely to
  1289. fail as it cannot be safely retried.
  1290. params: The object that describes the import task, which can
  1291. have these fields:
  1292. name (string) The destination asset id (e.g.,
  1293. "projects/earthengine-legacy/assets/users/foo/bar").
  1294. sources (array) A list of GCS (Google Cloud Storage) file paths
  1295. with optional character encoding formatted like this:
  1296. "sources":[{"uris":["gs://bucket/file.shp"],"charset":"UTF-8"}]
  1297. Here 'charset' refers to the character encoding of the source file.
  1298. In general, this is a dict representation of a TableManifest.
  1299. allow_overwrite: Whether the ingested image can overwrite an
  1300. existing version.
  1301. Returns:
  1302. A dict with notes about the created task. This will include the ID for the
  1303. import task (under 'id'), which may be different from request_id.
  1304. """
  1305. request = {
  1306. 'tableManifest':
  1307. _cloud_api_utils.convert_params_to_table_manifest(params),
  1308. 'requestId':
  1309. request_id,
  1310. 'overwrite':
  1311. allow_overwrite
  1312. }
  1313. # It's only safe to retry the request if there's a unique ID to make it
  1314. # idempotent.
  1315. num_retries = MAX_RETRIES if request_id else 0
  1316. operation = _execute_cloud_call(
  1317. _get_cloud_api_resource().projects().table().import_(
  1318. project=_get_projects_path(), body=request),
  1319. num_retries=num_retries)
  1320. return {
  1321. 'id':
  1322. _cloud_api_utils.convert_operation_name_to_task_id(
  1323. operation['name']),
  1324. 'name': operation['name'],
  1325. 'started': 'OK'
  1326. }
  1327. def getAssetRoots():
  1328. """Returns the list of the root folders the user owns.
  1329. Note: The "id" values for roots are two levels deep, e.g. "users/johndoe"
  1330. not "users/johndoe/notaroot".
  1331. Returns:
  1332. A list of folder descriptions formatted like:
  1333. [
  1334. {"type": "Folder", "id": "users/foo"},
  1335. {"type": "Folder", "id": "projects/bar"},
  1336. ]
  1337. """
  1338. return _cloud_api_utils.convert_list_assets_result_to_get_list_result(
  1339. listBuckets())
  1340. def getAssetRootQuota(rootId):
  1341. """Returns quota usage details for the asset root with the given ID.
  1342. Usage notes:
  1343. - The id *must* be a root folder like "users/foo" (not "users/foo/bar").
  1344. - The authenticated user must own the asset root to see its quota usage.
  1345. Args:
  1346. rootId: The ID of the asset to check.
  1347. Returns:
  1348. A dict describing the asset's quota usage. Looks like, with size in bytes:
  1349. {
  1350. asset_count: {usage: number, limit: number},
  1351. asset_size: {usage: number, limit: number},
  1352. }
  1353. """
  1354. asset = getAsset(rootId)
  1355. if 'quota' not in asset:
  1356. raise ee_exception.EEException('{} is not a root folder.'.format(rootId))
  1357. quota = asset['quota']
  1358. # The quota fields are int64s, and int64s are represented as strings in
  1359. # JSON. Turn them back.
  1360. return {
  1361. 'asset_count': {
  1362. 'usage': int(quota.get('assetCount', 0)),
  1363. 'limit': int(quota.get('maxAssets', quota.get('maxAssetCount', 0)))
  1364. },
  1365. 'asset_size': {
  1366. 'usage': int(quota.get('sizeBytes', 0)),
  1367. 'limit': int(quota.get('maxSizeBytes', 0))
  1368. }
  1369. }
  1370. @deprecation.Deprecated('Use getIamPolicy')
  1371. def getAssetAcl(assetId):
  1372. """Returns the access control list of the asset with the given ID.
  1373. Args:
  1374. assetId: The ID of the asset to check.
  1375. Returns:
  1376. A dict describing the asset's ACL. Looks like:
  1377. {
  1378. "owners" : ["user@domain1.com"],
  1379. "writers": ["user2@domain1.com", "user3@domain1.com"],
  1380. "readers": ["some_group@domain2.com"],
  1381. "all_users_can_read" : True
  1382. }
  1383. If you are using the cloud API, then the entities in the ACL will
  1384. be prefixed by a type tag, such as "user:" or "group:".
  1385. """
  1386. policy = getIamPolicy(assetId)
  1387. return _cloud_api_utils.convert_iam_policy_to_acl(policy)
  1388. def getIamPolicy(asset_id):
  1389. """Loads ACL info for an asset, given an asset id.
  1390. Args:
  1391. asset_id: The asset to be retrieved.
  1392. Returns:
  1393. The asset's ACL, as an IAM Policy.
  1394. """
  1395. return _execute_cloud_call(
  1396. _get_cloud_api_resource().projects().assets().getIamPolicy(
  1397. resource=_cloud_api_utils.convert_asset_id_to_asset_name(asset_id),
  1398. body={},
  1399. prettyPrint=False))
  1400. @deprecation.Deprecated('Use setIamPolicy')
  1401. def setAssetAcl(assetId, aclUpdate):
  1402. """Sets the access control list of the asset with the given ID.
  1403. The owner ACL cannot be changed, and the final ACL of the asset
  1404. is constructed by merging the OWNER entries of the old ACL with
  1405. the incoming ACL record.
  1406. Args:
  1407. assetId: The ID of the asset to set the ACL on.
  1408. aclUpdate: The updated ACL for the asset. Must be formatted like the
  1409. value returned by getAssetAcl but without "owners".
  1410. """
  1411. # The ACL may be a string by the time it gets to us. Sigh.
  1412. if isinstance(aclUpdate, str):
  1413. aclUpdate = json.loads(aclUpdate)
  1414. setIamPolicy(assetId, _cloud_api_utils.convert_acl_to_iam_policy(aclUpdate))
  1415. return
  1416. def setIamPolicy(asset_id, policy):
  1417. """Sets ACL info for an asset.
  1418. Args:
  1419. asset_id: The asset to set the ACL policy on.
  1420. policy: The new Policy to apply to the asset. This replaces
  1421. the current Policy.
  1422. Returns:
  1423. The new ACL, as an IAM Policy.
  1424. """
  1425. return _execute_cloud_call(
  1426. _get_cloud_api_resource().projects().assets().setIamPolicy(
  1427. resource=_cloud_api_utils.convert_asset_id_to_asset_name(asset_id),
  1428. body={'policy': policy},
  1429. prettyPrint=False))
  1430. def setAssetProperties(assetId, properties):
  1431. """Sets metadata properties of the asset with the given ID.
  1432. To delete a property, set its value to None.
  1433. The authenticated user must be a writer or owner of the asset.
  1434. Args:
  1435. assetId: The ID of the asset to set the ACL on.
  1436. properties: A dictionary of keys and values for the properties to update.
  1437. """
  1438. def FieldMaskPathForKey(key):
  1439. return 'properties.\"%s\"' % key
  1440. # Specifying an update mask of 'properties' results in full replacement,
  1441. # which isn't what we want. Instead, we name each property that we'll be
  1442. # updating.
  1443. update_mask = [FieldMaskPathForKey(key) for key in properties]
  1444. updateAsset(assetId, {'properties': properties}, update_mask)
  1445. return
  1446. def updateAsset(asset_id, asset, update_mask):
  1447. """Updates an asset.
  1448. Args:
  1449. asset_id: The ID of the asset to update.
  1450. asset: The updated version of the asset, containing only the new values of
  1451. the fields to be updated. Only the "start_time", "end_time", and
  1452. "properties" fields can be updated. If a value is named in "update_mask",
  1453. but is unset in "asset", then that value will be deleted from the asset.
  1454. update_mask: A list of the values to update. This should contain the strings
  1455. "start_time" or "end_time" to update the corresponding timestamp. If
  1456. a property is to be updated or deleted, it should be named here as
  1457. "properties.THAT_PROPERTY_NAME". If the entire property set is to be
  1458. replaced, this should contain the string "properties". If this list is
  1459. empty, all properties and both timestamps will be updated.
  1460. """
  1461. name = _cloud_api_utils.convert_asset_id_to_asset_name(asset_id)
  1462. _execute_cloud_call(_get_cloud_api_resource().projects().assets().patch(
  1463. name=name, body={
  1464. 'updateMask': {
  1465. 'paths': update_mask
  1466. },
  1467. 'asset': asset
  1468. }))
  1469. def createAssetHome(requestedId):
  1470. """Attempts to create a home root folder for the current user ("users/joe").
  1471. Results in an error if the user already has a home root folder or the
  1472. requested ID is unavailable.
  1473. Args:
  1474. requestedId: The requested ID of the home folder (e.g. "users/joe").
  1475. """
  1476. # This is just a special case of folder creation.
  1477. createAsset({
  1478. 'name': _cloud_api_utils.convert_asset_id_to_asset_name(requestedId),
  1479. 'type': 'FOLDER'
  1480. })
  1481. return
  1482. def authorizeHttp(http):
  1483. if _credentials:
  1484. return AuthorizedHttp(_credentials)
  1485. else:
  1486. return http
  1487. def create_assets(asset_ids, asset_type, mk_parents):
  1488. """Creates the specified assets if they do not exist."""
  1489. for asset_id in asset_ids:
  1490. if getInfo(asset_id):
  1491. print('Asset %s already exists.' % asset_id)
  1492. continue
  1493. if mk_parents:
  1494. parts = asset_id.split('/')
  1495. # We don't need to create the namespace and the user's/project's folder.
  1496. if len(parts) > 2:
  1497. path = parts[0] + '/' + parts[1] + '/'
  1498. for part in parts[2:-1]:
  1499. path += part
  1500. if getInfo(path) is None:
  1501. createAsset({'type': ASSET_TYPE_FOLDER_CLOUD}, path)
  1502. path += '/'
  1503. createAsset({'type': asset_type}, asset_id)
  1504. def convert_asset_id_to_asset_name(asset_id):
  1505. """Converts an internal asset ID to a Cloud API asset name.
  1506. If asset_id already matches the format 'projects/*/assets/**', it is returned
  1507. as-is.
  1508. Args:
  1509. asset_id: The asset ID to convert.
  1510. Returns:
  1511. An asset name string in the format 'projects/*/assets/**'.
  1512. """
  1513. return _cloud_api_utils.convert_asset_id_to_asset_name(asset_id)
  1514. def getWorkloadTag():
  1515. """Returns the currently set workload tag."""
  1516. return _workloadTag.get()
  1517. def setWorkloadTag(tag):
  1518. """Sets the workload tag, used to label computation and exports.
  1519. Workload tag must be 1 - 63 characters, beginning and ending with an
  1520. alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots
  1521. (.), and alphanumerics between, or an empty string to clear the workload tag.
  1522. Args:
  1523. tag: The tag to set.
  1524. """
  1525. _workloadTag.set(tag)
  1526. @contextlib.contextmanager
  1527. def workloadTagContext(tag):
  1528. """Produces a context manager which sets the workload tag, then resets it.
  1529. Workload tag must be 1 - 63 characters, beginning and ending with an
  1530. alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots
  1531. (.), and alphanumerics between, or an empty string to clear the workload tag.
  1532. Args:
  1533. tag: The tag to set.
  1534. Yields:
  1535. None.
  1536. """
  1537. setWorkloadTag(tag)
  1538. try:
  1539. yield
  1540. finally:
  1541. resetWorkloadTag()
  1542. def setDefaultWorkloadTag(tag):
  1543. """Sets the workload tag, and as the default for which to reset back to.
  1544. For example, calling `ee.data.resetWorkloadTag()` will reset the workload tag
  1545. back to the default chosen here. To reset the default back to none, pass in
  1546. an empty string or pass in true to `ee.data.resetWorkloadTag(true)`, like so.
  1547. Workload tag must be 1 - 63 characters, beginning and ending with an
  1548. alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots
  1549. (.), and alphanumerics between, or an empty string to reset the default back
  1550. to none.
  1551. Args:
  1552. tag: The tag to set.
  1553. """
  1554. _workloadTag.setDefault(tag)
  1555. _workloadTag.set(tag)
  1556. def resetWorkloadTag(opt_resetDefault=False):
  1557. """Sets the default tag for which to reset back to.
  1558. If opt_resetDefault parameter is set to true, the default will be set to empty
  1559. before resetting. Defaults to False.
  1560. Args:
  1561. opt_resetDefault: Whether to reset the default back to empty.
  1562. """
  1563. if opt_resetDefault:
  1564. _workloadTag.setDefault('')
  1565. _workloadTag.reset()
  1566. class _WorkloadTag(object):
  1567. """A helper class to manage the workload tag."""
  1568. def __init__(self):
  1569. self._tag = ''
  1570. self._default = ''
  1571. def get(self):
  1572. return self._tag
  1573. def set(self, tag):
  1574. self._tag = self.validate(tag)
  1575. def setDefault(self, newDefault):
  1576. self._default = self.validate(newDefault)
  1577. def reset(self):
  1578. self._tag = self._default
  1579. def validate(self, tag):
  1580. """Throws an error if setting an invalid tag.
  1581. Args:
  1582. tag: the tag to validate.
  1583. Returns:
  1584. The validated tag.
  1585. Raises:
  1586. ValueError if the tag does not match the expected format.
  1587. """
  1588. if not tag and tag != 0:
  1589. return ''
  1590. tag = str(tag)
  1591. if not re.fullmatch(r'([a-z0-9]|[a-z0-9][-_\.a-z0-9]{0,61}[a-z0-9])', tag):
  1592. validationMessage = (
  1593. 'Tags must be 1-63 characters, '
  1594. 'beginning and ending with an lowercase alphanumeric character'
  1595. '([a-z0-9]) with dashes (-), underscores (_), '
  1596. 'dots (.), and lowercase alphanumerics between.')
  1597. raise ValueError(f'Invalid tag, "{tag}". {validationMessage}')
  1598. return tag
  1599. # Tracks the currently set workload tag.
  1600. _workloadTag = _WorkloadTag()