Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make terracotta python 3.12 compatible #346

Merged
merged 3 commits into from
Oct 24, 2024

Conversation

jkittner
Copy link
Contributor

fix deprecation/removal of pkg_resources and rasterio is_tiled

if we were to drop 3.8 which is now EOL we wouldn't need the additional dependency of importlib_resources and the if/else imports.

fix deprecation/removal of pkg_resources and rasterio is_tiled
Copy link
Collaborator

@dionhaefner dionhaefner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Some nits but overall this is a great contribution.

I'm fine with dropping 3.8 if you think that's easier, your call.

terracotta/cog.py Outdated Show resolved Hide resolved
tests/cmaps/test_get_cmap.py Outdated Show resolved Hide resolved
@jkittner
Copy link
Contributor Author

I'm fine with dropping 3.8 if you think that's easier, your call.

Yeah, imo that's much easier and actually less code.

With having python 3.9 there are nice syntax upgrades one could make. pyupgrade suggests the following:

huge diff
diff --git a/docs/_static/example-first-ingestion-script.py b/docs/_static/example-first-ingestion-script.py
index 805fc7e..5d2d05b 100644
--- a/docs/_static/example-first-ingestion-script.py
+++ b/docs/_static/example-first-ingestion-script.py
@@ -46,7 +46,7 @@ RASTER_FILES = [
 ]
 
 
-def load(db_name: str, keys: List[str], raster_files: List[Dict]):
+def load(db_name: str, keys: list[str], raster_files: list[dict]):
     # get a TerracottaDriver that we can use to interact with
     # the database
     driver = terracotta.get_driver(db_name)
diff --git a/docs/conf.py b/docs/conf.py
index 0227961..b3c4281 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # Configuration file for the Sphinx documentation builder.
 #
diff --git a/terracotta/__init__.py b/terracotta/__init__.py
index 93afc3b..36a144f 100644
--- a/terracotta/__init__.py
+++ b/terracotta/__init__.py
@@ -14,11 +14,12 @@ except ImportError:  # pragma: no cover
     ) from None
 
 # initialize settings, define settings API
-from typing import Mapping, Any, Set
+from typing import Any
+from collections.abc import Mapping
 from terracotta.config import parse_config, TerracottaSettings
 
 _settings: TerracottaSettings = parse_config()
-_overwritten_settings: Set = set()
+_overwritten_settings: set[str] = set()
 
 
 def update_settings(**new_config: Any) -> None:
@@ -60,7 +61,7 @@ def get_settings() -> TerracottaSettings:  # noqa: F821
 
 
 del parse_config, TerracottaSettings
-del Mapping, Any, Set
+del Mapping, Any, set
 
 
 # expose API
diff --git a/terracotta/cache.py b/terracotta/cache.py
index 27b9f3f..4bdbc5c 100644
--- a/terracotta/cache.py
+++ b/terracotta/cache.py
@@ -3,7 +3,7 @@
 Custom cache implementations.
 """
 
-from typing import Tuple, Callable, Any
+from typing import Callable, Any
 
 import sys
 import zlib
@@ -11,7 +11,7 @@ import zlib
 import numpy as np
 from cachetools import LFUCache
 
-CompressionTuple = Tuple[bytes, bytes, str, Tuple[int, int]]
+CompressionTuple = tuple[bytes, bytes, str, tuple[int, int]]
 SizeFunction = Callable[[CompressionTuple], int]
 
 
@@ -50,6 +50,6 @@ class CompressedLFUCache(LFUCache):
         return np.ma.masked_array(data, mask=mask)
 
     @staticmethod
-    def _get_size(x: Tuple) -> int:
+    def _get_size(x: tuple) -> int:
         sizes = map(sys.getsizeof, x)
         return sum(sizes)
diff --git a/terracotta/cmaps/get_cmaps.py b/terracotta/cmaps/get_cmaps.py
index 2e36c63..f9ce688 100644
--- a/terracotta/cmaps/get_cmaps.py
+++ b/terracotta/cmaps/get_cmaps.py
@@ -4,7 +4,6 @@ Define an interface to retrieve stored color maps.
 """
 
 import importlib.resources
-from typing import Dict
 import os
 import numpy as np
 
@@ -19,7 +18,7 @@ except ModuleNotFoundError:
     PACKAGE_DIR = os.path.join(os.path.dirname(__file__), "data")
 
 
-def _get_cmap_files() -> Dict[str, str]:
+def _get_cmap_files() -> dict[str, str]:
     cmap_files = {}
     for f in os.listdir(PACKAGE_DIR):
         if not f.endswith(SUFFIX):
@@ -32,7 +31,7 @@ def _get_cmap_files() -> Dict[str, str]:
         return cmap_files
 
     if not os.path.isdir(EXTRA_CMAP_FOLDER):
-        raise IOError(f"invalid TC_EXTRA_CMAP_FOLDER: {EXTRA_CMAP_FOLDER}")
+        raise OSError(f"invalid TC_EXTRA_CMAP_FOLDER: {EXTRA_CMAP_FOLDER}")
 
     for f in os.listdir(EXTRA_CMAP_FOLDER):
         if not f.endswith(SUFFIX):
diff --git a/terracotta/cog.py b/terracotta/cog.py
index 68c2ce8..e3880b0 100644
--- a/terracotta/cog.py
+++ b/terracotta/cog.py
@@ -3,7 +3,7 @@
 Provides a validator for cloud-optimized GeoTiff.
 """
 
-from typing import Tuple, List, Dict, Any
+from typing import Any
 
 import os
 
@@ -11,7 +11,7 @@ import rasterio
 from rasterio.env import GDALVersion
 from rasterio._base import DatasetBase
 
-ValidationInfo = Tuple[List[str], List[str], Dict[str, Any]]
+ValidationInfo = tuple[list[str], list[str], dict[str, Any]]
 
 
 def validate(src_path: str, strict: bool = True) -> bool:
@@ -46,9 +46,9 @@ def check_raster_file(src_path: str) -> ValidationInfo:  # pragma: no cover
     This function is the rasterio equivalent of
     https://svn.osgeo.org/gdal/trunk/gdal/swig/python/samples/validate_cloud_optimized_geotiff.py
     """
-    errors: List[str] = []
-    warnings: List[str] = []
-    details: Dict[str, Any] = {}
+    errors: list[str] = []
+    warnings: list[str] = []
+    details: dict[str, Any] = {}
 
     if not GDALVersion.runtime().at_least("2.2"):
         raise RuntimeError("GDAL 2.2 or above required")
@@ -109,16 +109,14 @@ def check_raster_file(src_path: str) -> ValidationInfo:  # pragma: no cover
                 # https://github.com/mapbox/rasterio/blob/4ebdaa08cdcc65b141ed3fe95cf8bbdd9117bc0b/rasterio/_base.pyx
                 # We just need to make sure the decimation level is > 1
                 if not dec > 1:
-                    errors.append(
-                        "Invalid Decimation {} for overview level {}".format(dec, ix)
-                    )
+                    errors.append(f"Invalid Decimation {dec} for overview level {ix}")
 
                 # Check that the IFD of descending overviews are sorted by increasing
                 # offsets
                 ifd_offset = int(src.get_tag_item("IFD_OFFSET", "TIFF", bidx=1, ovr=ix))
                 ifd_offsets.append(ifd_offset)
 
-                details["ifd_offsets"]["overview_{}".format(ix)] = ifd_offset
+                details["ifd_offsets"][f"overview_{ix}"] = ifd_offset
                 if ifd_offsets[-1] < ifd_offsets[-2]:
                     if ix == 0:
                         errors.append(
@@ -150,7 +148,7 @@ def check_raster_file(src_path: str) -> ValidationInfo:  # pragma: no cover
                 )
                 data_offset = int(block_offset) if block_offset else 0
                 data_offsets.append(data_offset)
