Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2256ec4
fix: authorize other type of parameter in copy/move
12rambau Jul 29, 2025
3c77a33
refactor: simplify the ordering method
12rambau Aug 2, 2025
9481793
refactor: simplify the ordering method
12rambau Aug 2, 2025
a634186
style: refine coding look & feel
12rambau Aug 2, 2025
d3dc7d4
fix: remain 100% server side
12rambau Aug 2, 2025
4499113
docs: add an example to sortmany method
12rambau Aug 4, 2025
6739181
fix: authorize empty dictionnary argument
12rambau Aug 12, 2025
89f46f8
fix: authorize empty dictionnary argument
12rambau Aug 12, 2025
e68aae6
refactor: use the index instead of converting the imageCollection int…
12rambau Aug 12, 2025
bf0c22e
fix: authorize other type of parameter in copy/move (#462)
12rambau Aug 12, 2025
b1ed4f3
test: test the asset not the image object
12rambau Aug 13, 2025
2f3888f
refactor: use the index instead of converting the imageCollection int…
12rambau Aug 13, 2025
9cf91a1
fix: authorize empty dictionnary argument (#466)
12rambau Aug 13, 2025
cbc0653
fix: add CRS and drop Z coordinate + tests
fitoprincipe Sep 30, 2025
da6f1b4
fix: use state to determine the project Id
12rambau Oct 16, 2025
1c37cfc
fix: register the project_id using vanilla method
12rambau Oct 16, 2025
6026266
fix: drop the _project_id global variable
12rambau Oct 16, 2025
ee781a4
fix: remove reference to deprecated variable
12rambau Oct 16, 2025
3591be0
fix: inverse order of the decorator for 3.9
12rambau Oct 16, 2025
34f25c0
fix: use the non-interactive backend for tests
12rambau Oct 16, 2025
00dc976
fix: update the credential set-up to the new State object (#474)
12rambau Oct 17, 2025
9776bb4
bump: version 1.17.2 → 1.17.3
12rambau Oct 17, 2025
c2a4d91
Merge branch 'main' into better-sort
12rambau Oct 17, 2025
2bc32b6
fix: make systematically sure the data is 2d
12rambau Oct 17, 2025
48cb99c
test: remove the unused parameter
12rambau Oct 17, 2025
a8553ad
fix: pin the version of EE
12rambau Oct 20, 2025
211ed93
bump: version 1.17.3 → 1.17.4
12rambau Oct 20, 2025
88b96fd
build: align on GEE latest pinned version
12rambau Oct 20, 2025
634a991
test: remove 3.9 from the testing matrix
12rambau Oct 24, 2025
4e7f829
add CRS and drop Z coordinate + tests (#473)
12rambau Nov 3, 2025
59439db
refactor: Better sort (#464)
12rambau Jan 26, 2026
2f81006
feat: add the pixelArea supercharged
12rambau Jan 26, 2026
3577802
test: skip the asset tests
12rambau Jan 26, 2026
1dc800f
test: skip the asset tests (#477)
12rambau Jan 26, 2026
9b66748
test: fix the image related tests that were failing
12rambau Jan 26, 2026
54d957a
test: repair the tests relative to imageCollection
12rambau Jan 26, 2026
33a901a
test: skip for python 3.10
12rambau Jan 26, 2026
49f4831
test: update the test non-regressions (#479)
12rambau Jan 27, 2026
2762143
feat: sort featurecollection by area
12rambau Jan 27, 2026
b03655a
docs: execute the documentation
12rambau Jan 27, 2026
a674149
test: update tests (#481)
12rambau Jan 27, 2026
2ef3c0e
feat: sort featurecollection by area (#482)
12rambau Jan 27, 2026
3a097f6
fix: compute boolean correctly
12rambau Jan 27, 2026
32cac7e
fix: use the jupyter execute method
12rambau Jan 27, 2026
419f6d6
feat: add the pixelArea supercharged (#478)
12rambau Jan 27, 2026
17ef305
docs: typo in example
12rambau Jan 27, 2026
0547eee
docs: fix the docstring
12rambau Jan 27, 2026
e143d77
fix: add the missing breakFeature
12rambau Jan 27, 2026
49657b2
feat: filter per geometry Type (#484)
12rambau Jan 27, 2026
6f95ebb
bump: version 1.17.4 → 1.18.0
12rambau Jan 28, 2026
69bb851
Merge branch 'ee-extra' into main
12rambau Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/unit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
include:
- os: macos-latest # macos test
python-version: "3.12"
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
project = "geetools"
author = "Rodrigo E. Principe"
copyright = f"2017-{datetime.now().year}, {author}"
release = "1.17.2"
release = "1.18.0"

# -- General configuration -----------------------------------------------------
extensions = [
Expand Down
2 changes: 1 addition & 1 deletion geetools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
__title__ = "geetools"
__summary__ = "A set of useful tools to use with Google Earth Engine Python" "API"
__uri__ = "http://geetools.readthedocs.io"
__version__ = "1.17.2"
__version__ = "1.18.0"

__author__ = "Rodrigo E. Principe"
__email__ = "fitoprincipe82@gmail.com"
Expand Down
18 changes: 13 additions & 5 deletions geetools/ee_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import ee
import ee.data
from ee._state import get_state

from .accessors import _register_extention
from .utils import format_description
Expand All @@ -24,7 +25,7 @@ def __init__(self, *args):
An asset cannot be an absolute path like in a normal filesystem and thus any trailing "/" will be removed.
"""
if len(args) == 0:
self._path = f"projects/{ee.data._cloud_api_user_project}/assets/"
self._path = f"projects/{get_state().cloud_api_user_project}/assets/"
else:
self._path = args[0]._path if isinstance(args[0], Asset) else PurePosixPath(*args)
project_assets = PurePosixPath(str(self._path)[1:])
Expand Down Expand Up @@ -90,7 +91,7 @@ def home(cls) -> Asset:

ee.Asset.home()
"""
return cls(f"projects/{ee.data._cloud_api_user_project}/assets/")
return cls(f"projects/{get_state().cloud_api_user_project}/assets/")

def as_posix(self) -> str:
"""Return the asset id as a posix path.
Expand Down Expand Up @@ -162,7 +163,7 @@ def is_user_project(self, raised: bool = False) -> bool:
return True
else:
if raised is True:
user_project = ee.data._cloud_api_user_project
user_project = get_state().cloud_api_user_project
msg = f"Asset {self.as_posix()} is not in the same project as the user ({user_project})"
raise ValueError(msg)
else:
Expand Down Expand Up @@ -541,7 +542,7 @@ def owner(self):
self.is_absolute(raised=True)
return self.parts[1]

def move(self, new_asset: Asset, overwrite: bool = False) -> Asset:
def move(self, new_asset: os.PathLike, overwrite: bool = False) -> Asset:
"""Move the asset to a target destination.

Move this asset (any type) to the given target, and return a new ``Asset`` instance
Expand All @@ -564,6 +565,7 @@ def move(self, new_asset: Asset, overwrite: bool = False) -> Asset:
asset.move(new_asset, overwrite=False)
"""
# copy the assets
new_asset = new_asset if isinstance(new_asset, Asset) else ee.Asset(str(new_asset))
self.copy(new_asset, overwrite=overwrite)

# delete the original
Expand Down Expand Up @@ -648,7 +650,7 @@ def rmdir(self, recursive: bool = False, dry_run: bool | None = None) -> list:
self.exists(raised=True)
return self.delete(recursive, dry_run)

def copy(self, new_asset: Asset, overwrite: bool = False) -> Asset:
def copy(self, new_asset: os.PathLike, overwrite: bool = False) -> Asset:
"""Copy the asset to a target destination.

Copy this asset (any type) to the given target, and return a new ``Asset`` instance
Expand All @@ -671,6 +673,7 @@ def copy(self, new_asset: Asset, overwrite: bool = False) -> Asset:
asset.copy(new_asset, overwrite=False)
"""
# exit if the destination asset exist and overwrite is False
new_asset = new_asset if isinstance(new_asset, Asset) else ee.Asset(str(new_asset))
if new_asset.exists() and overwrite is False:
raise ValueError(f"Asset {new_asset.as_posix()} already exists.")

Expand Down Expand Up @@ -782,6 +785,11 @@ def date_in_str(d: str | int | float | datetime | date) -> str:
d = datetime.fromtimestamp(int(d) / 1000).isoformat() + "Z"
return str(d) # if any other format is used, we will simply return it as a string

# early exit if kwargs is empty
if len(kwargs) == 0:
return self

# Convert the system properties to the correct format
if "system:time_start" in kwargs:
kwargs["system:time_start"] = date_in_str(kwargs.pop("system:time_start"))
if "system:time_end" in kwargs:
Expand Down
18 changes: 16 additions & 2 deletions geetools/ee_authenticate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Toolbox for the :py:func:`ee.Authenticate` function."""
from __future__ import annotations

import json
import os
from contextlib import suppress
from pathlib import Path
Expand All @@ -17,14 +18,15 @@ class AuthenticateAccessor:
"""Create an accessor for the :py:func:`ee.Authenticate` function."""

@staticmethod
def new_user(name: str = "", credential_pathname: str | os.PathLike = ""):
def new_user(name: str = "", credential_pathname: str | os.PathLike = "", key_data: dict | None = None):
"""Authenticate the user and save the credentials in a specific folder.

Equivalent to :py:func:`ee.Authenticate` but where the registered user will not be the default one (the one you get when running :py:func:`ee.Initialize`).

Args:
name: The name of the user. If not set, it will reauthenticate default.
credential_pathname: The path to the folder where the credentials are stored. If not set, it uses the default path.
key_data: The json private key of the service account as a dict object. If not set, it will use the web-based authentication.

Example:
.. code-block:: python
Expand All @@ -48,10 +50,22 @@ def new_user(name: str = "", credential_pathname: str | os.PathLike = ""):
default = Path(ee.oauth.get_credentials_path())

with TemporaryDirectory() as dir:

# move the existing credentials to a safe place
with suppress(FileNotFoundError):
move(default, Path(dir) / default.name)
ee.Authenticate()

# save the credentials passed to key_data or alternatively run the
# web-based authentication that will write the credentials to the same file
if key_data is not None:
default.write_text(json.dumps(key_data))
else:
ee.Authenticate()

# move the new credentials to the desired name
move(default, credential_path / name)

# restore the previous default credentials
with suppress(FileNotFoundError):
move(Path(dir) / default.name, default)

Expand Down
21 changes: 9 additions & 12 deletions geetools/ee_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ def toAsset(
ee.data.createAsset({"type": "IMAGE_COLLECTION"}, aid.as_posix())

# loop over the collection and export each image
nb_images = imagecollection.size().getInfo()
imageList = imagecollection.toList(nb_images)
system_indices = imagecollection.aggregate_array("system:index").getInfo()
task_list = []
for i in range(nb_images):
for sys_idx in system_indices:
# extract image information
locImage = ee.Image(imageList.get(i))
locImage = imagecollection.filter(ee.Filter.eq(index_property, sys_idx)).first()
loc_id = locImage.get(index_property).getInfo()

# override the parameters related to the image itself
Expand Down Expand Up @@ -135,12 +134,11 @@ def toDrive(
fid = folder if folder else description

# loop over the collection and export each image
nb_images = imagecollection.size().getInfo()
imageList = imagecollection.toList(nb_images)
system_indices = imagecollection.aggregate_array("system:index").getInfo()
task_list = []
for i in range(nb_images):
for sys_idx in system_indices:
# extract image information
locImage = ee.Image(imageList.get(i))
locImage = imagecollection.filter(ee.Filter.eq(index_property, sys_idx)).first()
loc_id = locImage.get(index_property).getInfo()

# override the parameters related to the image itself
Expand Down Expand Up @@ -199,12 +197,11 @@ def toCloudStorage(
fid = folder if folder else description

# loop over the collection and export each image
nb_images = imagecollection.size().getInfo()
imageList = imagecollection.toList(nb_images)
system_indices = imagecollection.aggregate_array("system:index").getInfo()
task_list = []
for i in range(nb_images):
for sys_idx in system_indices:
# extract image information
locImage = ee.Image(imageList.get(i))
locImage = imagecollection.filter(ee.Filter.eq(index_property, sys_idx)).first()
loc_id = locImage.get(index_property).getInfo()

# override the parameters related to the image itself
Expand Down
113 changes: 111 additions & 2 deletions geetools/ee_feature_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import ee
import geopandas as gpd
import shapely
from matplotlib import pyplot as plt
from matplotlib.axes import Axes

Expand Down Expand Up @@ -711,7 +712,11 @@ def plot(
gdf.plot(column=property, ax=ax, cmap=cmap)

@classmethod
def fromGeoInterface(cls, data: dict | GeoInterface) -> ee.FeatureCollection:
def fromGeoInterface(
cls,
data: dict | GeoInterface,
crs: str = "EPSG:4326",
) -> ee.FeatureCollection:
"""Create a :py:class:`ee.FeatureCollection` from a geo interface.

The ``geo_interface`` is a protocol representing a vector collection as a python GeoJSON-like dictionary structure.
Expand All @@ -723,7 +728,7 @@ def fromGeoInterface(cls, data: dict | GeoInterface) -> ee.FeatureCollection:

Parameters:
data: The geo_interface to create the :py:class:`ee.FeatureCollection` from.
crs: The CRS to use for the FeatureCollection. Default to ``EPSG:4326``.
crs: The CRS of the input data. Defaults to "EPSG:4326".

Returns:
The created :py:class:`ee.FeatureCollection` from the geo_interface.
Expand Down Expand Up @@ -755,5 +760,109 @@ def fromGeoInterface(cls, data: dict | GeoInterface) -> ee.FeatureCollection:
elif not isinstance(data, dict):
raise ValueError("The data must be a geo_interface or a dictionary")

# ensure the geometries are 2D
gdf = gpd.GeoDataFrame.from_features(data["features"], crs=crs)
gdf.geometry = shapely.force_2d(gdf.geometry.values)
data = gdf.__geo_interface__

# create the feature collection
return ee.FeatureCollection(data)

def areaSort(self, ascending: bool = True) -> ee.FeatureCollection:
"""Sort the features in the collection by area.

Args:
ascending: Whether to sort in ascending order.

Returns:
The sorted collection.

Examples:
.. jupyter-execute::

import ee, geetools
from geetools.utils import initialize_documentation

initialize_documentation()

fc = ee.FeatureCollection("FAO/GAUL/2015/level0").limit(5)
fc = fc.geetools.areaSort()
fc.aggregate_array("ADM0_NAME").getInfo()
"""
# generate an area property
name = "__geetools_area__"
fc = self._obj.map(lambda feat: feat.set(name, feat.geometry().area(1)))

# sort by area and remove the property from the output
properties = fc.first().propertyNames().remove(name)
return fc.sort(name, ascending).map(lambda feat: feat.select(properties))

def filterGeometryType(self, type_: str | ee.String) -> ee.FeatureCollection:
"""Filter the collection by geometry type.

Only keep in the Final featureCollection the Feature with the specified geometry type.
Can be combined with ``breakGeometries`` to filter out multi geometries.

Args:
type_: The geometry type to filter on. Must be one of the `GEE compatible types <https://developers.google.com/earth-engine/guides/geometries>`__

Returns:
The filtered collection

Examples:
.. jupyter-execute::

import ee, geetools
from geetools.utils import initialize_documentation

initialize_documentation()

geoms = [ee.Geometry.Point([0, 0]), ee.Geometry.Point([0, 0]).buffer(1)]
fc = ee.FeatureCollection([ee.Feature(g, {"test": "test"}) for g in geoms])
fc = fc.geetools.breakGeometries().geetools.filterGeometryType("Point")
fc.aggregate_array("test").getInfo()
"""
# extract the properties of the features before filtering
# and create an extra column with the geometry type
type_, name = ee.String(type_), "__geetools_type__"
properties = self._obj.first().propertyNames()
fc = self._obj.map(lambda feat: feat.set(name, feat.geometry().type()))

# filter the collection and remove the extra column
fc = fc.filter(ee.Filter.eq(name, type_)).select(properties)

return ee.FeatureCollection(fc)

def breakGeometries(self) -> ee.FeatureCollection:
"""Break every feature using geometries into it's constituents.

Each Geometry that is using a multi parts geometry type will be duplicated
into multiple features with each one carrying one of the constituent of the multiPolygon.

Returns:
ee.FeatureCollection: The collection with the broken down geometries

Examples:
.. code-block:: python

import ee, geetools
from geetools.utils import initialize_documentation

initialize_documentation()

multipoint = ee.Geometry.MultiPoint([ee.Geometry.Point([0, 0]), ee.Geometry.Point([1, 0])])
fc = ee.FeatureCollection([ee.Feature(multipoint, {"test": "test"})])
fc = fc.geetools.breakGeometries()
fc.aggregate_array("test").getInfo()
"""
# helper function to break the geometry of a feature
def split(feat):
feat = ee.Feature(feat)
geometries = feat.geometry().geometries()
return geometries.map(lambda g: ee.Feature(ee.Geometry(g), feat.toDictionary()))

# apply the function to the collection and flatten the list as each feature
# can have multiple geometries (thus list of list) and we want a single list
list = self._obj.toList(self._obj.size()).map(split).flatten()

return ee.FeatureCollection(list)
33 changes: 32 additions & 1 deletion geetools/ee_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from xee.ext import REQUEST_BYTE_LIMIT

from .accessors import register_class_accessor
from .utils import format_class_info, plot_data
from .utils import area_units_to_m2, format_class_info, plot_data


@register_class_accessor(ee.Image, "geetools")
Expand Down Expand Up @@ -2287,3 +2287,34 @@ def plot_hist(
ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left")

return ax

@classmethod
def pixelArea(cls, area_unit: str = "m2", rename_to_units: bool = False) -> ee.Image:
"""Extend the :py:method:`ee.Image.pixelArea` method by setting the unit of the output.

Args:
area_unit: the unit of the output area. can be one of "m2", "ha", "kha", "km2" or "acres".
rename_to_units: if ``True``, the output image will be renamed to the given unit.

Returns:
the area ``ee.Image`` using the given unit.

Examples:
.. jupyter-execute::

import ee, geetools
from geetools.utils import initialize_documentation

initialize_documentation()

hectares = ee.Image.geetools.pixelArea("ha").rename("ha")
acres = ee.Image.geetools.pixelArea("acres").rename("acres")
total = hectares.addBands(acres)

buffer = ee.Geometry.Point(0,0).buffer(100)
values = total.reduceRegion(ee.Reducer.mean(), buffer, 1)
values.getInfo()
"""
name = area_unit if rename_to_units is True else "area"
divisor = area_units_to_m2(area_unit)
return ee.Image.pixelArea().divide(divisor).rename(name)
Loading
Loading