Geolocation

The openviper.contrib.fields.geolocation package provides PostGIS-compatible geolocation support for OpenViper models. It adds a Point geometry class and a PointField ORM field that maps to GEOMETRY(Point, 4326) on PostgreSQL/PostGIS and falls back to a WKT TEXT column on other databases.

shapely is a required dependency, installed automatically via the Geolocation extras or directly with pip install shapely>=2.0.

Overview

  • PostGIS-ready - GEOMETRY(Point, 4326) column DDL out of the box.

  • Fallback TEXT backend - store WKT strings on any database.

  • Pure-Python Point - no external library needed for basic use.

  • Haversine distance - great-circle distance without shapely.

  • WKT / EWKT / GeoJSON - three serialisation formats included.

  • shapely interop - convert to/from shapely.geometry.Point; shapely is imported at module level and required for WKB decoding.

  • Clear error messages - missing shapely raises DependencyMissingError with the install command.

Installation

shapely is a required dependency for the geolocation module. Install via the Geolocation extras (recommended) or directly:

pip install openviper[Geolocation]

This installs:

  • shapely>=2.0 - hex-WKB decoding and Shapely interoperability.

  • psycopg2-binary>=2.9 - PostgreSQL driver for PostGIS databases.

Basic Usage

Define a model with a geographic location:

from openviper.db import Model
from openviper.contrib.fields.geolocation import Point, PointField
from openviper.db.fields import AutoField, CharField


class Store(Model):
    name: str = CharField(max_length=200)
    location: Point | None = PointField(null=True)

Create and query records - ORM operations are async:

import asyncio

from openviper.contrib.fields.geolocation import Point, PointField
from openviper.db import Model
from openviper.db.fields import AutoField, CharField


class Store(Model):
    id: int = AutoField()
    name: str = CharField(max_length=200)
    location: Point | None = PointField(null=True)


async def create_store() -> Store:
    store: Store = await Store.objects.create(
        name="Shop",
        location=Point(-0.1276, 51.5074),  # longitude, latitude
    )
    return store


async def fetch_store(store_id: int) -> None:
    shop: Store = await Store.objects.get(id=store_id)
    if shop.location is not None:
        print(shop.location.to_wkt())   # POINT(-0.1276 51.5074)
        print(shop.location.to_ewkt())  # SRID=4326;POINT(-0.1276 51.5074)


async def main() -> None:
    store = await create_store()
    await fetch_store(store.id)

Point Geometry

from openviper.contrib.fields.geolocation import Point

# Construct from longitude, latitude (both are floats)
london: Point = Point(-0.1276, 51.5074)
paris: Point  = Point(2.3522, 48.8566)

# WKT serialisation
wkt: str = london.to_wkt()    # 'POINT(-0.1276 51.5074)'
ewkt: str = london.to_ewkt()  # 'SRID=4326;POINT(-0.1276 51.5074)'

# GeoJSON
gj: dict[str, object] = london.to_geojson()
# {'type': 'Point', 'coordinates': [-0.1276, 51.5074]}

# Parse from WKT
p: Point = Point.from_wkt("POINT(10.0 20.0)")

# Parse from GeoJSON dict
p = Point.from_geojson({"type": "Point", "coordinates": [10.0, 20.0]})

# Haversine distance (metres, pure Python)
distance: float = london.distance_to(paris)  # ~341 000 m

Coordinate validation:

  • Longitude must be in [-180, 180].

  • Latitude must be in [-90, 90].

  • NaN and inf values are rejected with InvalidPointError.

PointField

from openviper.contrib.fields.geolocation import Point, PointField
from openviper.db import Model
from openviper.db.fields import AutoField, CharField


class Restaurant(Model):
    id: int = AutoField()
    name: str = CharField(max_length=200)
    location: Point | None = PointField(null=True)

Constructor arguments:

Argument

Default

Description

srid

4326

Spatial Reference ID (WGS-84).

geography

False

Use PostGIS GEOGRAPHY type for accurate metric distances.

null

False

Allow NULL values.

db_index

False

Add a database index on the column.

On PostgreSQL/PostGIS the migration engine generates:

location GEOMETRY(Point,4326)

or, with geography=True:

location GEOGRAPHY(Point,4326)

On all other databases the column is declared as TEXT and the value is stored in WKT format.

Backends

The backend layer handles database-specific serialisation. It is selected automatically via get_backend():

from openviper.contrib.fields.geolocation.backends import BaseGeoBackend, get_backend

backend: BaseGeoBackend = get_backend("postgresql")   # PostGISBackend
backend = get_backend("sqlite")                       # FallbackTextBackend

Supported dialects:

Dialect strings

Backend class

Column type

postgresql, postgres, postgis