-                details["data_offsets"]["overview_{}".format(ix)] = data_offset
+                details["data_offsets"][f"overview_{ix}"] = data_offset
 
             if data_offsets[-1] != 0 and data_offsets[-1] < ifd_offsets[-1]:
                 if len(overviews) > 0:
@@ -183,6 +181,6 @@ def check_raster_file(src_path: str) -> ValidationInfo:  # pragma: no cover
             with rasterio.open(src_path, OVERVIEW_LEVEL=ix) as ovr_dst:
                 if ovr_dst.width > 512 and ovr_dst.height > 512:
                     if not is_tiled(ovr_dst):
-                        errors.append("Overview of index {} is not tiled".format(ix))
+                        errors.append(f"Overview of index {ix} is not tiled")
 
     return errors, warnings, details
diff --git a/terracotta/config.py b/terracotta/config.py
index 791f6f3..8aa2d4c 100644
--- a/terracotta/config.py
+++ b/terracotta/config.py
@@ -3,7 +3,8 @@
 Terracotta settings parsing.
 """
 
-from typing import Mapping, Any, Tuple, NamedTuple, Dict, List, Optional
+from typing import Any, NamedTuple, Optional
+from collections.abc import Mapping
 import os
 import json
 import tempfile
@@ -42,10 +43,10 @@ class TerracottaSettings(NamedTuple):
     RASTER_CACHE_COMPRESS_LEVEL: int = 9
 
     #: Tile size to return if not given in parameters
-    DEFAULT_TILE_SIZE: Tuple[int, int] = (256, 256)
+    DEFAULT_TILE_SIZE: tuple[int, int] = (256, 256)
 
     #: Maximum size to use when lazy loading metadata (less is faster but less accurate)
-    LAZY_LOADING_MAX_SHAPE: Tuple[int, int] = (1024, 1024)
+    LAZY_LOADING_MAX_SHAPE: tuple[int, int] = (1024, 1024)
 
     #: Compression level of output PNGs, from 0-9
     PNG_COMPRESS_LEVEL: int = 1
@@ -66,10 +67,10 @@ class TerracottaSettings(NamedTuple):
     REPROJECTION_METHOD: str = "linear"
 
     #: CORS allowed origins for metadata endpoint
-    ALLOWED_ORIGINS_METADATA: List[str] = ["*"]
+    ALLOWED_ORIGINS_METADATA: list[str] = ["*"]
 
     #: CORS allowed origins for tiles endpoints
-    ALLOWED_ORIGINS_TILES: List[str] = [r"http[s]?://(localhost|127\.0\.0\.1):*"]
+    ALLOWED_ORIGINS_TILES: list[str] = [r"http[s]?://(localhost|127\.0\.0\.1):*"]
 
     #: SQL database username (if not given in driver path)
     SQL_USER: Optional[str] = None
@@ -96,9 +97,9 @@ class TerracottaSettings(NamedTuple):
     MAX_POST_METADATA_KEYS: int = 100
 
 
-AVAILABLE_SETTINGS: Tuple[str, ...] = TerracottaSettings._fields
+AVAILABLE_SETTINGS: tuple[str, ...] = TerracottaSettings._fields
 
-DEPRECATION_MAP: Dict[str, str] = {
+DEPRECATION_MAP: dict[str, str] = {
     # TODO: Remove in v0.8.0
     "MYSQL_USER": "SQL_USER",
     "MYSQL_PASSWORD": "SQL_PASSWORD",
@@ -164,7 +165,7 @@ class SettingSchema(Schema):
     MAX_POST_METADATA_KEYS = fields.Integer(validate=validate.Range(min=1))
 
     @pre_load
-    def decode_lists(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
+    def decode_lists(self, data: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
         for var in (
             "DEFAULT_TILE_SIZE",
             "LAZY_LOADING_MAX_SHAPE",
@@ -183,8 +184,8 @@ class SettingSchema(Schema):
 
     @pre_load
     def handle_deprecated_fields(
-        self, data: Dict[str, Any], **kwargs: Any
-    ) -> Dict[str, Any]:
+        self, data: dict[str, Any], **kwargs: Any
+    ) -> dict[str, Any]:
         for deprecated_field, new_field in DEPRECATION_MAP.items():
             if data.get(deprecated_field):
                 warnings.warn(
@@ -201,7 +202,7 @@ class SettingSchema(Schema):
         return data
 
     @post_load
-    def make_settings(self, data: Dict[str, Any], **kwargs: Any) -> TerracottaSettings:
+    def make_settings(self, data: dict[str, Any], **kwargs: Any) -> TerracottaSettings:
         # encode tuples
         for var in (
             "DEFAULT_TILE_SIZE",
diff --git a/terracotta/drivers/__init__.py b/terracotta/drivers/__init__.py
index 08971aa..899ebfc 100644
--- a/terracotta/drivers/__init__.py
+++ b/terracotta/drivers/__init__.py
@@ -4,7 +4,7 @@ Define an interface to retrieve Terracotta drivers.
 """
 
 import os
-from typing import Optional, Union, Tuple, Dict, Type
+from typing import Optional, Union
 import urllib.parse as urlparse
 from pathlib import Path
 
@@ -15,7 +15,7 @@ from terracotta.drivers.geotiff_raster_store import GeoTiffRasterStore
 URLOrPathType = Union[str, Path]
 
 
-def load_driver(provider: str) -> Type[MetaStore]:
+def load_driver(provider: str) -> type[MetaStore]:
     if provider == "sqlite-remote":
         from terracotta.drivers.sqlite_remote_meta_store import RemoteSQLiteMetaStore
 
@@ -55,7 +55,7 @@ def auto_detect_provider(url_or_path: str) -> str:
     return "sqlite"
 
 
-_DRIVER_CACHE: Dict[Tuple[URLOrPathType, str, int], TerracottaDriver] = {}
+_DRIVER_CACHE: dict[tuple[URLOrPathType, str, int], TerracottaDriver] = {}
 
 
 def get_driver(
diff --git a/terracotta/drivers/base_classes.py b/terracotta/drivers/base_classes.py
index c1a6d3b..37ff566 100644
--- a/terracotta/drivers/base_classes.py
+++ b/terracotta/drivers/base_classes.py
@@ -10,20 +10,16 @@ from collections import OrderedDict
 from typing import (
     Any,
     Callable,
-    Dict,
-    List,
-    Mapping,
     Optional,
-    Sequence,
-    Tuple,
     TypeVar,
     Union,
 )
+from collections.abc import Mapping, Sequence
 
 from terracotta import exceptions
 
 KeysType = Mapping[str, str]
-MultiValueKeysType = Mapping[str, Union[str, List[str]]]
+MultiValueKeysType = Mapping[str, Union[str, list[str]]]
 Number = TypeVar("Number", int, float)
 T = TypeVar("T")
 
@@ -56,7 +52,7 @@ class MetaStore(ABC):
 
     @property
     @abstractmethod
-    def key_names(self) -> Tuple[str, ...]:
+    def key_names(self) -> tuple[str, ...]:
         """Names of all keys defined by the database."""
         pass
 
@@ -99,14 +95,14 @@ class MetaStore(ABC):
         where: Optional[MultiValueKeysType] = None,
         page: int = 0,
         limit: Optional[int] = None,
-    ) -> Dict[Tuple[str, ...], Any]:
+    ) -> dict[tuple[str, ...], Any]:
         """Get all known dataset key combinations matching the given constraints,
         and a path to retrieve the data
         """
         pass
 
     @abstractmethod
-    def get_metadata(self, keys: KeysType) -> Optional[Dict[str, Any]]:
+    def get_metadata(self, keys: KeysType) -> Optional[dict[str, Any]]:
         """Return all stored metadata for given keys."""
         pass
 
