Skip to content

Commit 82472c4

Browse files
committed
feat(geo): enhance GeoFilter with validation and documentation
- Add comprehensive docstrings to GeoFilter class with usage examples - Add coordinate validation (longitude: -180 to 180, latitude: -90 to 90) - Add radius validation (must be positive) - Add from_coordinates() convenience class method - Improve error messages with actual invalid values - Add return type hints for better type safety - Add clarifying comments in tests about distance calculations These improvements address GitHub Copilot suggestions and enhance the robustness and usability of the geographic filtering feature.
1 parent 67a4098 commit 82472c4

File tree

3 files changed

+71
-7
lines changed

3 files changed

+71
-7
lines changed

aredis_om/model/types.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Annotated, Any, Literal
1+
from typing import Annotated, Any, Literal, Tuple, Union
22

33
from pydantic import BeforeValidator, PlainSerializer
44
from pydantic_extra_types.coordinate import Coordinate
@@ -8,33 +8,93 @@
88

99

1010
class GeoFilter:
11+
"""
12+
A geographic filter for searching within a radius of a coordinate point.
13+
14+
This filter is used with GEO fields to find models within a specified
15+
distance from a given location.
16+
17+
Args:
18+
longitude: The longitude of the center point (-180 to 180)
19+
latitude: The latitude of the center point (-90 to 90)
20+
radius: The search radius (must be positive)
21+
unit: The unit of measurement ('m', 'km', 'mi', or 'ft')
22+
23+
Example:
24+
>>> # Find all locations within 10 miles of Portland, OR
25+
>>> filter = GeoFilter(
26+
... longitude=-122.6765,
27+
... latitude=45.5231,
28+
... radius=10,
29+
... unit="mi"
30+
... )
31+
>>> results = await Location.find(
32+
... Location.coordinates == filter
33+
... ).all()
34+
"""
35+
1136
def __init__(self, longitude: float, latitude: float, radius: float, unit: RadiusUnit):
37+
# Validate coordinates
38+
if not -180 <= longitude <= 180:
39+
raise ValueError(f"Longitude must be between -180 and 180, got {longitude}")
40+
if not -90 <= latitude <= 90:
41+
raise ValueError(f"Latitude must be between -90 and 90, got {latitude}")
42+
if radius <= 0:
43+
raise ValueError(f"Radius must be positive, got {radius}")
44+
1245
self.longitude = longitude
1346
self.latitude = latitude
1447
self.radius = radius
1548
self.unit = unit
1649

17-
def __str__(self):
50+
def __str__(self) -> str:
1851
return f"{self.longitude} {self.latitude} {self.radius} {self.unit}"
52+
53+
@classmethod
54+
def from_coordinates(cls, coords: Coordinate, radius: float, unit: RadiusUnit) -> "GeoFilter":
55+
"""
56+
Create a GeoFilter from a Coordinates object.
57+
58+
Args:
59+
coords: A Coordinate object with latitude and longitude
60+
radius: The search radius
61+
unit: The unit of measurement
62+
63+
Returns:
64+
A new GeoFilter instance
65+
"""
66+
return cls(coords.longitude, coords.latitude, radius, unit)
1967

2068

2169
CoordinateType = Coordinate
2270

2371

24-
def parse_redis(v: Any):
72+
def parse_redis(v: Any) -> Union[Tuple[str, str], Any]:
2573
"""
74+
Transform Redis coordinate format to Pydantic coordinate format.
75+
2676
The pydantic coordinate type expects a string in the format 'latitude,longitude'.
27-
Redis expects a string in the format 'longitude,latitude'.
28-
77+
Redis stores coordinates in the format 'longitude,latitude'.
78+
2979
This validator transforms the input from Redis into the expected format for pydantic.
80+
81+
Args:
82+
v: The value from Redis (typically a string like "longitude,latitude")
83+
84+
Returns:
85+
A tuple of (latitude, longitude) strings if input is a coordinate string,
86+
otherwise returns the input unchanged.
87+
88+
Raises:
89+
ValueError: If the coordinate string format is invalid
3090
"""
3191
if isinstance(v, str):
3292
parts = v.split(",")
3393

3494
if len(parts) != 2:
35-
raise ValueError("Invalid coordinate format")
95+
raise ValueError(f"Invalid coordinate format. Expected 'longitude,latitude' but got: {v}")
3696

37-
return (parts[1], parts[0])
97+
return (parts[1], parts[0]) # Swap to (latitude, longitude)
3898

3999
return v
40100

tests/test_hash_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,8 @@ class Meta:
11491149
longitude = -122.6765
11501150

11511151
loc1 = Location(coordinates=(latitude, longitude), name="Portland")
1152+
# Offset by 0.01 degrees (~1.1 km at this latitude) to create a nearby location
1153+
# This ensures "Nearby" is within the 10 mile search radius but not at the exact same location
11521154
loc2 = Location(coordinates=(latitude + 0.01, longitude + 0.01), name="Nearby")
11531155

11541156
await loc1.save()

tests/test_json_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,8 @@ class Meta:
14591459
longitude = -122.6765
14601460

14611461
loc1 = Location(coordinates=(latitude, longitude), name="Portland")
1462+
# Offset by 0.01 degrees (~1.1 km at this latitude) to create a nearby location
1463+
# This ensures "Nearby" is within the 10 mile search radius but not at the exact same location
14621464
loc2 = Location(coordinates=(latitude + 0.01, longitude + 0.01), name="Nearby")
14631465

14641466
await loc1.save()

0 commit comments

Comments
 (0)