PostGISBackend

GEOMETRY(Point,<srid>)

sqlite, mysql, mariadb, mssql, oracle, generic

FallbackTextBackend

TEXT

The backend registry is a module-level dict:

BACKEND_REGISTRY

dict[str, BaseGeoBackend] mapping lowercase dialect strings to singleton backend instances. get_backend() looks up this registry and falls back to "generic".

SQLAlchemy type helpers

The openviper.contrib.fields.geolocation.fields module registers PostGIS types with SQLAlchemy’s PostgreSQL dialect at import time and provides adaptive column types:

register_postgis_types() None

Register geometry, geography, point, polygon, and linestring with PGDialect.ischema_names so SQLAlchemy introspection recognises PostGIS columns.

is_postgresql() bool

Return True if the configured database engine targets PostgreSQL. Checks db_connection._engine.url first, then falls back to DATABASES['default']['OPTIONS']['URL'].

class GeoType

sqlalchemy.types.UserDefinedType placeholder for PostGIS GEOMETRY columns during SQLAlchemy reflection.

class AdaptiveGeometryType

sqlalchemy.types.TypeDecorator that wraps EWKT for PostGIS at runtime. Uses ST_GeomFromEWKT on bind and ST_AsEWKT on fetch when connected to PostgreSQL; passes through unchanged on other databases.

Utilities

from openviper.contrib.fields.geolocation import Point
from openviper.contrib.fields.geolocation.utils import (
    haversine_distance,
    parse_point,
    point_from_shapely,
    point_from_wkb_hex,
    point_to_shapely,
    require_shapely,
)

london: Point = Point(-0.1276, 51.5074)
paris: Point  = Point(2.3522, 48.8566)

# Best-effort coercion from any input
p: Point | None = parse_point((-0.1276, 51.5074))          # tuple
p = parse_point([10.0, 20.0])                               # list
p = parse_point("POINT(10.0 20.0)")                         # WKT string
p = parse_point({"type": "Point", "coordinates": [10.0, 20.0]})  # GeoJSON dict

# Pure-Python Haversine distance (returns metres)
distance: float = haversine_distance(london, paris)

# shapely interop (requires shapely, installed via openviper[Geolocation])
import shapely.geometry

shapely_pt: shapely.geometry.Point = point_to_shapely(london)
back: Point = point_from_shapely(shapely_pt)

# Decode hex-encoded WKB from PostGIS
pt: Point = point_from_wkb_hex("0101000020e6100000...", srid=4326)

# Access the shapely.geometry module directly
sg = require_shapely()

Errors

Exception

When raised

GeoLocationError

Base class for all geolocation errors.

InvalidPointError

Coordinate out of range, NaN/Inf, or malformed WKT/GeoJSON input.

DependencyMissingError

Optional dependency not installed. Subclass of both GeoLocationError and ImportError.

DependencyMissingError is a subclass of both GeoLocationError and ImportError, so existing except ImportError guards continue to work.

Example:

from openviper.contrib.fields.geolocation import Point
from openviper.contrib.fields.geolocation.exceptions import DependencyMissingError
from openviper.contrib.fields.geolocation.utils import point_to_shapely

my_point: Point = Point(-0.1276, 51.5074)
try:
    s = point_to_shapely(my_point)
except DependencyMissingError as exc:
    print(exc)  # "pip install openviper[Geolocation]"

Settings & Configuration

No framework-level settings are required. The recommended pattern is to configure PointField options directly on the field and pass connection details through the standard OpenViper DATABASE setting.

Example settings.py with typed annotations:

from __future__ import annotations

# Database - PostGIS requires asyncpg or psycopg2-binary
DATABASE: dict[str, str | int] = {
    "ENGINE": "postgresql",
    "NAME": "mydb",
    "USER": "postgres",
    "PASSWORD": "secret",
    "HOST": "localhost",
    "PORT": 5432,
}

# Optional: limit JSON/file sizes (unrelated to geolocation, shown for completeness)
MAX_FILE_SIZE: int = 10 * 1024 * 1024    # 10 MB
MAX_JSON_SIZE: int = 1 * 1024 * 1024     # 1 MB

# MEDIA_DIR is used by FileField; geolocation does not write files
MEDIA_DIR: str = "./media"

INSTALLED_APPS: list[str] = [
    "myapp",
]

Per-field configuration example on a model:

from openviper.contrib.fields.geolocation import Point, PointField
from openviper.db import Model
from openviper.db.fields import AutoField, CharField