@@ -154,7 +150,7 @@ class RasterStore(ABC):
         extra_metadata: Optional[Any] = None,
         use_chunks: Optional[bool] = None,
         max_shape: Optional[Sequence[int]] = None,
-    ) -> Dict[str, Any]:
+    ) -> dict[str, Any]:
         """Compute metadata for a given input file"""
         pass
 
diff --git a/terracotta/drivers/geotiff_raster_store.py b/terracotta/drivers/geotiff_raster_store.py
index 26928c9..f5efbb2 100644
--- a/terracotta/drivers/geotiff_raster_store.py
+++ b/terracotta/drivers/geotiff_raster_store.py
@@ -3,7 +3,8 @@
 Base class for drivers operating on physical raster files.
 """
 
-from typing import Optional, Any, Callable, Sequence, Dict, TypeVar
+from typing import Optional, Any, Callable, TypeVar
+from collections.abc import Sequence
 from concurrent.futures import Future, Executor, ProcessPoolExecutor, ThreadPoolExecutor
 from concurrent.futures.process import BrokenProcessPool
 
@@ -102,7 +103,7 @@ class GeoTiffRasterStore(RasterStore):
         extra_metadata: Optional[Any] = None,
         use_chunks: Optional[bool] = None,
         max_shape: Optional[Sequence[int]] = None
-    ) -> Dict[str, Any]:
+    ) -> dict[str, Any]:
         return raster.compute_metadata(
             path,
             extra_metadata=extra_metadata,
@@ -130,7 +131,7 @@ class GeoTiffRasterStore(RasterStore):
         if tile_size is None:
             tile_size = settings.DEFAULT_TILE_SIZE
 
-        kwargs = dict(
+        kwargs: dict[str, Any] = dict(
             path=path,
             tile_bounds=tile_bounds,
             tile_size=tuple(tile_size),
diff --git a/terracotta/drivers/mysql_meta_store.py b/terracotta/drivers/mysql_meta_store.py
index d81b217..6418023 100644
--- a/terracotta/drivers/mysql_meta_store.py
+++ b/terracotta/drivers/mysql_meta_store.py
@@ -4,7 +4,8 @@ MySQL-backed metadata driver. Metadata is stored in a MySQL database.
 """
 
 import functools
-from typing import Optional, Mapping, Sequence
+from typing import Optional
+from collections.abc import Mapping, Sequence
 
 import sqlalchemy as sqla
 from sqlalchemy.dialects.mysql import TEXT, VARCHAR
diff --git a/terracotta/drivers/postgresql_meta_store.py b/terracotta/drivers/postgresql_meta_store.py
index 9c944e1..8e18041 100644
--- a/terracotta/drivers/postgresql_meta_store.py
+++ b/terracotta/drivers/postgresql_meta_store.py
@@ -3,7 +3,8 @@
 PostgreSQL-backed metadata driver. Metadata is stored in a PostgreSQL database.
 """
 
-from typing import Optional, Mapping, Sequence
+from typing import Optional
+from collections.abc import Mapping, Sequence
 
 import sqlalchemy as sqla
 from terracotta.drivers.relational_meta_store import RelationalMetaStore
diff --git a/terracotta/drivers/relational_meta_store.py b/terracotta/drivers/relational_meta_store.py
index d5d7dd4..7bdb317 100644
--- a/terracotta/drivers/relational_meta_store.py
+++ b/terracotta/drivers/relational_meta_store.py
@@ -10,7 +10,8 @@ import re
 import urllib.parse as urlparse
 from abc import ABC, abstractmethod
 from collections import OrderedDict
-from typing import Any, Dict, Iterator, Mapping, Optional, Sequence, Tuple, Type, Union
+from typing import Any, Optional, Union
+from collections.abc import Iterator, Mapping, Sequence
 
 import numpy as np
 import sqlalchemy as sqla
@@ -31,14 +32,14 @@ _ERROR_ON_CONNECT = (
     "to a valid Terracotta database, and that you ran driver.create()."
 )
 
-DATABASE_DRIVER_EXCEPTIONS_TO_CONVERT: Tuple[Type[Exception], ...] = (
+DATABASE_DRIVER_EXCEPTIONS_TO_CONVERT: tuple[type[Exception], ...] = (
     sqla.exc.OperationalError,
     sqla.exc.InternalError,
     sqla.exc.ProgrammingError,
     sqla.exc.InvalidRequestError,
 )
 
-ExceptionType = Union[Type[Exception], Tuple[Type[Exception], ...]]
+ExceptionType = Union[type[Exception], tuple[type[Exception], ...]]
 
 
 @contextlib.contextmanager
@@ -70,13 +71,13 @@ class RelationalMetaStore(MetaStore, ABC):
     SQL_TIMEOUT_KEY: str
 
     SQLA_STRING: Any = sqla.types.String
-    SQLA_METADATA_TYPE_LOOKUP: Dict[str, Any] = {
+    SQLA_METADATA_TYPE_LOOKUP: dict[str, Any] = {
         "real": functools.partial(sqla.types.Float, precision=8),
         "text": sqla.types.Text,
         "blob": sqla.types.LargeBinary,
     }
 
-    _METADATA_COLUMNS: Tuple[Tuple[str, str], ...] = (
+    _METADATA_COLUMNS: tuple[tuple[str, str], ...] = (
         ("bounds_north", "real"),
         ("bounds_east", "real"),
         ("bounds_south", "real"),
@@ -310,7 +311,7 @@ class RelationalMetaStore(MetaStore, ABC):
         return OrderedDict((row.key_name, row.description) for row in result.all())
 
     @property
-    def key_names(self) -> Tuple[str, ...]:
+    def key_names(self) -> tuple[str, ...]:
         """Names of all keys defined by the database"""
         if self._db_keys is None:
             self._db_keys = self.get_keys()
@@ -323,7 +324,7 @@ class RelationalMetaStore(MetaStore, ABC):
         where: Optional[MultiValueKeysType] = None,
         page: int = 0,
         limit: Optional[int] = None,
-    ) -> Dict[Tuple[str, ...], str]:
+    ) -> dict[tuple[str, ...], str]:
         if where is None:
             where = {}
 
@@ -352,7 +353,7 @@ class RelationalMetaStore(MetaStore, ABC):
         with self.connect() as conn:
             result = conn.execute(stmt).all()
 
-        def keytuple(row: sqla.engine.row.Row) -> Tuple[str, ...]:
+        def keytuple(row: sqla.engine.row.Row) -> tuple[str, ...]:
             return tuple(getattr(row, key) for key in self.key_names)
 
         datasets = {keytuple(row): row.path for row in result}
@@ -360,7 +361,7 @@ class RelationalMetaStore(MetaStore, ABC):
 
     @trace("get_metadata")
     @convert_exceptions("Could not retrieve metadata")
-    def get_metadata(self, keys: KeysType) -> Optional[Dict[str, Any]]:
+    def get_metadata(self, keys: KeysType) -> Optional[dict[str, Any]]:
         metadata_table = sqla.Table(
             "metadata", self.sqla_metadata, autoload_with=self.sqla_engine
         )
@@ -446,7 +447,7 @@ class RelationalMetaStore(MetaStore, ABC):
             )
 
     @staticmethod
-    def _encode_data(decoded: Mapping[str, Any]) -> Dict[str, Any]:
+    def _encode_data(decoded: Mapping[str, Any]) -> dict[str, Any]:
         """Transform from internal format to database representation"""
         encoded = {
             "bounds_north": decoded["bounds"][0],
@@ -465,7 +466,7 @@ class RelationalMetaStore(MetaStore, ABC):
         return encoded
 
     @staticmethod
-    def _decode_data(encoded: Mapping[str, Any]) -> Dict[str, Any]:
+    def _decode_data(encoded: Mapping[str, Any]) -> dict[str, Any]:
         """Transform from database format to internal representation"""
         decoded = {
             "bounds": tuple(
diff --git a/terracotta/drivers/sqlite_remote_meta_store.py b/terracotta/drivers/sqlite_remote_meta_store.py
index de9ab44..37cdf4e 100644
--- a/terracotta/drivers/sqlite_remote_meta_store.py
+++ b/terracotta/drivers/sqlite_remote_meta_store.py
@@ -11,7 +11,8 @@ import tempfile
 import time
 import urllib.parse as urlparse
 from pathlib import Path
-from typing import Iterator, Union
+from typing import Union
+from collections.abc import Iterator
 
 from terracotta import exceptions, get_settings
 from terracotta.drivers.sqlite_meta_store import SQLiteMetaStore
diff --git a/terracotta/drivers/terracotta_driver.py b/terracotta/drivers/terracotta_driver.py
index 3f40a7b..c4553c3 100644
--- a/terracotta/drivers/terracotta_driver.py
+++ b/terracotta/drivers/terracotta_driver.py
@@ -7,16 +7,11 @@ import contextlib
 from collections import OrderedDict
 from typing import (
     Any,
-    Collection,
-    Dict,
-    List,
-    Mapping,
     Optional,
-    Sequence,
-    Tuple,
     TypeVar,
     Union,
 )
+from collections.abc import Collection, Mapping, Sequence
 
 import terracotta
 from terracotta import exceptions
@@ -28,7 +23,7 @@ from terracotta.drivers.base_classes import (
 )
 
 ExtendedKeysType = Union[Sequence[str], Mapping[str, str]]
-ExtendedMultiValueKeysType = Union[Sequence[str], Mapping[str, Union[str, List[str]]]]
+ExtendedMultiValueKeysType = Union[Sequence[str], Mapping[str, Union[str, list[str]]]]
 T = TypeVar("T")
 
 
@@ -48,7 +43,7 @@ class TerracottaDriver:
         self.raster_store = raster_store
 
         settings = terracotta.get_settings()
-        self.LAZY_LOADING_MAX_SHAPE: Tuple[int, int] = settings.LAZY_LOADING_MAX_SHAPE
+        self.LAZY_LOADING_MAX_SHAPE: tuple[int, int] = settings.LAZY_LOADING_MAX_SHAPE
 
     @property
     def db_version(self) -> str:
@@ -62,7 +57,7 @@ class TerracottaDriver:
         return self.meta_store.db_version
 
     @property
-    def key_names(self) -> Tuple[str, ...]:
+    def key_names(self) -> tuple[str, ...]:
         """Get names of all keys defined by the meta store.
 
         Returns:
