Skip to content

Commit

Permalink
Added geo queries
Browse files Browse the repository at this point in the history
  • Loading branch information
gurcuff91 committed May 30, 2024
1 parent 2c54baa commit 11799aa
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 19 deletions.
2 changes: 1 addition & 1 deletion mongotoy/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def _get_document_indexes(
(
mappers.MultiPointMapper,
mappers.MultiLineStringMapper,
mappers.PolygonMapper,
mappers.MultiPolygonMapper,
)
):
indexes.append(
Expand Down
17 changes: 7 additions & 10 deletions mongotoy/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,23 @@ def __new__(mcls, name, bases, namespace, **kwargs):
_fields.update(getattr(base, '__fields__', {}))

# Add class namespace declared fields
for field_name, anno_type in namespace.get('__annotations__', {}).items():
options = namespace.get(field_name, fields.FieldOptions())
for field_name, annotated_type in namespace.get('__annotations__', {}).items():
options = namespace.get(field_name, expressions.EmptyValue)
if not isinstance(options, fields.FieldOptions):
# noinspection PyTypeChecker,SpellCheckingInspection
raise DocumentError(
loc=(name, field_name),
msg=f'Invalid field descriptor {type(options)}. '
f'Use mongotoy.field() or mongotoy.reference() descriptors'
)
options = fields.FieldOptions(default=options)

try:
_fields[field_name] = fields.Field(
mapper=mappers.build_mapper(anno_type, options=options),
mapper=mappers.build_mapper(annotated_type, options=options),
options=options
)
except TypeError as e:
# noinspection PyTypeChecker
raise DocumentError(
loc=(name, field_name),
msg=f'Invalid field annotation {anno_type}. {str(e)}'
msg=f'Invalid field annotation {annotated_type}. {str(e)}'
) from None

except Exception as e:
# noinspection PyTypeChecker
raise DocumentError(
Expand Down
87 changes: 87 additions & 0 deletions mongotoy/expressions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import re
import typing
from typing import Literal

import pymongo

from mongotoy import geodata

EmptyValue = type('EmptyValue', (), {})()
IndexType = Literal[-1, 1, '2d', '2dsphere', 'hashed', 'text']

Expand Down Expand Up @@ -258,6 +261,90 @@ def Regex(cls, field, value: re.Pattern) -> 'Query':
"""
return cls({str(field): {'$regex': value}})

@classmethod
def Intersects(cls, field, value: geodata.Geometry) -> 'Query':
"""
Creates a geo intersects query expression.
Selects documents whose geospatial data intersects with a specified GeoJSON object;
i.e. where the intersection of the data and the specified object is non-empty.
Args:
field: The field name.
value (geodata.Geometry): The geometry.
Returns:
Query: The geo intersects query expression.
"""
return cls({
str(field): {
'$geoIntersects': {
'$geometry': value.dump_json()
}
}
})

@classmethod
def Within(cls, field, value: geodata.Polygon | geodata.MultiPolygon) -> 'Query':
"""
Creates a geo within query expression.
Selects documents with geospatial data that exists entirely within a specified shape.
Args:
field: The field name.
value (geodata.Polygon | geodata.MultiPolygon): The polygon or multipolygon geometry.
Returns:
Query: The geo within query expression.
"""
return cls({
str(field): {
'$geoWithin': {
'$geometry': value.dump_json()
}
}
})

@classmethod
def Near(
cls,
field,
value: geodata.Point,
max_distance: typing.Optional[int] = None,
min_distance: typing.Optional[int] = None,
as_near_sphere: bool = False
) -> 'Query':
"""
Creates a geo near query expression.
Specifies a point for which a geospatial query returns the documents from nearest to farthest
Args:
field: The field name.
value (geodata.Point): The center point geometry.
max_distance (int, Optional): Max distance (in meters) from the center point.
min_distance (int, Optional): Min distance (in meters) from the center point.
as_near_sphere (bool): Calculates distances using spherical geometry.
Returns:
Query: The geo near query expression.
"""
near_op = '$nearSphere' if as_near_sphere else '$near'
near_exp = {
'$geometry': value.dump_json()
}
if max_distance:
near_exp['$maxDistance'] = max_distance
if min_distance:
near_exp['$minDistance'] = min_distance

return cls({
str(field): {
near_op: near_exp
}
})


# noinspection PyPep8Naming
def Q(**kwargs) -> Query:
Expand Down
21 changes: 14 additions & 7 deletions mongotoy/geodata.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def dump_json(self) -> dict:
Returns:
dict: The GeoJSON representation of the geometry.
"""
return {"type": self.type, "coordinates": self.coordinates}
extra = getattr(self, '__geo_extra__', {})
return {"type": self.type, "coordinates": self.coordinates, **extra}


def parse_geojson(geojson: dict, parser: typing.Type[Geometry]) -> Geometry:
Expand Down Expand Up @@ -101,10 +102,11 @@ class Point(Position, Geometry):
https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.2
"""

def __init__(self, *coordinates: int | float):
def __init__(self, *coordinates: int | float, **kwargs):
if len(coordinates) != 2:
raise TypeError(f'The Point must represent single position, i.e. [lat, long]')
super().__init__(*coordinates)
self.__geo_extra__ = kwargs


class MultiPoint(list[Point], Geometry):
Expand All @@ -115,8 +117,9 @@ class MultiPoint(list[Point], Geometry):
https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.3
"""

def __init__(self, *points: Point):
def __init__(self, *points: Point, **kwargs):
super().__init__([Point(*i) for i in points])
self.__geo_extra__ = kwargs


class LineString(list[Point], Geometry):
Expand All @@ -127,10 +130,11 @@ class LineString(list[Point], Geometry):
https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.4
"""

def __init__(self, *points: Point):
def __init__(self, *points: Point, **kwargs):
if not len(points) >= 2:
raise TypeError('The LineString must be an array of two or more Points')
super().__init__([Point(*i) for i in points])
self.__geo_extra__ = kwargs


class MultiLineString(list[LineString], Geometry):
Expand All @@ -141,17 +145,19 @@ class MultiLineString(list[LineString], Geometry):
https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.5
"""

def __init__(self, *lines: LineString):
def __init__(self, *lines: LineString, **kwargs):
super().__init__([LineString(*i) for i in lines])
self.__geo_extra__ = kwargs


class Polygon(list[LineString], Geometry):
"""
https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6
"""

def __init__(self, *rings: LineString):
def __init__(self, *rings: LineString, **kwargs):
super().__init__([LinearRing(*i) for i in rings])
self.__geo_extra__ = kwargs


class MultiPolygon(list[Polygon], Geometry):
Expand All @@ -162,5 +168,6 @@ class MultiPolygon(list[Polygon], Geometry):
https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.7
"""

def __init__(self, *polygons: Polygon):
def __init__(self, *polygons: Polygon, **kwargs):
super().__init__([Polygon(*i) for i in polygons])
self.__geo_extra__ = kwargs
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mongotoy"
version = "0.1.6"
version = "0.1.7"
description = "Comprehensive ODM for MongoDB"
license = "Apache-2.0"
authors = ["gurcuff91 <[email protected]>"]
Expand Down

0 comments on commit 11799aa

Please sign in to comment.