From a1306121a8845a92d0b802a0bcfebf7bb470571a Mon Sep 17 00:00:00 2001 From: Xee authors Date: Wed, 15 Nov 2023 18:26:35 -0800 Subject: [PATCH] Add Python 3.8 support. Fixes [#76](https://github.com/google/Xee/issues/76). PiperOrigin-RevId: 582869103 --- .github/workflows/ci-build.yml | 8 +++++- pyproject.toml | 2 +- xee/ext.py | 48 +++++++++++++++++----------------- xee/micro_benchmarks.py | 3 ++- xee/types.py | 18 ++++++------- 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 3089b1a..3b99dee 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -30,7 +30,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: [ + "3.8", + "3.9", + "3.10", + "3.11", + "3.12", + ] permissions: id-token: write # This is required for requesting the JWT. steps: diff --git a/pyproject.toml b/pyproject.toml index 3727d84..942cb56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "xee" dynamic = ["version"] description = "A Google Earth Engine extension for Xarray." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.8" license = {text = "Apache-2.0"} authors = [ {name = "Google LLC", email = "noreply@google.com"}, diff --git a/xee/ext.py b/xee/ext.py index 99a826f..4b00c8c 100644 --- a/xee/ext.py +++ b/xee/ext.py @@ -24,7 +24,7 @@ import math import os import sys -from typing import Any, Iterable, Literal, Optional, Union +from typing import Any, Dict, List, Iterable, Literal, Optional, Tuple, Union from urllib import parse import warnings @@ -56,7 +56,7 @@ # # The 'int' case let's users specify `io_chunks=-1`, which means to load the # data as a single chunk. -Chunks = Union[int, dict[Any, Any], Literal['auto'], None] +Chunks = Union[int, Dict[Any, Any], Literal['auto'], None] _BUILTIN_DTYPES = { @@ -72,7 +72,7 @@ REQUEST_BYTE_LIMIT = 2**20 * 48 # 48 MBs -def _check_request_limit(chunks: dict[str, int], dtype_size: int, limit: int): +def _check_request_limit(chunks: Dict[str, int], dtype_size: int, limit: int): """Checks that the actual number of bytes exceeds the limit.""" index, width, height = chunks['index'], chunks['width'], chunks['height'] actual_bytes = index * width * height * dtype_size @@ -95,19 +95,19 @@ class EarthEngineStore(common.AbstractDataStore): """Read-only Data Store for Google Earth Engine.""" # "Safe" default chunks that won't exceed the request limit. - PREFERRED_CHUNKS: dict[str, int] = { + PREFERRED_CHUNKS: Dict[str, int] = { 'index': 48, 'width': 512, 'height': 256, } - SCALE_UNITS: dict[str, int] = { + SCALE_UNITS: Dict[str, int] = { 'degree': 1, 'metre': 10_000, 'meter': 10_000, } - DIMENSION_NAMES: dict[str, tuple[str, str]] = { + DIMENSION_NAMES: Dict[str, Tuple[str, str]] = { 'degree': ('lon', 'lat'), 'metre': ('X', 'Y'), 'meter': ('X', 'Y'), @@ -254,7 +254,7 @@ def __init__( self.mask_value = mask_value @functools.cached_property - def get_info(self) -> dict[str, Any]: + def get_info(self) -> Dict[str, Any]: """Make all getInfo() calls to EE at once.""" rpcs = [ @@ -296,12 +296,12 @@ def get_info(self) -> dict[str, Any]: return dict(zip((name for name, _ in rpcs), info)) @property - def image_collection_properties(self) -> tuple[list[str], list[str]]: + def image_collection_properties(self) -> Tuple[List[str], List[str]]: system_ids, primary_coord = self.get_info['properties'] return (system_ids, primary_coord) @property - def image_ids(self) -> list[str]: + def image_ids(self) -> List[str]: image_ids, _ = self.image_collection_properties return image_ids @@ -313,7 +313,7 @@ def _max_itemsize(self) -> int: @classmethod def _auto_chunks( cls, dtype_bytes: int, request_byte_limit: int = REQUEST_BYTE_LIMIT - ) -> dict[str, int]: + ) -> Dict[str, int]: """Given the data type size and request limit, calculate optimal chunks.""" # Taking the data type number of bytes into account, let's try to have the # height and width follow round numbers (powers of two) and allocate the @@ -338,8 +338,8 @@ def _auto_chunks( return {'index': index, 'width': width, 'height': height} def _assign_index_chunks( - self, input_chunk_store: dict[Any, Any] - ) -> dict[Any, Any]: + self, input_chunk_store: Dict[Any, Any] + ) -> Dict[Any, Any]: """Assigns values of 'index', 'width', and 'height' to `self.chunks`. This method first attempts to retrieve values for 'index', 'width', @@ -380,7 +380,7 @@ def _assign_preferred_chunks(self) -> Chunks: chunks[y_dim_name] = self.chunks['height'] return chunks - def transform(self, xs: float, ys: float) -> tuple[float, float]: + def transform(self, xs: float, ys: float) -> Tuple[float, float]: transformer = pyproj.Transformer.from_crs( self.crs.geodetic_crs, self.crs, always_xy=True ) @@ -478,13 +478,13 @@ def _band_attrs(self, band_name: str) -> types.BandInfo: raise ValueError(f'Band {band_name!r} not found.') from e @functools.lru_cache() - def _bands(self) -> list[str]: + def _bands(self) -> List[str]: return [b['id'] for b in self._img_info['bands']] - def _make_attrs_valid(self, attrs: dict[str, Any]) -> dict[ + def _make_attrs_valid(self, attrs: Dict[str, Any]) -> Dict[ str, Union[ - str, int, float, complex, np.ndarray, np.number, list[Any], tuple[Any] + str, int, float, complex, np.ndarray, np.number, List[Any], Tuple[Any] ], ]: return { @@ -520,7 +520,7 @@ def get_dimensions(self) -> utils.Frozen[str, int]: def get_attrs(self) -> utils.Frozen[Any, Any]: return utils.FrozenDict(self._props) - def _get_primary_coordinates(self) -> list[Any]: + def _get_primary_coordinates(self) -> List[Any]: """Gets the primary dimension coordinate values from an ImageCollection.""" _, primary_coords = self.image_collection_properties @@ -654,8 +654,8 @@ def __getitem__(self, key: indexing.ExplicitIndexer) -> np.typing.ArrayLike: ) def _key_to_slices( - self, key: tuple[Union[int, slice], ...] - ) -> tuple[tuple[slice, ...], tuple[int, ...]]: + self, key: Tuple[Union[int, slice], ...] + ) -> Tuple[Tuple[slice, ...], Tuple[int, ...]]: """Convert all key indexes to slices. If any keys are integers, convert them to a slice (i.e. with a range of 1 @@ -712,7 +712,7 @@ def reduce_bands(x, acc): return target_image def _raw_indexing_method( - self, key: tuple[Union[int, slice], ...] + self, key: Tuple[Union[int, slice], ...] ) -> np.typing.ArrayLike: key, squeeze_axes = self._key_to_slices(key) @@ -777,8 +777,8 @@ def _raw_indexing_method( return out def _make_tile( - self, tile_index: tuple[types.TileIndex, types.BBox3d] - ) -> tuple[types.TileIndex, np.ndarray]: + self, tile_index: Tuple[types.TileIndex, types.BBox3d] + ) -> Tuple[types.TileIndex, np.ndarray]: """Get a numpy array from EE for a specific 3D bounding box (a 'tile').""" tile_idx, (istart, iend, *bbox) = tile_index target_image = self._slice_collection(slice(istart, iend)) @@ -788,7 +788,7 @@ def _make_tile( def _tile_indexes( self, index_range: slice, bbox: types.BBox - ) -> Iterable[tuple[types.TileIndex, types.BBox3d]]: + ) -> Iterable[Tuple[types.TileIndex, types.BBox3d]]: """Calculate indexes to break up a (3D) bounding box into chunks.""" tstep = self._apparent_chunks['index'] wstep = self._apparent_chunks['width'] @@ -836,7 +836,7 @@ def guess_can_open( def open_dataset( self, filename_or_obj: Union[str, os.PathLike[Any], ee.ImageCollection], - drop_variables: Optional[tuple[str, ...]] = None, + drop_variables: Optional[Tuple[str, ...]] = None, io_chunks: Optional[Any] = None, n_images: int = -1, mask_and_scale: bool = True, diff --git a/xee/micro_benchmarks.py b/xee/micro_benchmarks.py index 18d2af8..1e259a0 100644 --- a/xee/micro_benchmarks.py +++ b/xee/micro_benchmarks.py @@ -22,6 +22,7 @@ import os import tempfile import timeit +from typing import List from absl import app import numpy as np @@ -70,7 +71,7 @@ def open_and_write() -> None: ds.to_zarr(os.path.join(tmpdir, 'imerg.zarr')) -def main(_: list[str]) -> None: +def main(_: List[str]) -> None: print('Initializing EE...') init_ee_for_tests() print(f'[{REPEAT} time(s) with {LOOPS} loop(s) each.]') diff --git a/xee/types.py b/xee/types.py index 3789c53..a86e0f6 100644 --- a/xee/types.py +++ b/xee/types.py @@ -13,16 +13,16 @@ # limitations under the License. # ============================================================================== """Type definitions for Earth Engine concepts (and others).""" -from typing import Union, TypedDict +from typing import Dict, List, Tuple, Union, TypedDict -TileIndex = tuple[int, int, int] +TileIndex = Tuple[int, int, int] # x_min, y_min, x_max, y_max -Bounds = tuple[float, float, float, float] +Bounds = Tuple[float, float, float, float] # x_start, y_start, x_stop, y_stop -BBox = tuple[int, int, int, int] +BBox = Tuple[int, int, int, int] # index_start, index_stop, x_start, y_start, x_stop, y_stop -BBox3d = tuple[int, int, int, int, int, int] -Grid = dict[str, Union[dict[str, Union[float, str]], str, float]] +BBox3d = Tuple[int, int, int, int, int, int] +Grid = Dict[str, Union[Dict[str, Union[float, str]], str, float]] class DataType(TypedDict): @@ -34,14 +34,14 @@ class DataType(TypedDict): class BandInfo(TypedDict): crs: str - crs_transform: list[int] # len: 6, gdal order + crs_transform: List[int] # len: 6, gdal order data_type: DataType - dimensions: list[int] # len: 2 + dimensions: List[int] # len: 2 id: str class ImageInfo(TypedDict): type: str - bands: list[BandInfo] + bands: List[BandInfo] id: str version: int