@@ -133,7 +128,7 @@ class TerracottaDriver:
         where: Optional[ExtendedMultiValueKeysType] = None,
         page: int = 0,
         limit: Optional[int] = None,
-    ) -> Dict[Tuple[str, ...], Any]:
+    ) -> dict[tuple[str, ...], Any]:
         """Get all known dataset key combinations matching the given constraints,
         and a path to retrieve the data (dependent on the raster store).
 
@@ -154,7 +149,7 @@ class TerracottaDriver:
             limit=limit,
         )
 
-    def get_metadata(self, keys: ExtendedKeysType) -> Dict[str, Any]:
+    def get_metadata(self, keys: ExtendedKeysType) -> dict[str, Any]:
         """Return all stored metadata for given keys.
 
         Arguments:
@@ -297,7 +292,7 @@ class TerracottaDriver:
         extra_metadata: Optional[Any] = None,
         use_chunks: Optional[bool] = None,
         max_shape: Optional[Sequence[int]] = None,
-    ) -> Dict[str, Any]:
+    ) -> dict[str, Any]:
         """Compute metadata for a dataset.
 
         Arguments:
@@ -347,7 +342,7 @@ class TerracottaDriver:
         self,
         keys: Union[ExtendedKeysType, Optional[MultiValueKeysType]],
         requires_all_keys: bool = True,
-    ) -> Dict[str, Any]:
+    ) -> dict[str, Any]:
         if requires_all_keys and (keys is None or len(keys) != len(self.key_names)):
             raise exceptions.InvalidKeyError(
                 f"Got wrong number of keys (available keys: {self.key_names})"
diff --git a/terracotta/expressions.py b/terracotta/expressions.py
index 5d93b5a..485029f 100644
--- a/terracotta/expressions.py
+++ b/terracotta/expressions.py
@@ -3,7 +3,8 @@
 Safe execution of user-supplied math expressions
 """
 
-from typing import Mapping, Dict, Tuple, Callable, Type, Any
+from typing import Callable, Any
+from collections.abc import Mapping
 import ast
 import operator
 import concurrent.futures
@@ -11,7 +12,7 @@ import concurrent.futures
 import numpy as np
 
 
