]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Add Shapely-based adapter for PostGIS geometry, and docs
authorJacopo Farina <jacopo.farina@flixbus.com>
Tue, 14 Sep 2021 09:14:26 +0000 (11:14 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Tue, 21 Sep 2021 17:11:11 +0000 (18:11 +0100)
Previous commits were messy due to some rebase gone wrong, moved them to
a single commit

.mypy.ini
docs/basic/pgtypes.rst
psycopg/psycopg/types/geometry.py [new file with mode: 0644]

index 06aa120deb4187dcb6cec0ad80d2eb009732a849..ea6e1e5b46410420f3afec310b113c6ad404fbd6 100644 (file)
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -10,3 +10,6 @@ ignore_missing_imports = True
 
 [mypy-setuptools]
 ignore_missing_imports = True
+
+[mypy-shapely.*]
+ignore_missing_imports = True
index 81f3ffa5d8ffdc8264da21b8c6b5fbe9da7a024e..f8d81c1778df509eea7c4b0449280e54ae56ecd4 100644 (file)
@@ -211,3 +211,56 @@ Example::
 
     >>> conn.execute("SELECT 'foo => bar'::hstore").fetchone()[0]
     {'foo': 'bar'}
+
+Geometry adaptation using Shapely
+---------------------------------
+
+When using the PostGIS_ extension, it can be useful to retrieve geometry_
+values and have them automatically converted to Shapely_ instances. Likewise,
+you may want to store such instances in the database and have the conversion
+happen automatically.
+
+To support this, you will need to install Shapely_
+
+.. _PostGIS: https://postgis.net/
+.. _geometry: https://postgis.net/docs/geometry.html
+.. _Shapely: https://github.com/Toblerity/Shapely
+.. _shape: https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.shape
+
+Since PostgGIS is an extension, its oid is not well known, so it is necessary
+to use `~psycopg.types.TypeInfo` to query the database and get its oid. After
+that you can use `~psycopg.types.geometry.register_shapely()` to allow dumping
+`shape`_ instances to :sql:`geometry` columns and parsing :sql:`geometry` back
+to `!shape` in the context where it is registered.
+
+.. autofunction:: psycopg.types.geometry.register_shapely
+
+Example::
+
+    >>> from psycopg.types import TypeInfo
+    >>> from psycopg.types.geometry import register_shapely
+    >>> from shapely.geometry import Point
+
+    >>> info = TypeInfo.fetch(conn, "geometry")
+    >>> register_shapely(info, conn)
+
+    >>> conn.execute("SELECT pg_typeof(%s)", [Point(1.2, 3.4)]).fetchone()[0]
+    'geometry'
+
+    >>> conn.execute("""
+    ... SELECT ST_GeomFromGeoJSON('{
+    ...     "type":"Point",
+    ...     "coordinates":[-48.23456,20.12345]}')
+    ... """).fetchone()[0]
+    <shapely.geometry.multipolygon.MultiPolygon object at 0x7fb131f3cd90>
+
+Notice that the adapter is registered on the specific object, other
+connections will be unaffected::
+
+    >>> conn2 = psycopg.connect(CONN_STR)
+    >>> conn2.execute("""
+    ... SELECT ST_GeomFromGeoJSON('{
+    ...     "type":"Point",
+    ...     "coordinates":[-48.23456,20.12345]}')
+    ... """).fetchone()[0]
+    '0101000020E61000009279E40F061E48C0F2B0506B9A1F3440'
diff --git a/psycopg/psycopg/types/geometry.py b/psycopg/psycopg/types/geometry.py
new file mode 100644 (file)
index 0000000..7956ebb
--- /dev/null
@@ -0,0 +1,86 @@
+"""
+Adapters for PostGIS geometries
+"""
+
+from typing import Optional, Type
+
+from .. import postgres
+from ..abc import AdaptContext
+from ..adapt import Dumper, Loader
+from ..pq import Format
+from .._typeinfo import TypeInfo
+
+
+try:
+    import shapely.wkb as wkb
+    from shapely.geometry.base import BaseGeometry
+
+except ImportError:
+    raise ImportError(
+        "The module psycopg.types.geometry requires the package 'Shapely'"
+        " to be installed"
+    )
+
+
+class GeometryBinaryLoader(Loader):
+    format = Format.BINARY
+
+    def load(self, data: bytes) -> "BaseGeometry":
+        return wkb.loads(data)
+
+
+class GeometryLoader(Loader):
+    format = Format.TEXT
+
+    def load(self, data: bytes) -> "BaseGeometry":
+        # it's a hex string in binary
+        return wkb.loads(data.decode(), hex=True)
+
+
+class GeometryBinaryDumper(Dumper):
+    format = Format.BINARY
+
+    def dump(self, obj: "BaseGeometry") -> bytes:
+        return wkb.dumps(obj).encode()  # type: ignore
+
+
+class GeometryDumper(Dumper):
+    format = Format.TEXT
+
+    def dump(self, obj: "BaseGeometry") -> bytes:
+        return wkb.dumps(obj, hex=True).encode()  # type: ignore
+
+
+def register_shapely(
+    info: TypeInfo, context: Optional[AdaptContext] = None
+) -> None:
+    """Register Shapely dumper and loaders.
+
+    After invoking this function on an adapter, the queries retrieving
+    PostGIS geometry objects will return Shapely's shape object instances
+    both in text and binary mode.
+
+    Similarly, shape objects can be sent to the database.
+
+    This requires the Shapely library to be installed.
+
+    :param info: The object with the information about the geometry type.
+    :param context: The context where to register the adapters. If `!None`,
+        register it globally.
+
+    """
+
+    info.register(context)
+    adapters = context.adapters if context else postgres.adapters
+    # Generate and register the text and binary dumper
+    binary_dumper: Type[GeometryBinaryDumper] = type(
+        "GeometryBinaryDumper", (GeometryBinaryDumper,), {"oid": info.oid}
+    )
+    dumper: Type[GeometryDumper] = type(
+        "GeometryDumper", (GeometryDumper,), {"oid": info.oid}
+    )
+
+    adapters.register_loader(info.oid, GeometryBinaryLoader)
+    adapters.register_loader(info.oid, GeometryLoader)
+    adapters.register_dumper(BaseGeometry, binary_dumper)
+    adapters.register_dumper(BaseGeometry, dumper)