class Location(Model):
    """Stores a named geographic location."""

    id: int = AutoField()
    name: str = CharField(max_length=200)

    # Standard WGS-84 geometry column
    point: Point | None = PointField(null=True, srid=4326)

    # Geography type - enables accurate ST_Distance without SRID transforms
    point_geo: Point | None = PointField(
        null=True,
        srid=4326,
        geography=True,
    )

    # Indexed geometry for spatial queries
    point_indexed: Point | None = PointField(
        null=True,
        db_index=True,
    )

API Reference

PostGIS-compatible geolocation support.

Provides PointField ORM field and Point geometry class. Shapely integration available via the Geolocation extras.

class openviper.contrib.fields.geolocation.BaseGeoBackend[source]

Bases: object

Abstract base for geolocation database backends.

dialect: str = 'generic'
column_ddl(field)[source]

Return SQL column type string for DDL generation.

Parameters:

field (PointField)

Return type:

str

to_db(value)[source]

Serialise Point for database insertion.

Parameters:

value (Point | None)

Return type:

object

to_python(raw, srid=4326)[source]

Deserialise raw database value to Point.

Parameters:
Return type:

Point | None

openviper.contrib.fields.geolocation.GeoDependencyMissingError

alias of DependencyMissingError

exception openviper.contrib.fields.geolocation.GeoLocationError[source]

Bases: Exception

Base exception for geolocation errors.

exception openviper.contrib.fields.geolocation.InvalidPointError[source]

Bases: GeoLocationError, ValueError

Raised when a Point is constructed with out-of-range coordinates.

class openviper.contrib.fields.geolocation.Point(longitude, latitude, srid=4326)[source]

Bases: object

Geographic point as (longitude, latitude) in WGS-84.

Parameters:
longitude
latitude
srid
to_wkt()[source]

Return WKT representation.

Return type:

str

to_ewkt()[source]

Return EWKT representation including SRID.

Return type:

str

to_geojson()[source]

Return GeoJSON-compatible dict.

Return type:

GeoJSONOutput

distance_to(other)[source]

Haversine great-circle distance to other point in metres.

Parameters:

other (Point)

Return type:

float

classmethod from_wkt(wkt, srid=4326)[source]

Parse WKT string into Point.

Parameters:
Return type:

Point

classmethod from_geojson(data, srid=4326)[source]

Construct Point from GeoJSON geometry dict.

Parameters:
  • data (GeoJSONObject)

  • srid (int)

Return type:

Point

class openviper.contrib.fields.geolocation.PointField(srid=4326, geography=False, **kwargs)[source]

Bases: Field

ORM field storing a geographic Point.

Parameters:
  • srid (int)

  • geography (bool)

  • kwargs (Any)

column_type = 'GEOMETRY(Point,4326)'
property db_column_type: str

Return the PostGIS column type string.

to_python(value)[source]

Convert database value to Point instance.

Parameters:

value (object)

Return type:

Point | None

to_db(value)[source]

Serialise Point to EWKT for database storage.

Parameters:

value (object)

Return type:

str | None

get_sa_type()[source]

Return AdaptiveGeometryType for SQLAlchemy column.

Return type:

TypeEngine

validate(value)[source]

Run Field-level validation, then confirm value is a valid Point.

Parameters:

value (object)

Return type:

None

openviper.contrib.fields.geolocation.get_backend(dialect)[source]

Return geo backend for dialect, falling back to generic.

Parameters:

dialect (str)

Return type:

BaseGeoBackend

openviper.contrib.fields.geolocation.haversine_distance(point_a, point_b)[source]

Haversine great-circle distance between two points in metres.

Parameters:
Return type:

float

openviper.contrib.fields.geolocation.parse_point(value, srid=4326)[source]

Best-effort conversion of arbitrary value to Point.

Parameters:
Return type:

Point | None

class openviper.contrib.fields.geolocation.geometry.Point(longitude, latitude, srid=4326)[source]

Bases: object

Geographic point as (longitude, latitude) in WGS-84.

Parameters:
longitude
latitude
srid
to_wkt()[source]

Return WKT representation.

Return type:

str

to_ewkt()[source]

Return EWKT representation including SRID.

Return type:

str

to_geojson()[source]

Return GeoJSON-compatible dict.

Return type:

GeoJSONOutput

distance_to(other)[source]

Haversine great-circle distance to other point in metres.

Parameters:

other (Point)

Return type:

float

classmethod from_wkt(wkt, srid=4326)[source]

Parse WKT string into Point.

Parameters:
Return type:

Point

classmethod from_geojson(data, srid=4326)[source]

Construct Point from GeoJSON geometry dict.

Parameters:
  • data (GeoJSONObject)

  • srid (int)

Return type:

Point

class openviper.contrib.fields.geolocation.fields.PointField(srid=4326, geography=False, **kwargs)[source]

Bases: Field

ORM field storing a geographic Point.