-EXTRA_CALLABLES: Dict[str, Tuple[Callable, int]] = {
+EXTRA_CALLABLES: dict[str, tuple[Callable, int]] = {
     # 'name': (callable, nargs)
     # mask operations
     "where": (np.ma.where, 3),
@@ -63,7 +64,7 @@ class ParseException(Exception):
 
 
 class ExpressionParser(ast.NodeVisitor):
-    NODE_TO_BINOP: Dict[Type[ast.operator], Callable] = {
+    NODE_TO_BINOP: dict[type[ast.operator], Callable] = {
         # math
         ast.Add: operator.add,
         ast.Sub: operator.sub,
@@ -76,12 +77,12 @@ class ExpressionParser(ast.NodeVisitor):
         ast.BitOr: operator.or_,
     }
 
-    NODE_TO_UNOP: Dict[Type[ast.unaryop], Callable] = {
+    NODE_TO_UNOP: dict[type[ast.unaryop], Callable] = {
         ast.Invert: operator.invert,
         ast.USub: operator.neg,
     }
 
-    NODE_TO_COMPOP: Dict[Type[ast.cmpop], Callable] = {
+    NODE_TO_COMPOP: dict[type[ast.cmpop], Callable] = {
         ast.Eq: operator.eq,
         ast.NotEq: operator.ne,
         ast.Lt: operator.lt,
@@ -93,7 +94,7 @@ class ExpressionParser(ast.NodeVisitor):
     def __init__(
         self,
         constants: Mapping[str, Any],
-        callables: Mapping[str, Tuple[Callable, int]],
+        callables: Mapping[str, tuple[Callable, int]],
     ) -> None:
         self.constants = constants
         self.callables = callables
diff --git a/terracotta/handlers/colormap.py b/terracotta/handlers/colormap.py
index 97ef93f..e462d75 100644
--- a/terracotta/handlers/colormap.py
+++ b/terracotta/handlers/colormap.py
@@ -3,7 +3,7 @@
 Handle /colormap API endpoint.
 """
 
-from typing import Optional, List, Tuple, TypeVar, Dict, Any
+from typing import Optional, TypeVar, Any
 
 import numpy as np
 
@@ -15,10 +15,10 @@ Number = TypeVar("Number", "int", "float")
 @trace("colormap_handler")
 def colormap(
     *,
-    stretch_range: Tuple[Number, Number],
+    stretch_range: tuple[Number, Number],
     colormap: Optional[str] = None,
     num_values: int = 255
-) -> List[Dict[str, Any]]:
+) -> list[dict[str, Any]]:
     """Returns a list [{value=pixel value, rgba=rgba tuple}] for given stretch parameters"""
     from terracotta import image
 
diff --git a/terracotta/handlers/compute.py b/terracotta/handlers/compute.py
index 48e4395..bacccfb 100644
--- a/terracotta/handlers/compute.py
+++ b/terracotta/handlers/compute.py
@@ -3,7 +3,8 @@
 Handle /compute API endpoint. Band file retrieval is multi-threaded.
 """
 
-from typing import Sequence, Tuple, Mapping, Optional, TypeVar
+from typing import Optional, TypeVar
+from collections.abc import Sequence, Mapping
 from typing.io import BinaryIO
 from concurrent.futures import Future
 
@@ -11,7 +12,7 @@ from terracotta import get_settings, get_driver, image, xyz, exceptions
 from terracotta.profile import trace
 
 Number = TypeVar("Number", int, float)
-RangeType = Optional[Tuple[Optional[Number], Optional[Number]]]
+RangeType = Optional[tuple[Optional[Number], Optional[Number]]]
 
 
 @trace("compute_handler")
@@ -19,11 +20,11 @@ def compute(
     expression: str,
     some_keys: Sequence[str],
     operand_keys: Mapping[str, str],
-    stretch_range: Tuple[Number, Number],
-    tile_xyz: Optional[Tuple[int, int, int]] = None,
+    stretch_range: tuple[Number, Number],
+    tile_xyz: Optional[tuple[int, int, int]] = None,
     *,
     colormap: Optional[str] = None,
-    tile_size: Optional[Tuple[int, int]] = None,
+    tile_size: Optional[tuple[int, int]] = None,
 ) -> BinaryIO:
     """Return singleband image computed from one or more images as PNG
 
diff --git a/terracotta/handlers/datasets.py b/terracotta/handlers/datasets.py
index b19c58d..5dae643 100644
--- a/terracotta/handlers/datasets.py
+++ b/terracotta/handlers/datasets.py
@@ -3,7 +3,8 @@
 Handle /datasets API endpoint.
 """
 
-from typing import Optional, Mapping, List, Union  # noqa: F401
+from typing import Optional, List, Union  # noqa: F401
+from collections.abc import Mapping
 from collections import OrderedDict
 
 from terracotta import get_settings, get_driver
@@ -12,7 +13,7 @@ from terracotta.profile import trace
 
 @trace("datasets_handler")
 def datasets(
-    some_keys: Optional[Mapping[str, Union[str, List[str]]]] = None,
+    some_keys: Optional[Mapping[str, Union[str, list[str]]]] = None,
     page: int = 0,
     limit: int = 500,
 ) -> "List[OrderedDict[str, str]]":
diff --git a/terracotta/handlers/keys.py b/terracotta/handlers/keys.py
index 09f8076..ae36e9c 100644
--- a/terracotta/handlers/keys.py
+++ b/terracotta/handlers/keys.py
@@ -3,14 +3,12 @@
 Handle /keys API endpoint.
 """
 
-from typing import List, Dict
-
 from terracotta import get_settings, get_driver
 from terracotta.profile import trace
 
 
 @trace("keys_handler")
-def keys() -> List[Dict[str, str]]:
+def keys() -> list[dict[str, str]]:
     """List available keys, in order"""
     settings = get_settings()
     driver = get_driver(settings.DRIVER_PATH, provider=settings.DRIVER_PROVIDER)
diff --git a/terracotta/handlers/metadata.py b/terracotta/handlers/metadata.py
index 15b22e3..1e583e9 100644
--- a/terracotta/handlers/metadata.py
+++ b/terracotta/handlers/metadata.py
@@ -3,7 +3,8 @@
 Handle /metadata API endpoint.
 """
 
-from typing import Mapping, Sequence, Dict, Any, Union, List, Optional
+from typing import Any, Union, Optional
+from collections.abc import Mapping, Sequence
 from collections import OrderedDict
 
 from terracotta import get_settings, get_driver
@@ -12,8 +13,8 @@ from terracotta.profile import trace
 
 
 def filter_metadata(
-    metadata: Dict[str, Any], columns: Optional[List[str]]
-) -> Dict[str, Any]:
+    metadata: dict[str, Any], columns: Optional[list[str]]
+) -> dict[str, Any]:
     """Filter metadata by columns, if given"""
     assert (
         columns is None or len(columns) > 0
@@ -27,8 +28,8 @@ def filter_metadata(
 
 @trace("metadata_handler")
 def metadata(
-    columns: Optional[List[str]], keys: Union[Sequence[str], Mapping[str, str]]
-) -> Dict[str, Any]:
+    columns: Optional[list[str]], keys: Union[Sequence[str], Mapping[str, str]]
+) -> dict[str, Any]:
     """Returns all metadata for a single dataset"""
     settings = get_settings()
     driver = get_driver(settings.DRIVER_PATH, provider=settings.DRIVER_PROVIDER)
@@ -39,8 +40,8 @@ def metadata(
 
 @trace("multiple_metadata_handler")
 def multiple_metadata(
-    columns: Optional[List[str]], datasets: List[List[str]]
-) -> List[Dict[str, Any]]:
+    columns: Optional[list[str]], datasets: list[list[str]]
+) -> list[dict[str, Any]]:
     """Returns all metadata for multiple datasets"""
     settings = get_settings()
     driver = get_driver(settings.DRIVER_PATH, provider=settings.DRIVER_PROVIDER)
diff --git a/terracotta/handlers/rgb.py b/terracotta/handlers/rgb.py
index 6b75661..8a1680e 100644
--- a/terracotta/handlers/rgb.py
+++ b/terracotta/handlers/rgb.py
@@ -3,7 +3,8 @@
 Handle /rgb API endpoint. Band file retrieval is multi-threaded.
 """
 
-from typing import Sequence, Tuple, Optional, TypeVar
+from typing import Optional, TypeVar
+from collections.abc import Sequence
 from typing.io import BinaryIO
 from concurrent.futures import Future
 
@@ -12,7 +13,7 @@ from terracotta.profile import trace
 
 NumberOrString = TypeVar("NumberOrString", int, float, str)
 ListOfRanges = Sequence[
-    Optional[Tuple[Optional[NumberOrString], Optional[NumberOrString]]]
+    Optional[tuple[Optional[NumberOrString], Optional[NumberOrString]]]
 ]
 
 
@@ -20,10 +21,10 @@ ListOfRanges = Sequence[
 def rgb(
     some_keys: Sequence[str],
     rgb_values: Sequence[str],
-    tile_xyz: Optional[Tuple[int, int, int]] = None,
+    tile_xyz: Optional[tuple[int, int, int]] = None,
     *,
     stretch_ranges: Optional[ListOfRanges] = None,
-    tile_size: Optional[Tuple[int, int]] = None
+    tile_size: Optional[tuple[int, int]] = None
 ) -> BinaryIO:
     """Return RGB image as PNG
 
diff --git a/terracotta/handlers/singleband.py b/terracotta/handlers/singleband.py
index 15311fc..7ac163a 100644
--- a/terracotta/handlers/singleband.py
+++ b/terracotta/handlers/singleband.py
@@ -3,7 +3,8 @@
 Handle /singleband API endpoint.
 """
 
-from typing import Sequence, Mapping, Union, Tuple, Optional, TypeVar, cast
+from typing import Union, Optional, TypeVar, cast
+from collections.abc import Sequence, Mapping
 from typing.io import BinaryIO
 
 import collections
@@ -14,19 +15,19 @@ from terracotta.profile import trace
 Number = TypeVar("Number", int, float)
 NumberOrString = TypeVar("NumberOrString", int, float, str)
 ListOfRanges = Sequence[
-    Optional[Tuple[Optional[NumberOrString], Optional[NumberOrString]]]
+    Optional[tuple[Optional[NumberOrString], Optional[NumberOrString]]]
 ]
-RGBA = Tuple[Number, Number, Number, Number]
+RGBA = tuple[Number, Number, Number, Number]
 
 
 @trace("singleband_handler")
 def singleband(
     keys: Union[Sequence[str], Mapping[str, str]],
-    tile_xyz: Optional[Tuple[int, int, int]] = None,
+    tile_xyz: Optional[tuple[int, int, int]] = None,
     *,
     colormap: Union[str, Mapping[Number, RGBA], None] = None,
-    stretch_range: Optional[Tuple[NumberOrString, NumberOrString]] = None,
-    tile_size: Optional[Tuple[int, int]] = None
+    stretch_range: Optional[tuple[NumberOrString, NumberOrString]] = None,
+    tile_size: Optional[tuple[int, int]] = None
 ) -> BinaryIO:
     """Return singleband image as PNG"""
 
diff --git a/terracotta/image.py b/terracotta/image.py
index 44c5b66..720062c 100755
--- a/terracotta/image.py
+++ b/terracotta/image.py
@@ -3,7 +3,8 @@
 Utilities to create and manipulate images.
 """
 
-from typing import List, Sequence, Tuple, TypeVar, Union
+from typing import TypeVar, Union
+from collections.abc import Sequence
 from typing.io import BinaryIO
 
 from io import BytesIO
@@ -16,7 +17,7 @@ from terracotta import exceptions, get_settings
 
 Number = TypeVar("Number", int, float)
 NumberOrString = TypeVar("NumberOrString", int, float, str)
-RGBA = Tuple[Number, Number, Number, Number]
+RGBA = tuple[Number, Number, Number, Number]
 Palette = Sequence[RGBA]
 Array = Union[np.ndarray, np.ma.MaskedArray]
 
@@ -28,7 +29,7 @@ def array_to_png(
     """Encode an 8bit array as PNG"""
     from terracotta.cmaps import get_cmap
 
-    transparency: Union[Tuple[int, int, int], int, bytes]
+    transparency: Union[tuple[int, int, int], int, bytes]
 
     settings = get_settings()
     compress_level = settings.PNG_COMPRESS_LEVEL
@@ -116,7 +117,7 @@ def array_to_png(
     return sio
 
 
-def empty_image(size: Tuple[int, int]) -> BinaryIO:
+def empty_image(size: tuple[int, int]) -> BinaryIO:
     """Return a fully transparent PNG image of given size"""
     settings = get_settings()
     compress_level = settings.PNG_COMPRESS_LEVEL
@@ -185,7 +186,7 @@ def label(data: Array, labels: Sequence[Number]) -> Array:
 
 
 def get_stretch_scale(
-    scale: NumberOrString, percentiles: List[int]
+    scale: NumberOrString, percentiles: list[int]
 ) -> Union[int, float]:
     if isinstance(scale, (int, float)):
         return scale
diff --git a/terracotta/profile.py b/terracotta/profile.py
index c29582d..390eab1 100644
--- a/terracotta/profile.py
+++ b/terracotta/profile.py
@@ -3,7 +3,7 @@
 Decorators for performance tracing.
 """
 
-from typing import Iterator
+from collections.abc import Iterator
 
 import traceback
 import contextlib
diff --git a/terracotta/raster.py b/terracotta/raster.py
index 950ac48..1241e46 100644
--- a/terracotta/raster.py
+++ b/terracotta/raster.py
@@ -3,7 +3,8 @@
 Extract information from raster files through rasterio.
 """
 
-from typing import Optional, Any, Dict, Tuple, Sequence, TYPE_CHECKING
+from typing import Optional, Any, TYPE_CHECKING
+from collections.abc import Sequence
 import contextlib
 import warnings
 import logging
@@ -57,7 +58,7 @@ def convex_hull_candidate_mask(mask: np.ndarray) -> np.ndarray:
     return out
 
 
-def compute_image_stats_chunked(dataset: "DatasetReader") -> Optional[Dict[str, Any]]:
+def compute_image_stats_chunked(dataset: "DatasetReader") -> Optional[dict[str, Any]]:
     """Compute statistics for the given rasterio dataset by looping over chunks."""
     from rasterio import features, warp, windows
     from shapely import geometry
@@ -122,7 +123,7 @@ def compute_image_stats_chunked(dataset: "DatasetReader") -> Optional[Dict[str,
 
 def compute_image_stats(
     dataset: "DatasetReader", max_shape: Optional[Sequence[int]] = None
-) -> Optional[Dict[str, Any]]:
+) -> Optional[dict[str, Any]]:
     """Compute statistics for the given rasterio dataset by reading it into memory."""
     from rasterio import features, warp, transform
     from shapely import geometry
@@ -187,13 +188,13 @@ def compute_metadata(
     use_chunks: Optional[bool] = None,
     max_shape: Optional[Sequence[int]] = None,
     large_raster_threshold: Optional[int] = None,
-    rio_env_options: Optional[Dict[str, Any]] = None,
-) -> Dict[str, Any]:
+    rio_env_options: Optional[dict[str, Any]] = None,
+) -> dict[str, Any]:
     import rasterio
     from rasterio import warp
     from terracotta.cog import validate
 
-    row_data: Dict[str, Any] = {}
+    row_data: dict[str, Any] = {}
     extra_metadata = extra_metadata or {}
 
     if max_shape is not None and len(max_shape) != 2:
@@ -296,11 +297,11 @@ def get_raster_tile(
     *,
     reprojection_method: str = "nearest",
     resampling_method: str = "nearest",
-    tile_bounds: Optional[Tuple[float, float, float, float]] = None,
-    tile_size: Tuple[int, int] = (256, 256),
+    tile_bounds: Optional[tuple[float, float, float, float]] = None,
+    tile_size: tuple[int, int] = (256, 256),
     preserve_values: bool = False,
     target_crs: str = "epsg:3857",
-    rio_env_options: Optional[Dict[str, Any]] = None,
+    rio_env_options: Optional[dict[str, Any]] = None,
 ) -> np.ma.MaskedArray:
     """Load a raster dataset from a file through rasterio.
 
@@ -311,7 +312,7 @@ def get_raster_tile(
     from rasterio.vrt import WarpedVRT
     from affine import Affine
 
-    dst_bounds: Tuple[float, float, float, float]
+    dst_bounds: tuple[float, float, float, float]
 
     if rio_env_options is None:
         rio_env_options = {}
@@ -328,7 +329,7 @@ def get_raster_tile(
             with trace("open_dataset"):
                 src = es.enter_context(rasterio.open(path))
         except OSError:
-            raise IOError("error while reading file {}".format(path))
+            raise OSError(f"error while reading file {path}")
 
         # compute buonds in target CRS
         dst_bounds = warp.transform_bounds(src.crs, target_crs, *src.bounds)
diff --git a/terracotta/scripts/cli.py b/terracotta/scripts/cli.py
index cb11f8b..0cfc8fe 100644
--- a/terracotta/scripts/cli.py
+++ b/terracotta/scripts/cli.py
@@ -3,7 +3,8 @@
 Entry point for CLI.
 """
 
-from typing import Optional, Any, Mapping
+from typing import Optional, Any
+from collections.abc import Mapping
 import sys
 
 import click
diff --git a/terracotta/scripts/click_types.py b/terracotta/scripts/click_types.py
index cb282b4..a9c1cb6 100644
--- a/terracotta/scripts/click_types.py
+++ b/terracotta/scripts/click_types.py
@@ -3,7 +3,7 @@
 Custom click parameter types and utilities.
 """
 
-from typing import List, Any, Tuple, Dict
+from typing import Any
 import pathlib
 import glob
 import re
@@ -18,7 +18,7 @@ class GlobbityGlob(click.ParamType):
 
     name = "glob"
 
-    def convert(self, value: str, *args: Any) -> List[pathlib.Path]:
+    def convert(self, value: str, *args: Any) -> list[pathlib.Path]:
         return [pathlib.Path(f) for f in glob.glob(value)]
 
 
@@ -29,10 +29,10 @@ class PathlibPath(click.Path):
         return pathlib.Path(str(super().convert(*args)))
 
 
-RasterPatternType = Tuple[List[str], Dict[Tuple[str, ...], str]]
+RasterPatternType = tuple[list[str], dict[tuple[str, ...], str]]
 
 
-def _parse_raster_pattern(raster_pattern: str) -> Tuple[List[str], str, str]:
+def _parse_raster_pattern(raster_pattern: str) -> tuple[list[str], str, str]:
     """Parse a raster pattern string using Python format syntax.
 
     Extracts names of unique placeholders, a glob pattern
@@ -48,9 +48,9 @@ def _parse_raster_pattern(raster_pattern: str) -> Tuple[List[str], str, str]:
     # raises ValueError on invalid patterns
     parsed_value = string.Formatter().parse(raster_pattern)
 
-    keys: List[str] = []
-    glob_pattern: List[str] = []
-    regex_pattern: List[str] = []
+    keys: list[str] = []
+    glob_pattern: list[str] = []
+    regex_pattern: list[str] = []
 
     for before_field, field_name, _, _ in parsed_value:
         glob_pattern += before_field
@@ -123,7 +123,7 @@ class TOMLFile(click.ParamType):
 
     name = "toml-file"
 
-    def convert(self, value: str, *args: Any) -> Dict[str, Any]:
+    def convert(self, value: str, *args: Any) -> dict[str, Any]:
         import toml
 
         return dict(toml.load(value))
diff --git a/terracotta/scripts/connect.py b/terracotta/scripts/connect.py
index 69984c3..1f3c591 100644
--- a/terracotta/scripts/connect.py
+++ b/terracotta/scripts/connect.py
@@ -3,7 +3,8 @@
 Use Flask development server to serve Terracotta client app.
 """
 
-from typing import Optional, Sequence
+from typing import Optional
+from collections.abc import Sequence
 
 import os
 import json
diff --git a/terracotta/scripts/http_utils.py b/terracotta/scripts/http_utils.py
index 8ce933e..6c14186 100644
--- a/terracotta/scripts/http_utils.py
+++ b/terracotta/scripts/http_utils.py
@@ -3,7 +3,8 @@
 Various utilities to work with HTTP connections
 """
 
-from typing import Optional, Sequence
+from typing import Optional
+from collections.abc import Sequence
 
 
 def check_socket(host: str, port: int) -> bool:
@@ -17,7 +18,7 @@ def check_socket(host: str, port: int) -> bool:
             sock.bind((host, port))
             sock.listen(1)
             return True
-        except socket.error:
+        except OSError:
             return False
 
 
diff --git a/terracotta/scripts/ingest.py b/terracotta/scripts/ingest.py
index f3ce69e..0681d6e 100644
--- a/terracotta/scripts/ingest.py
+++ b/terracotta/scripts/ingest.py
@@ -3,7 +3,8 @@
 A convenience tool to create a Terracotta database from some raster files.
 """
 
-from typing import Optional, Tuple, Sequence, Any
+from typing import Optional, Any
+from collections.abc import Sequence
 from pathlib import Path
 import logging
 
@@ -83,7 +84,7 @@ def ingest(
         # re-order keys
         rgb_idx = keys.index(rgb_key)
 
-        def push_to_last(seq: Sequence[Any], index: int) -> Tuple[Any, ...]:
+        def push_to_last(seq: Sequence[Any], index: int) -> tuple[Any, ...]:
             return (*seq[:index], *seq[index + 1 :], seq[index])
 
         keys = list(push_to_last(keys, rgb_idx))
diff --git a/terracotta/scripts/migrate.py b/terracotta/scripts/migrate.py
index 3bbfb10..f89043f 100644
--- a/terracotta/scripts/migrate.py
+++ b/terracotta/scripts/migrate.py
@@ -2,9 +2,6 @@
 
 Migrate databases between Terracotta versions.
 """
-
-from typing import Tuple
-
 import click
 import sqlalchemy as sqla
 
@@ -13,14 +10,14 @@ from terracotta.migrations import MIGRATIONS
 from terracotta.drivers.relational_meta_store import RelationalMetaStore
 
 
-def parse_version(verstr: str) -> Tuple[int, ...]:
+def parse_version(verstr: str) -> tuple[int, ...]:
     """Convert 'v<major>.<minor>.<patch>' to (major, minor)"""
     components = verstr.split(".")
     components[0] = components[0].lstrip("v")
     return tuple(int(c) for c in components[:2])
 
 
-def join_version(vertuple: Tuple[int, ...]) -> str:
+def join_version(vertuple: tuple[int, ...]) -> str:
     return "v" + ".".join(map(str, vertuple))
 
 
diff --git a/terracotta/scripts/optimize_rasters.py b/terracotta/scripts/optimize_rasters.py
index 70a4876..7f50dd4 100644
--- a/terracotta/scripts/optimize_rasters.py
+++ b/terracotta/scripts/optimize_rasters.py
@@ -3,7 +3,8 @@
 Convert some raster files to cloud-optimized GeoTiff for use with Terracotta.
 """
 
-from typing import Any, Sequence, Iterator, Union
+from typing import Any, Union
+from collections.abc import Sequence, Iterator
 import os
 import sys
 import math
diff --git a/terracotta/scripts/serve.py b/terracotta/scripts/serve.py
index 7cc6601..2a5b169 100644
--- a/terracotta/scripts/serve.py
+++ b/terracotta/scripts/serve.py
@@ -3,7 +3,8 @@
 Use Flask development server to serve up raster files or database locally.
 """
 
-from typing import Optional, Any, Tuple, Sequence, cast
+from typing import Optional, Any, cast
+from collections.abc import Sequence
 import os
 import tempfile
 import logging
@@ -98,7 +99,7 @@ def serve(
             # re-order keys
             rgb_idx = keys.index(rgb_key)
 
-            def push_to_last(seq: Sequence[Any], index: int) -> Tuple[Any, ...]:
+            def push_to_last(seq: Sequence[Any], index: int) -> tuple[Any, ...]:
                 return (*seq[:index], *seq[index + 1 :], seq[index])
 
             keys = list(push_to_last(keys, rgb_idx))
diff --git a/terracotta/server/colormap.py b/terracotta/server/colormap.py
index fc7858d..1f48bf2 100644
--- a/terracotta/server/colormap.py
+++ b/terracotta/server/colormap.py
@@ -3,7 +3,8 @@
 Flask route to handle /colormap calls.
 """
 
-from typing import Any, Mapping, Dict
+from typing import Any
+from collections.abc import Mapping
 import json
 
 from flask import jsonify, request, Response
@@ -44,7 +45,7 @@ class ColormapOptionSchema(Schema):
     num_values = fields.Int(description="Number of values to return", missing=255)
 
     @pre_load
-    def process_ranges(self, data: Mapping[str, Any], **kwargs: Any) -> Dict[str, Any]:
+    def process_ranges(self, data: Mapping[str, Any], **kwargs: Any) -> dict[str, Any]:
         data = dict(data.items())
         var = "stretch_range"
         val = data.get(var)
diff --git a/terracotta/server/compute.py b/terracotta/server/compute.py
index e0dbe9c..22b5576 100644
--- a/terracotta/server/compute.py
+++ b/terracotta/server/compute.py
@@ -3,7 +3,8 @@
 Flask route to handle /compute calls.
 """
 
-from typing import Optional, Any, Mapping, Dict, Tuple
+from typing import Optional, Any
+from collections.abc import Mapping
 import json
 
 from marshmallow import Schema, fields, validate, pre_load, ValidationError, EXCLUDE
@@ -64,7 +65,7 @@ class ComputeOptionSchema(Schema):
     v5 = _operator_field(5)
 
     @pre_load
-    def decode_json(self, data: Mapping[str, Any], **kwargs: Any) -> Dict[str, Any]:
+    def decode_json(self, data: Mapping[str, Any], **kwargs: Any) -> dict[str, Any]:
         data = dict(data.items())
         for var in ("stretch_range", "tile_size"):
             val = data.get(var)
@@ -144,7 +145,7 @@ def get_compute_preview(keys: str = "") -> Response:
 
 
 def _get_compute_image(
-    keys: str, tile_xyz: Optional[Tuple[int, int, int]] = None
+    keys: str, tile_xyz: Optional[tuple[int, int, int]] = None
 ) -> Any:
     from terracotta.handlers.compute import compute
 
diff --git a/terracotta/server/datasets.py b/terracotta/server/datasets.py
index 4d8dffb..1a995c8 100644
--- a/terracotta/server/datasets.py
+++ b/terracotta/server/datasets.py
@@ -3,7 +3,7 @@
 Flask route to handle /datasets calls.
 """
 
-from typing import Any, Dict, List, Union
+from typing import Any, Union
 from flask import request, jsonify, Response
 from marshmallow import Schema, fields, validate, INCLUDE, post_load
 import re
@@ -31,8 +31,8 @@ class DatasetOptionSchema(Schema):
 
     @post_load
     def list_items(
-        self, data: Dict[str, Any], **kwargs: Any
-    ) -> Dict[str, Union[str, List[str]]]:
+        self, data: dict[str, Any], **kwargs: Any
+    ) -> dict[str, Union[str, list[str]]]:
         # Create lists of values supplied as stringified lists
         for key, value in data.items():
             if isinstance(value, str) and re.match(r"^\[.*\]$", value):
diff --git a/terracotta/server/flask_api.py b/terracotta/server/flask_api.py
index 0c3ee4b..dd2eeab 100644
--- a/terracotta/server/flask_api.py
+++ b/terracotta/server/flask_api.py
@@ -1,4 +1,4 @@
-from typing import Any, cast, Callable, Type, TYPE_CHECKING
+from typing import Any, cast, Callable, TYPE_CHECKING
 import copy
 
 from apispec import APISpec
@@ -38,7 +38,7 @@ def _abort(status_code: int, message: str = "") -> Any:
 
 def _setup_error_handlers(app: Flask) -> None:
     def register_error_handler(
-        exc: Type[Exception], func: Callable[[Exception], Any]
+        exc: type[Exception], func: Callable[[Exception], Any]
     ) -> None:
         if TYPE_CHECKING:  # pragma: no cover
             # Flask defines this type only during type checking
diff --git a/terracotta/server/metadata.py b/terracotta/server/metadata.py
index 573926f..8bafd55 100644
--- a/terracotta/server/metadata.py
+++ b/terracotta/server/metadata.py
@@ -3,7 +3,8 @@
 Flask route to handle /metadata calls.
 """
 
-from typing import Any, Mapping, Dict
+from typing import Any
+from collections.abc import Mapping
 import json
 
 from marshmallow import Schema, fields, validate, pre_load, ValidationError
@@ -64,7 +65,7 @@ class MetadataColumnsSchema(Schema):
     @pre_load
     def validate_columns(
         self, data: Mapping[str, Any], **kwargs: Any
-    ) -> Dict[str, Any]:
+    ) -> dict[str, Any]:
         data = dict(data.items())
         var = "columns"
         val = data.get(var)
diff --git a/terracotta/server/rgb.py b/terracotta/server/rgb.py
index fe9c7a1..d3e5af2 100644
--- a/terracotta/server/rgb.py
+++ b/terracotta/server/rgb.py
@@ -3,7 +3,8 @@
 Flask route to handle /rgb calls.
 """
 
-from typing import Optional, Any, Mapping, Dict, Tuple
+from typing import Optional, Any
+from collections.abc import Mapping
 import json
 
 from marshmallow import Schema, fields, validate, pre_load, ValidationError, EXCLUDE
@@ -76,7 +77,7 @@ class RGBOptionSchema(Schema):
     )
 
     @pre_load
-    def process_ranges(self, data: Mapping[str, Any], **kwargs: Any) -> Dict[str, Any]:
+    def process_ranges(self, data: Mapping[str, Any], **kwargs: Any) -> dict[str, Any]:
         data = dict(data.items())
         for var in ("r_range", "g_range", "b_range", "tile_size"):
             val = data.get(var)
@@ -154,7 +155,7 @@ def get_rgb_preview(keys: str = "") -> Response:
 
 
 def _get_rgb_image(
-    keys: str, tile_xyz: Optional[Tuple[int, int, int]] = None
+    keys: str, tile_xyz: Optional[tuple[int, int, int]] = None
 ) -> Response:
     from terracotta.handlers.rgb import rgb
 
diff --git a/terracotta/server/singleband.py b/terracotta/server/singleband.py
index 642527e..bd3a887 100644
--- a/terracotta/server/singleband.py
+++ b/terracotta/server/singleband.py
@@ -3,7 +3,8 @@
 Flask route to handle /singleband calls.
 """
 
-from typing import Optional, Any, Mapping, Dict, Tuple
+from typing import Optional, Any
+from collections.abc import Mapping
 import json
 
 from marshmallow import (
@@ -89,7 +90,7 @@ class SinglebandOptionSchema(Schema):
             )
 
     @pre_load
-    def decode_json(self, data: Mapping[str, Any], **kwargs: Any) -> Dict[str, Any]:
+    def decode_json(self, data: Mapping[str, Any], **kwargs: Any) -> dict[str, Any]:
         data = dict(data.items())
         for var in ("stretch_range", "tile_size", "explicit_color_map"):
             val = data.get(var)
@@ -182,7 +183,7 @@ def get_singleband_preview(keys: str) -> Response:
 
 
 def _get_singleband_image(
-    keys: str, tile_xyz: Optional[Tuple[int, int, int]] = None
+    keys: str, tile_xyz: Optional[tuple[int, int, int]] = None
 ) -> Response:
     from terracotta.handlers.singleband import singleband
 
diff --git a/terracotta/xyz.py b/terracotta/xyz.py
index 2133d0a..e6259c2 100644
--- a/terracotta/xyz.py
+++ b/terracotta/xyz.py
@@ -3,7 +3,8 @@
 Utilities to work with XYZ Mercator tiles.
 """
 
-from typing import Optional, Sequence, Union, Mapping, Tuple, Any
+from typing import Optional, Union, Any
+from collections.abc import Sequence, Mapping
 
 import mercantile
 
@@ -15,9 +16,9 @@ from terracotta.drivers.terracotta_driver import TerracottaDriver
 def get_tile_data(
     driver: TerracottaDriver,
     keys: Union[Sequence[str], Mapping[str, str]],
-    tile_xyz: Optional[Tuple[int, int, int]] = None,
+    tile_xyz: Optional[tuple[int, int, int]] = None,
     *,
-    tile_size: Tuple[int, int] = (256, 256),
+    tile_size: tuple[int, int] = (256, 256),
     preserve_values: bool = False,
     asynchronous: bool = False,
 ) -> Any:

@j08lue
Copy link
Collaborator

j08lue commented Oct 23, 2024

With having python 3.9 there are nice syntax upgrades one could make

Looks like pyupgrade would introduce a few format strings and a different way to use typing.

No opinion on that from my side - would be nice in a separate PR.

Copy link
Collaborator

@dionhaefner dionhaefner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks a lot!

@dionhaefner dionhaefner merged commit 265dd5d into DHI:main Oct 24, 2024
5 of 7 checks passed
@jkittner jkittner deleted the fix-deprecation-warnings branch October 24, 2024 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants