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;shapelyis imported at module level and required for WKB decoding.Clear error messages - missing
shapelyraisesDependencyMissingErrorwith 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].
NaNandinfvalues are rejected withInvalidPointError.
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 |
|---|---|---|
|
|
Spatial Reference ID (WGS-84). |
|
|
Use PostGIS |
|
|
Allow |
|
|
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 |
|---|---|---|
|
|
|
|
|
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, andlinestringwithPGDialect.ischema_namesso SQLAlchemy introspection recognises PostGIS columns.
- is_postgresql() bool
Return
Trueif the configured database engine targets PostgreSQL. Checksdb_connection._engine.urlfirst, then falls back toDATABASES['default']['OPTIONS']['URL'].
- class GeoType
sqlalchemy.types.UserDefinedTypeplaceholder for PostGISGEOMETRYcolumns during SQLAlchemy reflection.
- class AdaptiveGeometryType
sqlalchemy.types.TypeDecoratorthat wraps EWKT for PostGIS at runtime. UsesST_GeomFromEWKTon bind andST_AsEWKTon 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 |
|---|---|
Base class for all geolocation errors. |
|
Coordinate out of range, NaN/Inf, or malformed WKT/GeoJSON input. |
|
Optional dependency not installed. Subclass of both |
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:
objectAbstract base for geolocation database backends.
- column_ddl(field)[source]
Return SQL column type string for DDL generation.
- Parameters:
field (PointField)
- Return type:
- openviper.contrib.fields.geolocation.GeoDependencyMissingError
alias of
DependencyMissingError
- exception openviper.contrib.fields.geolocation.GeoLocationError[source]
Bases:
ExceptionBase exception for geolocation errors.
- exception openviper.contrib.fields.geolocation.InvalidPointError[source]
Bases:
GeoLocationError,ValueErrorRaised when a Point is constructed with out-of-range coordinates.
- class openviper.contrib.fields.geolocation.Point(longitude, latitude, srid=4326)[source]
Bases:
objectGeographic point as (longitude, latitude) in WGS-84.
- longitude
- latitude
- srid
- class openviper.contrib.fields.geolocation.PointField(srid=4326, geography=False, **kwargs)[source]
Bases:
FieldORM field storing a geographic Point.
- column_type = 'GEOMETRY(Point,4326)'
- openviper.contrib.fields.geolocation.get_backend(dialect)[source]
Return geo backend for dialect, falling back to generic.
- Parameters:
dialect (str)
- Return type:
- openviper.contrib.fields.geolocation.haversine_distance(point_a, point_b)[source]
Haversine great-circle distance between two points in metres.
- openviper.contrib.fields.geolocation.parse_point(value, srid=4326)[source]
Best-effort conversion of arbitrary value to Point.
- class openviper.contrib.fields.geolocation.geometry.Point(longitude, latitude, srid=4326)[source]
Bases:
objectGeographic point as (longitude, latitude) in WGS-84.
- longitude
- latitude
- srid
- class openviper.contrib.fields.geolocation.fields.PointField(srid=4326, geography=False, **kwargs)[source]
Bases:
FieldORM field storing a geographic Point.
- column_type = 'GEOMETRY(Point,4326)'
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:
objectAbstract base for geolocation database backends.
- column_ddl(field)[source]
Return SQL column type string for DDL generation.
- Parameters:
field (PointField)
- Return type:
- class openviper.contrib.fields.geolocation.backends.PostGISBackend[source]
Bases:
BaseGeoBackendPostgreSQL/PostGIS spatial backend using EWKT.
- column_ddl(field)[source]
Return SQL column type string for DDL generation.
- Parameters:
field (PointField)
- Return type:
- class openviper.contrib.fields.geolocation.backends.FallbackTextBackend[source]
Bases:
BaseGeoBackendGeneric fallback storing WKT in TEXT columns.
- column_ddl(field)[source]
Return SQL column type string for DDL generation.
- Parameters:
field (PointField)
- Return type:
- openviper.contrib.fields.geolocation.backends.get_backend(dialect)[source]
Return geo backend for dialect, falling back to generic.
- Parameters:
dialect (str)
- Return type:
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.
- openviper.contrib.fields.geolocation.utils.point_from_wkb_hex(hex_str, srid=4326)[source]
Decode hex-encoded WKB string from PostGIS into Point.
- openviper.contrib.fields.geolocation.utils.parse_point(value, srid=4326)[source]
Best-effort conversion of arbitrary value to Point.
- openviper.contrib.fields.geolocation.utils.haversine_distance(point_a, point_b)[source]
Haversine great-circle distance between two points in metres.
Exceptions raised by geolocation contrib.
- exception openviper.contrib.fields.geolocation.exceptions.GeoLocationError[source]
Bases:
ExceptionBase exception for geolocation errors.
- exception openviper.contrib.fields.geolocation.exceptions.DependencyMissingError(package)[source]
Bases:
GeoLocationError,ImportErrorRaised 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,ValueErrorRaised when a Point is constructed with out-of-range coordinates.