Parameters:
  • srid (int)

  • geography (bool)

  • kwargs (Any)

column_type = 'GEOMETRY(Point,4326)'
property db_column_type: str

Return the PostGIS column type string.

to_python(value)[source]

Convert database value to Point instance.

Parameters:

value (object)

Return type:

Point | None

to_db(value)[source]

Serialise Point to EWKT for database storage.

Parameters:

value (object)

Return type:

str | None

get_sa_type()[source]

Return AdaptiveGeometryType for SQLAlchemy column.

Return type:

TypeEngine

validate(value)[source]

Run Field-level validation, then confirm value is a valid Point.

Parameters:

value (object)

Return type:

None

Backend adapters for geolocation database columns.

Each backend maps PointField to SQL DDL, serialises Point values for writes, and deserialises raw driver values to Point instances.

class openviper.contrib.fields.geolocation.backends.BaseGeoBackend[source]

Bases: object

Abstract base for geolocation database backends.

dialect: str = 'generic'
column_ddl(field)[source]

Return SQL column type string for DDL generation.

Parameters:

field (PointField)

Return type:

str

to_db(value)[source]

Serialise Point for database insertion.

Parameters:

value (Point | None)

Return type:

object

to_python(raw, srid=4326)[source]

Deserialise raw database value to Point.

Parameters:
Return type:

Point | None

class openviper.contrib.fields.geolocation.backends.PostGISBackend[source]

Bases: BaseGeoBackend

PostgreSQL/PostGIS spatial backend using EWKT.

dialect: str = 'postgresql'
column_ddl(field)[source]

Return SQL column type string for DDL generation.

Parameters:

field (PointField)

Return type:

str

to_db(value)[source]

Serialise Point for database insertion.

Parameters:

value (Point | None)

Return type:

str | None

to_python(raw, srid=4326)[source]

Deserialise raw database value to Point.

Parameters:
Return type:

Point | None

class openviper.contrib.fields.geolocation.backends.FallbackTextBackend[source]

Bases: BaseGeoBackend

Generic fallback storing WKT in TEXT columns.

dialect: str = 'generic'
column_ddl(field)[source]

Return SQL column type string for DDL generation.

Parameters:

field (PointField)

Return type:

str

to_db(value)[source]

Serialise Point for database insertion.

Parameters:

value (Point | None)

Return type:

str | None

to_python(raw, srid=4326)[source]

Deserialise raw database value to Point.

Parameters:
Return type:

Point | None

openviper.contrib.fields.geolocation.backends.get_backend(dialect)[source]

Return geo backend for dialect, falling back to generic.

Parameters:

dialect (str)

Return type:

BaseGeoBackend

Utility helpers for geolocation.

Requires shapely for WKB and geometry conversion helpers. Pure-Python helpers (parse_point, haversine_distance) have no external requirements.

openviper.contrib.fields.geolocation.utils.require_shapely()[source]

Return the shapely.geometry module.

Return type:

ShapelyGeometryModule

openviper.contrib.fields.geolocation.utils.point_to_shapely(point)[source]

Convert Point to shapely.geometry.Point.

Parameters:

point (Point)

Return type:

ShapelyPoint

openviper.contrib.fields.geolocation.utils.point_from_shapely(shapely_point, srid=4326)[source]

Convert shapely.geometry.Point to Point.

Parameters:
  • shapely_point (ShapelyPoint)

  • srid (int)

Return type:

Point

openviper.contrib.fields.geolocation.utils.point_from_wkb_hex(hex_str, srid=4326)[source]

Decode hex-encoded WKB string from PostGIS into Point.

Parameters:
Return type:

Point

openviper.contrib.fields.geolocation.utils.parse_point(value, srid=4326)[source]

Best-effort conversion of arbitrary value to Point.

Parameters:
Return type:

Point | None

openviper.contrib.fields.geolocation.utils.haversine_distance(point_a, point_b)[source]

Haversine great-circle distance between two points in metres.

Parameters:
Return type:

float

Exceptions raised by geolocation contrib.

exception openviper.contrib.fields.geolocation.exceptions.GeoLocationError[source]

Bases: Exception

Base exception for geolocation errors.

exception openviper.contrib.fields.geolocation.exceptions.DependencyMissingError(package)[source]

Bases: GeoLocationError, ImportError

Raised when an optional geolocation dependency is not installed.

Parameters:

package (str)

Return type:

None

MESSAGE = 'The openviper geolocation module requires optional dependencies that are not installed.\nInstall them with:  pip install openviper[Geolocation]\nMissing package: {package}'
exception openviper.contrib.fields.geolocation.exceptions.InvalidPointError[source]

Bases: GeoLocationError, ValueError

Raised when a Point is constructed with out-of-range coordinates.