Source code for geoalchemy2.types

"""This module defines the Column types.

The :class:`geoalchemy2.types.Geometry`, :class:`geoalchemy2.types.Geography`, and
:class:`geoalchemy2.types.Raster` classes are used when defining geometry, geography and raster
columns/properties in models.
"""

import re
import warnings
from typing import Any

from sqlalchemy import Computed
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql import func
from sqlalchemy.types import Float
from sqlalchemy.types import Integer
from sqlalchemy.types import UserDefinedType

try:
    # SQLAlchemy >= 2
    from sqlalchemy.sql._typing import _TypeEngineArgument
except ImportError:
    # SQLAlchemy < 2
    _TypeEngineArgument = Any  # type: ignore

from geoalchemy2.comparator import BaseComparator
from geoalchemy2.comparator import Comparator
from geoalchemy2.elements import CompositeElement
from geoalchemy2.elements import RasterElement
from geoalchemy2.elements import WKBElement
from geoalchemy2.exc import ArgumentError
from geoalchemy2.types import dialects


[docs] def select_dialect(dialect_name): """Select the dialect from its name.""" known_dialects = { "geopackage": dialects.geopackage, "mysql": dialects.mysql, "mariadb": dialects.mariadb, "postgresql": dialects.postgresql, "sqlite": dialects.sqlite, } return known_dialects.get(dialect_name, dialects.common)
[docs] class _GISType(UserDefinedType): """The base class for spatial types. This class defines ``bind_expression`` and ``column_expression`` methods that wrap column expressions in ``ST_GeomFromEWKT``, ``ST_GeogFromText``, or ``ST_AsEWKB`` calls. This class also defines ``result_processor`` and ``bind_processor`` methods. The function returned by ``result_processor`` converts WKB values received from the database to :class:`geoalchemy2.elements.WKBElement` objects. The function returned by ``bind_processor`` converts :class:`geoalchemy2.elements.WKTElement` objects to EWKT strings. Args: geometry_type: The geometry type. Possible values are: * ``"GEOMETRY"``, * ``"POINT"``, * ``"LINESTRING"``, * ``"POLYGON"``, * ``"MULTIPOINT"``, * ``"MULTILINESTRING"``, * ``"MULTIPOLYGON"``, * ``"GEOMETRYCOLLECTION"``, * ``"CURVE"``, * ``None``. The latter is actually not supported with :class:`geoalchemy2.types.Geography`. When set to ``None`` then no "geometry type" constraints will be attached to the geometry type declaration. Default is ``"GEOMETRY"``. srid: The SRID for this column. E.g. 4326. Default is ``-1``. dimension: The dimension of the geometry. Default is ``2``. spatial_index: Indicate if a spatial index should be created. Default is ``True``. use_N_D_index: Use the N-D index instead of the standard 2-D index. use_typmod: By default PostgreSQL type modifiers are used to create the geometry column. To use check constraints instead set ``use_typmod`` to ``False``. By default this option is not included in the call to ``AddGeometryColumn``. Note that this option is only available for PostGIS 2.x. """ name: str | None = None """ Name used for defining the main geo type (geometry or geography) in CREATE TABLE statements. Set in subclasses. """ from_text: str | None = None """ The name of "from text" function for this type. Set in subclasses. """ as_binary: str | None = None """ The name of the "as binary" function for this type. Set in subclasses. """ comparator_factory: Any = Comparator """ This is the way by which spatial operators are defined for geometry/geography columns. """ cache_ok = False """ Disable cache for this type. """ def __init__( self, geometry_type: str | None = "GEOMETRY", srid: int = -1, dimension: int | None = None, spatial_index: bool = True, use_N_D_index: bool = False, use_typmod: bool | None = None, from_text: str | None = None, name: str | None = None, nullable: bool = True, _spatial_index_reflected=None, ) -> None: geometry_type, srid, dimension = self.check_ctor_args( geometry_type, srid, dimension, use_typmod, nullable ) self.geometry_type = geometry_type self.srid = srid if name is not None: self.name = name if from_text is not None: self.from_text = from_text self.dimension = dimension self.spatial_index = spatial_index self.use_N_D_index = use_N_D_index self.use_typmod = use_typmod self.extended: bool | None = self.as_binary == "ST_AsEWKB" self.nullable = nullable self._spatial_index_reflected = _spatial_index_reflected def get_col_spec(self): if not self.geometry_type: return self.name return f"{self.name}({self.geometry_type},{self.srid})"
[docs] def column_expression(self, col): """Specific column_expression that automatically adds a conversion function.""" return getattr(func, self.as_binary)(col, type_=self)
[docs] def result_processor(self, dialect, coltype): """Specific result_processor that automatically process spatial elements.""" def process(value): if value is not None: kwargs = {} if self.srid > 0: kwargs["srid"] = self.srid if self.extended is not None and dialect.name not in ["mysql", "mariadb"]: kwargs["extended"] = self.extended return self.ElementType(value, **kwargs) return process
[docs] def bind_expression(self, bindvalue): """Specific bind_expression that automatically adds a conversion function.""" return getattr(func, self.from_text)(bindvalue, type_=self)
[docs] def bind_processor(self, dialect): """Specific bind_processor that automatically process spatial elements.""" def process(bindvalue): return select_dialect(dialect.name).bind_processor_process(self, bindvalue) return process
@staticmethod def check_ctor_args(geometry_type, srid, dimension, use_typmod, nullable): try: # passing default SRID if it is NULL from DB srid = int(srid if srid is not None else -1) except (ValueError, TypeError): raise ArgumentError("srid must be convertible to an integer") from None if geometry_type: geometry_type = geometry_type.upper() elif srid > 0: warnings.warn("srid not enforced when geometry_type is None", stacklevel=1) if use_typmod is not None and not nullable: raise ArgumentError( 'The "nullable" and "use_typmod" arguments can not be used together' ) if dimension not in [None, 2, 3, 4]: raise ValueError(f"dimension must be one of [None, 2, 3, 4] but got {dimension}") if geometry_type is not None: if geometry_type.endswith("ZM"): if dimension not in [None, 4]: raise ValueError("dimension must be 4 when geometry_type ends with 'ZM'") dimension = 4 elif geometry_type[-1] in ["Z", "M"]: if dimension not in [None, 3]: raise ValueError("dimension must be 3 when geometry_type ends with 'Z' or 'M'") dimension = 3 else: dimension = 2 return geometry_type, srid, dimension
@compiles(_GISType, "mysql") @compiles(_GISType, "mariadb") def get_col_spec_mysql(self, compiler, *args, **kwargs): spec = f"{self.geometry_type}" if self.geometry_type is not None else "GEOMETRY" type_expression = kwargs.get("type_expression") if type_expression is None or type_expression.computed is None: if not self.nullable or self.spatial_index: spec += " NOT NULL" if self.srid > 0 and compiler.dialect.name != "mariadb": spec += f" SRID {self.srid}" return spec @compiles(Computed, "mysql") @compiles(Computed, "mariadb") def get_col_spec_computed_mysql(self, compiler, *args, **kwargs): # MySQL uses a different syntax for computed columns # than PostgreSQL, so we need to handle it here. spec = self.sqltext.compile(compiler, **kwargs).string pattern = re.compile("st_", re.IGNORECASE) spec = f"AS ({re.sub(pattern, '', spec)})" return spec
[docs] class Geometry(_GISType): """The Geometry type. Creating a geometry column is done like this:: Column(Geometry(geometry_type='POINT', srid=4326)) See :class:`geoalchemy2.types._GISType` for the list of arguments that can be passed to the constructor. If ``srid`` is set then the ``WKBElement`` objects resulting from queries will have that SRID, and, when constructing the ``WKBElement`` objects, the SRID won't be read from the data returned by the database. If ``srid`` is not set (meaning it's ``-1``) then the SRID set in ``WKBElement`` objects will be read from the data returned by the database. """ name = "geometry" """ Type name used for defining geometry columns in ``CREATE TABLE``. """ from_text = "ST_GeomFromEWKT" """ The "from text" geometry constructor. Used by the parent class' ``bind_expression`` method. """ as_binary = "ST_AsEWKB" """ The "as binary" function to use. Used by the parent class' ``column_expression`` method. """ ElementType = WKBElement """ The element class to use. Used by the parent class' ``result_processor`` method. """ cache_ok = True """ Enable cache for this type. """
[docs] class Geography(_GISType): """The Geography type. Creating a geography column is done like this:: Column(Geography(geometry_type='POINT', srid=4326)) See :class:`geoalchemy2.types._GISType` for the list of arguments that can be passed to the constructor. """ name = "geography" """ Type name used for defining geography columns in ``CREATE TABLE``. """ from_text = "ST_GeogFromText" """ The ``FromText`` geography constructor. Used by the parent class' ``bind_expression`` method. """ as_binary = "ST_AsBinary" """ The "as binary" function to use. Used by the parent class' ``column_expression`` method. """ ElementType = WKBElement """ The element class to use. Used by the parent class' ``result_processor`` method. """ cache_ok = True """ Enable cache for this type. """
[docs] class Raster(_GISType): """The Raster column type. Creating a raster column is done like this:: Column(Raster) This class defines the ``result_processor`` method, so that raster values received from the database are converted to :class:`geoalchemy2.elements.RasterElement` objects. Args: spatial_index: Indicate if a spatial index should be created. Default is ``True``. """ comparator_factory = BaseComparator """ This is the way by which spatial operators and functions are defined for raster columns. """ name = "raster" """ Type name used for defining raster columns in ``CREATE TABLE``. """ from_text = "raster" """ The "from text" raster constructor. Used by the parent class' ``bind_expression`` method. """ as_binary = "raster" """ The "as binary" function to use. Used by the parent class' ``column_expression`` method. """ ElementType = RasterElement """ The element class to use. Used by the parent class' ``result_processor`` method. """ cache_ok = True """ Enable cache for this type. """ def __init__( self, spatial_index=True, from_text=None, name=None, nullable=True, _spatial_index_reflected=None, ) -> None: # Enforce default values super().__init__( geometry_type=None, srid=-1, dimension=None, spatial_index=spatial_index, use_N_D_index=False, use_typmod=False, from_text=from_text, name=name, nullable=nullable, _spatial_index_reflected=_spatial_index_reflected, ) self.extended = None @staticmethod def check_ctor_args(*args, **kwargs): return None, -1, None
class _DummyGeometry(Geometry): """A dummy type only used with SQLite.""" def get_col_spec(self): return self.geometry_type or "GEOMETRY"
[docs] class CompositeType(UserDefinedType): """A composite type used by some spatial functions. A wrapper for :class:`geoalchemy2.elements.CompositeElement`, that can be used as the return type in PostgreSQL functions that return composite values. This is used as the base class of :class:`geoalchemy2.types.GeometryDump`. """ typemap: dict[str, _TypeEngineArgument] = {} """ Dictionary used for defining the content types and their corresponding keys. Set in subclasses. """
[docs] class comparator_factory(UserDefinedType.Comparator): def __getattr__(self, key): try: type_ = self.type.typemap[key] except KeyError: raise AttributeError( f"Type '{self.type}' doesn't have an attribute: '{key}'" ) from None return CompositeElement(self.expr, key, type_)
[docs] class GeometryDump(CompositeType): """The return type for functions like ``ST_Dump``. The type consists in a path and a geom field. You should normally never use this class directly. """ typemap = {"path": postgresql.ARRAY(Integer), "geom": Geometry} """ Dictionary defining the contents of a ``geometry_dump``. """ cache_ok = True """ Enable cache for this type. """
[docs] class SummaryStats(CompositeType): """Define the composite type returned by the function ST_SummaryStatsAgg.""" typemap = { "count": Integer, "sum": Float, "mean": Float, "stddev": Float, "min": Float, "max": Float, } cache_ok = True """ Enable cache for this type. """
__all__ = [ "_GISType", "CompositeType", "Geography", "Geometry", "GeometryDump", "Raster", "SummaryStats", "dialects", "select_dialect", ] def __dir__(): return __all__