├── .gitignore ├── MANIFEST.in ├── Makefile ├── README.md ├── circle.yml ├── postgis ├── __init__.py ├── asyncpg.py ├── ewkb.py ├── geojson.py ├── geometry.py ├── geometrycollection.py ├── linestring.py ├── multi.py ├── multilinestring.py ├── multipoint.py ├── multipolygon.py ├── point.py ├── polygon.py └── psycopg.py ├── pytest.ini ├── setup.py └── tests ├── __init__.py ├── asyncpg ├── __init__.py ├── conftest.py ├── test_geometrycollection.py ├── test_linestring.py ├── test_multilinestring.py ├── test_multipoint.py ├── test_multipolygon.py ├── test_point.py └── test_polygon.py ├── psycopg ├── __init__.py ├── conftest.py ├── test_geometrycollection.py ├── test_linestring.py ├── test_multilinestring.py ├── test_multipoint.py ├── test_multipolygon.py ├── test_point.py └── test_polygon.py ├── test_geometrycollection.py ├── test_linestring.py ├── test_multilinestring.py ├── test_multipoint.py ├── test_multipolygon.py ├── test_point.py └── test_polygon.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | *.pyc 4 | .cache 5 | *.c 6 | build/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | develop: 2 | pip install -e .[test] 3 | test: 4 | py.test -vvx 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Circle CI](https://img.shields.io/circleci/project/yohanboniface/python-postgis.svg)](https://circleci.com/gh/yohanboniface/python-postgis) [![PyPI](https://img.shields.io/pypi/v/postgis.svg)](https://pypi.python.org/pypi/postgis) [![PyPI](https://img.shields.io/pypi/pyversions/postgis.svg)](https://pypi.python.org/pypi/postgis) [![PyPI](https://img.shields.io/pypi/implementation/postgis.svg)](https://pypi.python.org/pypi/postgis) [![PyPI](https://img.shields.io/pypi/status/postgis.svg)](https://pypi.python.org/pypi/postgis) 2 | 3 | # python-postgis 4 | 5 | PostGIS helpers for psycopg2 and asyncpg. 6 | 7 | ## Install 8 | 9 | pip install postgis 10 | 11 | If you want a compiled version, first install `cython`: 12 | 13 | pip install cython 14 | pip install postgis 15 | 16 | 17 | ## Usage 18 | 19 | You need to register the extension: 20 | 21 | # With psycopg2 22 | > from postgis.psycopg import register 23 | > register(connection) 24 | 25 | # With asyncpg 26 | > from postgis.asyncpg import register 27 | > await register(connection) 28 | 29 | Then you can pass python geometries instance to psycopg: 30 | 31 | > cursor.execute('INSERT INTO table (geom) VALUES (%s)', [Point(x=1, y=2, srid=4326)]) 32 | 33 | And retrieve data as python geometries instances: 34 | 35 | > cursor.execute('SELECT geom FROM points LIMIT 1') 36 | > geom = cursor.fetchone()[0] 37 | > geom 38 | 39 | 40 | 41 | ## Example with psycopg2 42 | 43 | > import psycopg2 44 | > from postgis import LineString 45 | > from postgis.psycopg import register 46 | > db = psycopg2.connect(dbname="test") 47 | > register(db) 48 | > cursor = db.cursor() 49 | > cursor.execute('CREATE TABLE IF NOT EXISTS mytable ("geom" geometry(LineString) NOT NULL)') 50 | > cursor.execute('INSERT INTO mytable (geom) VALUES (%s)', [LineString([(1, 2), (3, 4)], srid=4326)]) 51 | > cursor.execute('SELECT geom FROM mytable LIMIT 1') 52 | > geom = cursor.fetchone()[0] 53 | > geom 54 | 55 | > geom[0] 56 | 57 | > geom.coords 58 | ((1.0, 2.0), (3.0, 4.0)) 59 | > geom.geojson 60 | {'coordinates': ((1.0, 2.0), (3.0, 4.0)), 'type': 'LineString'} 61 | > str(geom.geojson) 62 | '{"type": "LineString", "coordinates": [[1, 2], [3, 4]]}' 63 | 64 | 65 | ## Example with asyncpg 66 | 67 | from postgis.asyncpg import register 68 | pool = await create_pool(**DB_CONFIG, loop=loop, max_size=100, 69 | init=register) 70 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | python: 3 | version: 3.6.1 4 | 5 | database: 6 | pre: 7 | - psql -U ubuntu -c "CREATE DATABASE test;" 8 | - psql -U ubuntu -c "create extension postgis" -d test 9 | 10 | test: 11 | override: 12 | - py.test tests/ 13 | 14 | dependencies: 15 | pre: 16 | - pip install pytest psycopg2 asyncpg cython 17 | -------------------------------------------------------------------------------- /postgis/__init__.py: -------------------------------------------------------------------------------- 1 | "Postgis helpers for psycopg2 and asyncpg." 2 | from .geometry import Geometry 3 | from .geometrycollection import GeometryCollection 4 | from .linestring import LineString 5 | from .multilinestring import MultiLineString 6 | from .multipoint import MultiPoint 7 | from .multipolygon import MultiPolygon 8 | from .point import Point 9 | from .polygon import Polygon 10 | try: 11 | from .psycopg import register # Retrocompat. 12 | except ImportError: 13 | pass 14 | 15 | __all__ = ['Geometry', 'register', 'Point', 'LineString', 'Polygon', 16 | 'MultiPoint', 'MultiLineString', 'MultiPolygon', 17 | 'GeometryCollection'] 18 | 19 | try: 20 | import pkg_resources 21 | except ImportError: # pragma: no cover 22 | pass 23 | else: 24 | if __package__: 25 | VERSION = pkg_resources.get_distribution(__package__).version 26 | -------------------------------------------------------------------------------- /postgis/asyncpg.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry 2 | 3 | 4 | async def register(connection): 5 | 6 | def encoder(value): 7 | if not isinstance(value, Geometry): 8 | raise ValueError('Geometry value must subclass Geometry class') 9 | return value.to_ewkb() 10 | 11 | def decoder(value): 12 | return Geometry.from_ewkb(value) 13 | 14 | await connection.set_type_codec( 15 | 'geography', encoder=encoder, decoder=decoder) 16 | await connection.set_type_codec( 17 | 'geometry', encoder=encoder, decoder=decoder) 18 | -------------------------------------------------------------------------------- /postgis/ewkb.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from io import BytesIO 3 | import struct 4 | 5 | 6 | class Typed(type): 7 | 8 | types = {} 9 | 10 | def __new__(mcs, name, bases, attrs, **kwargs): 11 | cls = super().__new__(mcs, name, bases, attrs, **kwargs) 12 | if hasattr(cls, 'TYPE'): 13 | Typed.types[cls.TYPE] = cls 14 | return cls 15 | 16 | def __call__(cls, *args, **kwargs): 17 | # Allow to pass an instance as first argument, for blind casting. 18 | if args and isinstance(args[0], cls): 19 | return args[0] 20 | return super().__call__(*args, **kwargs) 21 | 22 | 23 | class Reader: 24 | 25 | __slots__ = ['stream', 'endianness', 'has_z', 'has_m'] 26 | 27 | def __init__(self, stream): 28 | self.stream = stream 29 | 30 | def clone(self): 31 | return type(self)(self.stream) 32 | 33 | def read(self): 34 | # https://en.wikipedia.org/wiki/Well-known_text#Well-known_binary 35 | byte_order = self.stream.read(1) 36 | if byte_order == b'\x00': 37 | self.endianness = b'>' 38 | elif byte_order == b'\x01': 39 | self.endianness = b'<' 40 | else: 41 | raise Exception('invalid encoding') 42 | 43 | type_ = self.read_int() 44 | self.has_z = bool(type_ & 0x80000000) 45 | self.has_m = bool(type_ & 0x40000000) 46 | srid = self.read_int() if bool(type_ & 0x20000000) else None 47 | type_ &= 0x1fffffff 48 | 49 | try: 50 | class_ = Typed.types[type_] 51 | except KeyError: 52 | raise ValueError('unsupported geometry type {0}'.format(type_)) 53 | else: 54 | return class_.from_ewkb_body(self, srid) 55 | 56 | def read_int(self): 57 | return struct.unpack(self.endianness + b'I', self.stream.read(4))[0] 58 | 59 | def read_double(self): 60 | return struct.unpack(self.endianness + b'd', self.stream.read(8))[0] 61 | 62 | @classmethod 63 | def from_hex(cls, value): 64 | return cls(BytesIO(binascii.a2b_hex(value))).read() 65 | 66 | 67 | class Writer: 68 | 69 | __slots__ = ['stream'] 70 | 71 | def __init__(self, geometry, stream=None): 72 | self.stream = stream or BytesIO() 73 | try: 74 | type_ = geometry.TYPE 75 | except AttributeError: 76 | raise ValueError('Unknown geometry {}'.format(geometry.__class__)) 77 | 78 | # Little endian. 79 | self.stream.write(b'\x01') 80 | self.write_int( 81 | type_ | 82 | (0x80000000 if geometry.has_z else 0) | 83 | (0x40000000 if geometry.has_m else 0) | 84 | (0x20000000 if geometry.has_srid else 0)) 85 | if geometry.has_srid: 86 | self.write_int(geometry.srid) 87 | 88 | def write_int(self, value): 89 | self.stream.write(struct.pack(b''.format(self.__class__.__name__, self.wkt) 57 | 58 | def __eq__(self, other): 59 | if isinstance(other, self.__class__): 60 | other = other.coords 61 | return self.coords == other 62 | 63 | @property 64 | def name(self): 65 | return self.__class__.__name__ 66 | 67 | @property 68 | def wkt(self): 69 | return "{}({})".format(self.name.upper(), self.wkt_coords) 70 | 71 | @property 72 | def geojson(self): 73 | return GeoJSON({ 74 | 'type': self.name, 75 | 'coordinates': self.coords 76 | }) 77 | -------------------------------------------------------------------------------- /postgis/geometrycollection.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry 2 | from .geojson import GeoJSON 3 | 4 | 5 | class GeometryCollection(Geometry): 6 | 7 | TYPE = 7 8 | 9 | def __init__(self, geoms, srid=None): 10 | for geom in geoms: 11 | if not isinstance(geom, Geometry): 12 | raise ValueError('{} is not instance of Geometry'.format(geom)) 13 | self.geoms = list(geoms) 14 | if srid: 15 | self.srid = srid 16 | 17 | def __iter__(self): 18 | return self.geoms.__iter__() 19 | 20 | def __eq__(self, other): 21 | if isinstance(other, self.__class__): 22 | other = other.geoms 23 | return self.geoms == other 24 | 25 | @property 26 | def has_z(self): 27 | return self[0].has_z 28 | 29 | @property 30 | def has_m(self): 31 | return self[0].has_m 32 | 33 | def __getitem__(self, item): 34 | return self.geoms[item] 35 | 36 | @classmethod 37 | def from_ewkb_body(cls, reader, srid=None): 38 | return cls([reader.read() for index in range(reader.read_int())], srid) 39 | 40 | def write_ewkb_body(self, writer): 41 | writer.write_int(len(self.geoms)) 42 | for geom in self: 43 | geom.write_ewkb(writer) 44 | 45 | @property 46 | def wkt_coords(self): 47 | return ', '.join(g.wkt for g in self) 48 | 49 | @property 50 | def geojson(self): 51 | return GeoJSON({ 52 | 'type': self.name, 53 | 'geometries': [g.geojson for g in self] 54 | }) 55 | -------------------------------------------------------------------------------- /postgis/linestring.py: -------------------------------------------------------------------------------- 1 | from .point import Point 2 | from .multi import Multi 3 | 4 | 5 | class LineString(Multi): 6 | 7 | TYPE = 2 8 | SUBCLASS = Point 9 | 10 | @classmethod 11 | def from_ewkb_body(cls, reader, srid=None): 12 | return cls([Point.from_ewkb_body(reader) 13 | for index in range(reader.read_int())], srid) 14 | 15 | def write_ewkb_body(self, writer): 16 | writer.write_int(len(self.geoms)) 17 | for geom in self: 18 | geom.write_ewkb_body(writer) 19 | -------------------------------------------------------------------------------- /postgis/multi.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry 2 | from .point import Point 3 | 4 | class Multi(Geometry): 5 | 6 | __slots__ = ['geoms', 'srid'] 7 | SUBCLASS = None 8 | 9 | def __init__(self, geoms, srid=None): 10 | self.geoms = [self.SUBCLASS(g, srid=srid) for g in geoms] 11 | if srid: 12 | self.srid = srid 13 | 14 | def __iter__(self): 15 | return iter(self.geoms) 16 | 17 | @property 18 | def has_z(self): 19 | return self[0].has_z 20 | 21 | @property 22 | def has_m(self): 23 | return self[0].has_m 24 | 25 | def __getitem__(self, item): 26 | return self.geoms[item] 27 | 28 | @classmethod 29 | def from_ewkb_body(cls, reader, srid=None): 30 | return cls([reader.read() for index in range(reader.read_int())], srid) 31 | 32 | @property 33 | def wkt_coords(self): 34 | fmt = '{}' if self.SUBCLASS == Point else '({})' 35 | return ', '.join(fmt.format(g.wkt_coords) for g in self) 36 | 37 | def write_ewkb_body(self, writer): 38 | writer.write_int(len(self.geoms)) 39 | for geom in self: 40 | geom.write_ewkb(writer) 41 | 42 | @property 43 | def coords(self): 44 | return tuple(g.coords for g in self) 45 | -------------------------------------------------------------------------------- /postgis/multilinestring.py: -------------------------------------------------------------------------------- 1 | from .multi import Multi 2 | from .linestring import LineString 3 | 4 | 5 | class MultiLineString(Multi): 6 | 7 | TYPE = 5 8 | SUBCLASS = LineString 9 | -------------------------------------------------------------------------------- /postgis/multipoint.py: -------------------------------------------------------------------------------- 1 | from .multi import Multi 2 | from .point import Point 3 | 4 | 5 | class MultiPoint(Multi): 6 | 7 | TYPE = 4 8 | SUBCLASS = Point 9 | -------------------------------------------------------------------------------- /postgis/multipolygon.py: -------------------------------------------------------------------------------- 1 | from .multi import Multi 2 | from .polygon import Polygon 3 | 4 | 5 | class MultiPolygon(Multi): 6 | 7 | TYPE = 6 8 | SUBCLASS = Polygon 9 | -------------------------------------------------------------------------------- /postgis/point.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry 2 | 3 | 4 | class Point(Geometry): 5 | 6 | __slots__ = ['x', 'y', 'z', 'm', 'srid'] 7 | TYPE = 1 8 | 9 | def __init__(self, x, y=None, z=None, m=None, srid=None): 10 | if y is None and isinstance(x, (tuple, list)): 11 | x, y, *extra = x 12 | if extra: 13 | z, *extra = extra 14 | if extra: 15 | m = extra[0] 16 | self.x = float(x) 17 | self.y = float(y) 18 | self.z = float(z) if z is not None else None 19 | self.m = float(m) if m is not None else None 20 | if srid is not None: 21 | self.srid = srid 22 | 23 | def __getitem__(self, item): 24 | if item in (0, 'x'): 25 | return self.x 26 | elif item in (1, 'y'): 27 | return self.y 28 | elif item in (2, 'z'): 29 | return self.z 30 | elif item in (3, 'm'): 31 | return self.m 32 | 33 | def __iter__(self): 34 | return iter(self.values()) 35 | 36 | _keys = ['x', 'y', 'z', 'm'] 37 | 38 | def keys(self): 39 | return [k for k in self._keys if self[k] is not None] 40 | 41 | def values(self): 42 | return tuple(self[k] for k in self.keys()) 43 | 44 | @property 45 | def has_z(self): 46 | return self.z is not None 47 | 48 | @classmethod 49 | def from_ewkb_body(cls, reader, srid=None): 50 | return cls(reader.read_double(), reader.read_double(), 51 | reader.read_double() if reader.has_z else None, 52 | reader.read_double() if reader.has_m else None, 53 | srid) 54 | 55 | def write_ewkb_body(self, writer): 56 | writer.write_double(self.x) 57 | writer.write_double(self.y) 58 | if self.z is not None: 59 | writer.write_double(self.z) 60 | if self.m is not None: 61 | writer.write_double(self.m) 62 | 63 | @property 64 | def wkt_coords(self): 65 | return ' '.join(map(str, self.coords)) 66 | 67 | @property 68 | def coords(self): 69 | return self.values() 70 | -------------------------------------------------------------------------------- /postgis/polygon.py: -------------------------------------------------------------------------------- 1 | from .linestring import LineString 2 | from .multi import Multi 3 | 4 | 5 | class Polygon(Multi): 6 | 7 | TYPE = 3 8 | SUBCLASS = LineString 9 | 10 | @classmethod 11 | def from_ewkb_body(cls, reader, srid=None): 12 | return cls([LineString.from_ewkb_body(reader) 13 | for index in range(reader.read_int())], srid) 14 | 15 | def write_ewkb_body(self, writer): 16 | writer.write_int(len(self.geoms)) 17 | for geom in self: 18 | geom.write_ewkb_body(writer) 19 | -------------------------------------------------------------------------------- /postgis/psycopg.py: -------------------------------------------------------------------------------- 1 | from psycopg2 import extensions 2 | 3 | from .geometry import Geometry 4 | 5 | 6 | def register(connection): 7 | if isinstance(connection, extensions.cursor): 8 | # Retrocompat. 9 | cursor = connection 10 | else: 11 | cursor = connection.cursor() 12 | cursor.execute("SELECT NULL::geometry") 13 | oid = cursor.description[0][1] 14 | GEOMETRY = extensions.new_type((oid, ), "GEOMETRY", Geometry.from_ewkb) 15 | extensions.register_type(GEOMETRY) 16 | 17 | cursor.execute("SELECT NULL::geography") 18 | oid = cursor.description[0][1] 19 | GEOGRAPHY = extensions.new_type((oid, ), "GEOGRAPHY", Geometry.from_ewkb) 20 | extensions.register_type(GEOGRAPHY) 21 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -x 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Pyscopg and asyncpg helpers to work with PostGIS.""" 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | from setuptools import Extension, find_packages, setup 7 | 8 | 9 | def list_modules(dirname): 10 | paths = Path(dirname).glob("*.py") 11 | return [p.stem for p in paths if p.stem != "__init__"] 12 | 13 | 14 | try: 15 | from Cython.Distutils import build_ext 16 | 17 | CYTHON = True 18 | except ImportError: 19 | sys.stdout.write( 20 | "\nNOTE: Cython not installed. python-postgis will " 21 | "still work fine, but may run a bit slower.\n\n" 22 | ) 23 | CYTHON = False 24 | cmdclass = {} 25 | ext_modules = [] 26 | else: 27 | ext_modules = [ 28 | Extension("postgis." + ext, [str(Path("postgis") / f"{ext}.py")]) 29 | for ext in list_modules(Path("postgis")) 30 | ] 31 | 32 | cmdclass = {"build_ext": build_ext} 33 | 34 | 35 | VERSION = (1, 0, 4) 36 | 37 | setup( 38 | name="postgis", 39 | version=".".join(map(str, VERSION)), 40 | description=__doc__, 41 | long_description=Path("README.md").read_text(), 42 | url="https://github.com/tilery/python-postgis", 43 | author="Yohan Boniface", 44 | author_email="yohanboniface@free.fr", 45 | license="WTFPL", 46 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 47 | classifiers=[ 48 | "Development Status :: 4 - Beta", 49 | "Intended Audience :: Developers", 50 | "Topic :: Scientific/Engineering :: GIS", 51 | "Programming Language :: Python :: 3", 52 | "Programming Language :: Python :: 3.5", 53 | "Programming Language :: Python :: 3.6", 54 | "Programming Language :: Python :: 3.7", 55 | "Programming Language :: Python :: 3.8", 56 | ], 57 | keywords="psycopg postgis gis asyncpg", 58 | packages=find_packages(exclude=["tests"]), 59 | extras_require={ 60 | "test": ["pytest", "pytest-asyncio", "psycopg2", "asyncpg"], 61 | "docs": "mkdocs", 62 | }, 63 | include_package_data=True, 64 | cmdclass=cmdclass, 65 | ext_modules=ext_modules, 66 | ) 67 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilery/python-postgis/fd31227e69735db92aef62642ef4ff6806d61871/tests/__init__.py -------------------------------------------------------------------------------- /tests/asyncpg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilery/python-postgis/fd31227e69735db92aef62642ef4ff6806d61871/tests/asyncpg/__init__.py -------------------------------------------------------------------------------- /tests/asyncpg/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncpg 2 | import pytest 3 | from postgis import (GeometryCollection, LineString, MultiLineString, 4 | MultiPoint, MultiPolygon, Point, Polygon) 5 | from postgis.asyncpg import register 6 | 7 | geoms = [Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, 8 | GeometryCollection] 9 | 10 | 11 | @pytest.yield_fixture 12 | async def connection(): 13 | conn = await asyncpg.connect('postgresql://postgres@localhost/test') 14 | await register(conn) 15 | tpl = 'CREATE TABLE IF NOT EXISTS {}_async ("geom" geometry({}) NOT NULL)' 16 | for geom in geoms: 17 | name = geom.__name__ 18 | await conn.execute(tpl.format(name.lower(), name)) 19 | yield conn 20 | await conn.close() 21 | -------------------------------------------------------------------------------- /tests/asyncpg/test_geometrycollection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import Point, LineString, Polygon, GeometryCollection 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | POLYGON = Polygon(( 9 | ((1, 2), (3, 4), (5, 6), (1, 2)), 10 | ((2, 3), (4, 5), (6, 7), (2, 3)) 11 | )) 12 | 13 | COLLECTION = [ 14 | Point(1, 2), 15 | LineString(((1, 2), (3, 4))), 16 | POLYGON 17 | ] 18 | 19 | 20 | async def test_geometrycollection_should_round(connection): 21 | geom = GeometryCollection(COLLECTION, srid=4326) 22 | await connection.execute('INSERT INTO geometrycollection_async (geom) ' 23 | 'VALUES ($1)', geom) 24 | geom = await connection.fetchval('SELECT geom ' 25 | 'FROM geometrycollection_async ' 26 | 'WHERE geom=$1', geom, column=0) 27 | assert geom == COLLECTION 28 | -------------------------------------------------------------------------------- /tests/asyncpg/test_linestring.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import LineString 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | @pytest.mark.parametrize('expected', [ 9 | ((30, 10), (10, 30), (40, 40)), 10 | ]) 11 | async def test_linestring_should_round(connection, expected): 12 | geom = LineString(expected, srid=4326) 13 | await connection.execute('INSERT INTO linestring_async (geom) VALUES ($1)', 14 | geom) 15 | geom = await connection.fetchval('SELECT geom FROM linestring_async WHERE ' 16 | 'geom=$1', geom, column=0) 17 | assert geom.coords == expected 18 | -------------------------------------------------------------------------------- /tests/asyncpg/test_multilinestring.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import MultiLineString 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | @pytest.mark.parametrize('expected', [ 9 | (((30, 10), (10, 30)), ((40, 10), (10, 40))), 10 | ]) 11 | async def test_multilinestring_should_round(connection, expected): 12 | geom = MultiLineString(expected, srid=4326) 13 | await connection.execute('INSERT INTO multilinestring_async (geom) ' 14 | 'VALUES ($1)', geom) 15 | geom = await connection.fetchval('SELECT geom FROM multilinestring_async ' 16 | 'WHERE geom=$1', geom, column=0) 17 | assert geom.coords == expected 18 | -------------------------------------------------------------------------------- /tests/asyncpg/test_multipoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import MultiPoint 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | @pytest.mark.parametrize('expected', [ 9 | ((30, 10), (10, 30)), 10 | ]) 11 | async def test_multipoint_should_round(connection, expected): 12 | geom = MultiPoint(expected, srid=4326) 13 | await connection.execute('INSERT INTO multipoint_async (geom) VALUES ($1)', 14 | geom) 15 | geom = await connection.fetchval('SELECT geom FROM multipoint_async WHERE ' 16 | 'geom=$1', geom, column=0) 17 | assert geom.coords == expected 18 | -------------------------------------------------------------------------------- /tests/asyncpg/test_multipolygon.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import MultiPolygon 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | MULTI = ( 8 | ( 9 | ((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), 10 | ((20, 30), (35, 35), (30, 20), (20, 30)) 11 | ), 12 | ( 13 | ((36, 10), (46, 45), (16, 40), (16, 20), (36, 10)), 14 | ((21, 30), (36, 35), (36, 20), (21, 30)) 15 | ), 16 | ) 17 | 18 | 19 | @pytest.mark.parametrize('expected', [ 20 | MULTI, 21 | ]) 22 | async def test_multipolygon_should_round(connection, expected): 23 | geom = MultiPolygon(expected, srid=4326) 24 | await connection.execute('INSERT INTO multipolygon_async (geom) ' 25 | 'VALUES ($1)', geom) 26 | geom = await connection.fetchval('SELECT geom FROM multipolygon_async ' 27 | 'WHERE geom=$1', geom, column=0) 28 | assert geom.coords == expected 29 | -------------------------------------------------------------------------------- /tests/asyncpg/test_point.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import Point 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | @pytest.mark.parametrize('expected', [ 9 | (1, -2), 10 | (-1.123456789, 2.987654321), 11 | ]) 12 | async def test_point_should_round(connection, expected): 13 | point = Point(*expected, srid=4326) 14 | await connection.execute('INSERT INTO point_async (geom) VALUES ($1)', 15 | point) 16 | geom = await connection.fetchval('SELECT geom FROM point_async WHERE ' 17 | 'geom=$1', point, column=0) 18 | assert geom.coords == expected 19 | 20 | 21 | async def test_point_with_geography_column(connection): 22 | await connection.execute('CREATE TABLE geo ("geom" geography(PointZ))') 23 | point = Point(1, 2, 3, srid=4326) 24 | await connection.execute('INSERT INTO geo (geom) VALUES ($1)', point) 25 | geom = await connection.fetchval('SELECT geom FROM geo WHERE geom=$1', 26 | point, column=0) 27 | assert geom.coords == (1, 2, 3) 28 | await connection.execute('DROP TABLE geo') 29 | -------------------------------------------------------------------------------- /tests/asyncpg/test_polygon.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import Polygon 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | @pytest.mark.parametrize('expected', [ 9 | (((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), ((20, 30), (35, 35), (30, 20), (20, 30))), # noqa 10 | ]) 11 | async def test_polygon_should_round(connection, expected): 12 | geom = Polygon(expected, srid=4326) 13 | await connection.execute('INSERT INTO polygon_async (geom) VALUES ($1)', 14 | geom) 15 | geom = await connection.fetchval('SELECT geom FROM polygon_async WHERE ' 16 | 'geom=$1', geom, column=0) 17 | assert geom.coords == expected 18 | -------------------------------------------------------------------------------- /tests/psycopg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tilery/python-postgis/fd31227e69735db92aef62642ef4ff6806d61871/tests/psycopg/__init__.py -------------------------------------------------------------------------------- /tests/psycopg/conftest.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | 3 | import pytest 4 | from postgis import (GeometryCollection, LineString, MultiLineString, 5 | MultiPoint, MultiPolygon, Point, Polygon, register) 6 | 7 | db = psycopg2.connect(dbname="test") 8 | cur = db.cursor() 9 | register(db) 10 | 11 | geoms = [Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, 12 | GeometryCollection] 13 | 14 | 15 | def pytest_configure(config): 16 | tpl = 'CREATE TABLE IF NOT EXISTS {} ("geom" geometry({}) NOT NULL)' 17 | for geom in geoms: 18 | name = geom.__name__ 19 | cur.execute(tpl.format(name.lower(), name)) 20 | 21 | 22 | def pytest_unconfigure(config): 23 | for geom in geoms: 24 | cur.execute('DROP TABLE {}'.format(geom.__name__.lower())) 25 | db.commit() 26 | db.close() 27 | 28 | 29 | @pytest.fixture 30 | def cursor(): 31 | # Make sure tables are clean. 32 | for geom in geoms: 33 | cur.execute('TRUNCATE TABLE {}'.format(geom.__name__.lower())) 34 | return cur 35 | -------------------------------------------------------------------------------- /tests/psycopg/test_geometrycollection.py: -------------------------------------------------------------------------------- 1 | from postgis import Point, LineString, Polygon, GeometryCollection 2 | 3 | POLYGON = Polygon(( 4 | ((1, 2), (3, 4), (5, 6), (1, 2)), 5 | ((2, 3), (4, 5), (6, 7), (2, 3)) 6 | )) 7 | 8 | COLLECTION = [ 9 | Point(1, 2), 10 | LineString(((1, 2), (3, 4))), 11 | POLYGON 12 | ] 13 | 14 | 15 | def test_geometrycollection_should_round(cursor): 16 | params = [GeometryCollection(COLLECTION, srid=4326)] 17 | cursor.execute('INSERT INTO geometrycollection (geom) VALUES (%s)', params) 18 | cursor.execute('SELECT geom FROM geometrycollection WHERE geom=%s', params) 19 | geom = cursor.fetchone()[0] 20 | assert geom == COLLECTION 21 | -------------------------------------------------------------------------------- /tests/psycopg/test_linestring.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import LineString 4 | 5 | 6 | @pytest.mark.parametrize('expected', [ 7 | ((30, 10), (10, 30), (40, 40)), 8 | ]) 9 | def test_linestring_should_round(cursor, expected): 10 | params = [LineString(expected, srid=4326)] 11 | cursor.execute('INSERT INTO linestring (geom) VALUES (%s)', params) 12 | cursor.execute('SELECT geom FROM linestring WHERE geom=%s', params) 13 | geom = cursor.fetchone()[0] 14 | assert geom.coords == expected 15 | -------------------------------------------------------------------------------- /tests/psycopg/test_multilinestring.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import MultiLineString 4 | 5 | 6 | @pytest.mark.parametrize('expected', [ 7 | (((30, 10), (10, 30)), ((40, 10), (10, 40))), 8 | ]) 9 | def test_multilinestring_should_round(cursor, expected): 10 | params = [MultiLineString(expected, srid=4326)] 11 | cursor.execute('INSERT INTO multilinestring (geom) VALUES (%s)', params) 12 | cursor.execute('SELECT geom FROM multilinestring WHERE geom=%s', params) 13 | geom = cursor.fetchone()[0] 14 | assert geom.coords == expected 15 | -------------------------------------------------------------------------------- /tests/psycopg/test_multipoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import MultiPoint 4 | 5 | 6 | @pytest.mark.parametrize('expected', [ 7 | ((30, 10), (10, 30)), 8 | ]) 9 | def test_multipoint_should_round(cursor, expected): 10 | params = [MultiPoint(expected, srid=4326)] 11 | cursor.execute('INSERT INTO multipoint (geom) VALUES (%s)', params) 12 | cursor.execute('SELECT geom FROM multipoint WHERE geom=%s', params) 13 | geom = cursor.fetchone()[0] 14 | assert geom.coords == expected 15 | -------------------------------------------------------------------------------- /tests/psycopg/test_multipolygon.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import MultiPolygon 4 | 5 | MULTI = ( 6 | ( 7 | ((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), 8 | ((20, 30), (35, 35), (30, 20), (20, 30)) 9 | ), 10 | ( 11 | ((36, 10), (46, 45), (16, 40), (16, 20), (36, 10)), 12 | ((21, 30), (36, 35), (36, 20), (21, 30)) 13 | ), 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize('expected', [ 18 | MULTI, 19 | ]) 20 | def test_multipolygon_should_round(cursor, expected): 21 | params = [MultiPolygon(expected, srid=4326)] 22 | cursor.execute('INSERT INTO multipolygon (geom) VALUES (%s)', params) 23 | cursor.execute('SELECT geom FROM multipolygon WHERE geom=%s', params) 24 | geom = cursor.fetchone()[0] 25 | assert geom.coords == expected 26 | -------------------------------------------------------------------------------- /tests/psycopg/test_point.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import Point 4 | 5 | 6 | @pytest.mark.parametrize('expected', [ 7 | (1, -2), 8 | (-1.123456789, 2.987654321), 9 | ]) 10 | def test_point_should_round(cursor, expected): 11 | params = [Point(*expected, srid=4326)] 12 | cursor.execute('INSERT INTO point (geom) VALUES (%s)', params) 13 | cursor.execute('SELECT geom FROM point WHERE geom=%s', params) 14 | geom = cursor.fetchone()[0] 15 | assert geom.coords == expected 16 | 17 | 18 | @pytest.mark.parametrize('expected', [ 19 | (1, -2, 3), 20 | (-1.123456789, 2.987654321, 231), 21 | (1, -2, 0), 22 | ]) 23 | def test_point_geography_column_should_round(cursor, expected): 24 | cursor.execute('CREATE TABLE geography_point ("geom" geography(PointZ))') 25 | params = [Point(*expected, srid=4326)] 26 | cursor.execute('INSERT INTO geography_point (geom) VALUES (%s)', params) 27 | cursor.execute('SELECT geom FROM geography_point WHERE geom=%s', params) 28 | geom = cursor.fetchone()[0] 29 | assert geom.coords == expected 30 | cursor.execute('DROP TABLE geography_point') 31 | -------------------------------------------------------------------------------- /tests/psycopg/test_polygon.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from postgis import Polygon 4 | 5 | 6 | @pytest.mark.parametrize('expected', [ 7 | (((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), ((20, 30), (35, 35), (30, 20), (20, 30))), # noqa 8 | ]) 9 | def test_polyggon_should_round(cursor, expected): 10 | params = [Polygon(expected, srid=4326)] 11 | cursor.execute('INSERT INTO polygon (geom) VALUES (%s)', params) 12 | cursor.execute('SELECT geom FROM polygon WHERE geom=%s', params) 13 | geom = cursor.fetchone()[0] 14 | assert geom.coords == expected 15 | -------------------------------------------------------------------------------- /tests/test_geometrycollection.py: -------------------------------------------------------------------------------- 1 | from postgis import Point, LineString, Polygon, GeometryCollection 2 | 3 | POLYGON = Polygon(( 4 | ((1, 2), (3, 4), (5, 6), (1, 2)), 5 | ((2, 3), (4, 5), (6, 7), (2, 3)) 6 | )) 7 | 8 | COLLECTION = [ 9 | Point(1, 2), 10 | LineString(((1, 2), (3, 4))), 11 | POLYGON 12 | ] 13 | 14 | 15 | def test_geometrycollection_geojson(): 16 | collection = GeometryCollection(COLLECTION) 17 | assert collection.geojson == { 18 | "type": "GeometryCollection", 19 | "geometries": [ 20 | {'type': 'Point', 'coordinates': (1, 2)}, 21 | {'type': 'LineString', 'coordinates': ((1, 2), (3, 4))}, 22 | {'type': 'Polygon', 'coordinates': ( 23 | ((1, 2), (3, 4), (5, 6), (1, 2)), 24 | ((2, 3), (4, 5), (6, 7), (2, 3)) 25 | )}, 26 | ] 27 | } 28 | 29 | 30 | def test_geometrycollection_get_item(): 31 | collection = GeometryCollection(COLLECTION) 32 | assert collection[2] == POLYGON 33 | 34 | 35 | def test_geometrycollection_iter(): 36 | collection = GeometryCollection(COLLECTION) 37 | for i, geom in enumerate(collection): 38 | assert geom == COLLECTION[i] 39 | -------------------------------------------------------------------------------- /tests/test_linestring.py: -------------------------------------------------------------------------------- 1 | from postgis import LineString 2 | 3 | 4 | def test_linestring_geojson(): 5 | line = LineString(((1, 2), (3, 4))) 6 | assert line.geojson == {"type": "LineString", 7 | "coordinates": ((1, 2), (3, 4))} 8 | 9 | 10 | def test_linestring_geojson_as_string(): 11 | line = LineString(((1, 2), (3, 4))) 12 | geojson = str(line.geojson) 13 | assert '"type": "LineString"' in geojson 14 | assert '"coordinates": [[1.0, 2.0], [3.0, 4.0]]' in geojson 15 | 16 | 17 | def test_geom_should_compare_with_coords(): 18 | assert ((30, 10), (10, 30), (40, 40)) == LineString(((30, 10), (10, 30), (40, 40))) # noqa 19 | 20 | 21 | def test_linestring_get_item(): 22 | line = LineString(((30, 10), (10, 30), (40, 40))) 23 | assert line[0] == (30, 10) 24 | -------------------------------------------------------------------------------- /tests/test_multilinestring.py: -------------------------------------------------------------------------------- 1 | from postgis import MultiLineString, LineString 2 | 3 | 4 | def test_multilinestring_geojson(): 5 | multi = MultiLineString((((30, 10), (10, 30)), ((40, 10), (10, 40)))) 6 | assert multi.geojson == { 7 | "type": "MultiLineString", 8 | "coordinates": (((30, 10), (10, 30)), ((40, 10), (10, 40))) 9 | } 10 | 11 | 12 | def test_geom_should_compare_with_coords(): 13 | assert (((30, 10), (10, 30)), ((40, 10), (10, 40))) == MultiLineString((((30, 10), (10, 30)), ((40, 10), (10, 40)))) # noqa 14 | 15 | 16 | def test_multilinestring_get_item(): 17 | multi = MultiLineString((((30, 10), (10, 30)), ((40, 10), (10, 40)))) 18 | assert multi[0] == LineString(((30, 10), (10, 30))) 19 | -------------------------------------------------------------------------------- /tests/test_multipoint.py: -------------------------------------------------------------------------------- 1 | from postgis import MultiPoint, Point 2 | 3 | 4 | def test_multipoint_geojson(): 5 | line = MultiPoint(((1, 2), (3, 4))) 6 | assert line.geojson == {"type": "MultiPoint", 7 | "coordinates": ((1, 2), (3, 4))} 8 | 9 | 10 | def test_geom_should_compare_with_coords(): 11 | assert ((30, 10), (10, 30), (40, 40)) == MultiPoint(((30, 10), (10, 30), (40, 40))) # noqa 12 | 13 | 14 | def test_multipoint_get_item(): 15 | multi = MultiPoint(((30, 10), (10, 30), (40, 40))) 16 | assert multi[0] == Point(30, 10) 17 | -------------------------------------------------------------------------------- /tests/test_multipolygon.py: -------------------------------------------------------------------------------- 1 | from postgis import MultiPolygon, Polygon 2 | 3 | MULTI = ( 4 | ( 5 | ((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), 6 | ((20, 30), (35, 35), (30, 20), (20, 30)) 7 | ), 8 | ( 9 | ((36, 10), (46, 45), (16, 40), (16, 20), (36, 10)), 10 | ((21, 30), (36, 35), (36, 20), (21, 30)) 11 | ), 12 | ) 13 | 14 | 15 | def test_multilinestring_geojson(): 16 | multi = MultiPolygon(MULTI) 17 | assert multi.geojson == { 18 | "type": "MultiPolygon", 19 | "coordinates": MULTI 20 | } 21 | 22 | 23 | def test_geom_should_compare_with_coords(): 24 | assert MULTI == MultiPolygon(MULTI) 25 | 26 | 27 | def test_multipolygon_get_item(): 28 | multi = MultiPolygon(MULTI) 29 | assert multi[0] == Polygon(MULTI[0]) 30 | 31 | 32 | def test_multipolygon_wkt(): 33 | multi = MultiPolygon(MULTI) 34 | wkt = multi.wkt 35 | wkt = wkt.replace('.0','') 36 | wkt = wkt.replace(', ',',') 37 | assert wkt == 'MULTIPOLYGON(((35 10,45 45,15 40,10 20,35 10),(20 30,35 35,30 20,20 30)),((36 10,46 45,16 40,16 20,36 10),(21 30,36 35,36 20,21 30)))' 38 | -------------------------------------------------------------------------------- /tests/test_point.py: -------------------------------------------------------------------------------- 1 | from postgis import Point 2 | 3 | 4 | def test_point_geojson(): 5 | point = Point(1, 2) 6 | assert point.geojson == {"type": "Point", "coordinates": (1, 2)} 7 | 8 | 9 | def test_point_geojson_as_string(): 10 | point = Point(1, 2) 11 | geojson = str(point.geojson) 12 | assert '"type": "Point"' in geojson 13 | assert '"coordinates": [1.0, 2.0]' in geojson 14 | 15 | 16 | def test_point_should_compare_with_coords(): 17 | assert (-1.123456789, 2.987654321) == Point(-1.123456789, 2.987654321) 18 | 19 | 20 | def test_two_point_should_compare(): 21 | assert Point(1, -2) == Point(1, -2) 22 | 23 | 24 | def test_can_create_point_from_list(): 25 | point = Point([1, 2]) 26 | assert point.x == 1 27 | assert point.y == 2 28 | 29 | 30 | def test_can_get_point_item(): 31 | point = Point(1, 2) 32 | assert point[0] == 1 33 | assert point[1] == 2 34 | assert point['x'] == 1 35 | assert point['y'] == 2 36 | 37 | 38 | def test_can_create_point_with_z(): 39 | point = Point(1, 2, 3) 40 | assert point.has_z 41 | assert point.z == 3 42 | 43 | 44 | def test_point_geojson_with_z(): 45 | point = Point(1, 2, 3) 46 | assert point.geojson == {"type": "Point", "coordinates": (1, 2, 3)} 47 | 48 | 49 | def test_point_can_be_unpacked(): 50 | point = Point(1, 2) 51 | x, y = point 52 | assert x == 1 53 | assert y == 2 54 | 55 | 56 | def test_point_can_be_unpacked_to_dict(): 57 | point = Point(1, 2) 58 | data = dict(point) 59 | assert data['x'] == 1 60 | assert data['y'] == 2 61 | assert len(data) == 2 62 | 63 | 64 | def test_point_with_z_can_be_unpacked(): 65 | point = Point(1, 2, 3) 66 | x, y, z = point 67 | assert x == 1 68 | assert y == 2 69 | assert z == 3 70 | 71 | 72 | def test_point_with_z_can_be_unpacked_to_dict(): 73 | point = Point(1, 2, 3) 74 | data = dict(point) 75 | assert data['x'] == 1 76 | assert data['y'] == 2 77 | assert data['z'] == 3 78 | assert len(data) == 3 79 | 80 | 81 | def test_string_are_cast(): 82 | point = Point('1', '2', '3') 83 | assert point.x == 1.0 84 | assert point.y == 2.0 85 | assert point.z == 3 86 | 87 | 88 | def test_0_as_z_is_considered(): 89 | point = Point(1, 2, 0) 90 | assert point.x == 1.0 91 | assert point.y == 2.0 92 | assert point.z == 0 93 | 94 | 95 | def test_0_as_m_is_considered(): 96 | point = Point(1, 2, 3, 0) 97 | assert point.x == 1.0 98 | assert point.y == 2.0 99 | assert point.z == 3 100 | assert point.m == 0 101 | -------------------------------------------------------------------------------- /tests/test_polygon.py: -------------------------------------------------------------------------------- 1 | from postgis import Polygon 2 | 3 | 4 | def test_geom_should_compare_with_coords(): 5 | assert (((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), ((20, 30), (35, 35), (30, 20), (20, 30))) == Polygon((((35, 10), (45, 45), (15, 40), (10, 20), (35, 10)), ((20, 30), (35, 35), (30, 20), (20, 30)))) # noqa 6 | 7 | 8 | def test_polygon_geojson(): 9 | poly = Polygon((((1, 2), (3, 4), (5, 6), (1, 2)),)) 10 | assert poly.geojson == {"type": "Polygon", 11 | "coordinates": (((1, 2), (3, 4), (5, 6), (1, 2)),)} 12 | 13 | 14 | def test_polygon_wkt(): 15 | poly = Polygon((((1, 2), (3, 4), (5, 6), (1, 2)),)) 16 | wkt = poly.wkt 17 | wkt = wkt.replace('.0','') 18 | wkt = wkt.replace(', ',',') 19 | assert wkt == 'POLYGON((1 2,3 4,5 6,1 2))' 20 | --------------------------------------------------------------------------------