Skip to content

Commit b2f37a5

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 b2f37a5

File tree

3 files changed

+77
-7
lines changed

3 files changed

+77
-7
lines changed

aredis_om/model/types.py

Lines changed: 73 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,99 @@
88

99

1010
class GeoFilter:
11-
def __init__(self, longitude: float, latitude: float, radius: float, unit: RadiusUnit):
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+
36+
def __init__(
37+
self, longitude: float, latitude: float, radius: float, unit: RadiusUnit
38+
):
39+
# Validate coordinates
40+
if not -180 <= longitude <= 180:
41+
raise ValueError(f"Longitude must be between -180 and 180, got {longitude}")
42+
if not -90 <= latitude <= 90:
43+
raise ValueError(f"Latitude must be between -90 and 90, got {latitude}")
44+
if radius <= 0:
45+
raise ValueError(f"Radius must be positive, got {radius}")
46+
1247
self.longitude = longitude
1348
self.latitude = latitude
1449
self.radius = radius
1550
self.unit = unit
1651

17-
def __str__(self):
52+
def __str__(self) -> str:
1853
return f"{self.longitude} {self.latitude} {self.radius} {self.unit}"
1954

55+
@classmethod
56+
def from_coordinates(
57+
cls, coords: Coordinate, radius: float, unit: RadiusUnit
58+
) -> "GeoFilter":
59+
"""
60+
Create a GeoFilter from a Coordinates object.
61+
62+
Args:
63+
coords: A Coordinate object with latitude and longitude
64+
radius: The search radius
65+
unit: The unit of measurement
66+
67+
Returns:
68+
A new GeoFilter instance
69+
"""
70+
return cls(coords.longitude, coords.latitude, radius, unit)
71+
2072

2173
CoordinateType = Coordinate
2274

2375

24-
def parse_redis(v: Any):
76+
def parse_redis(v: Any) -> Union[Tuple[str, str], Any]:
2577
"""
78+
Transform Redis coordinate format to Pydantic coordinate format.
79+
2680
The pydantic coordinate type expects a string in the format 'latitude,longitude'.
27-
Redis expects a string in the format 'longitude,latitude'.
81+
Redis stores coordinates in the format 'longitude,latitude'.
2882
2983
This validator transforms the input from Redis into the expected format for pydantic.
84+
85+
Args:
86+
v: The value from Redis (typically a string like "longitude,latitude")
87+
88+
Returns:
89+
A tuple of (latitude, longitude) strings if input is a coordinate string,
90+
otherwise returns the input unchanged.
91+
92+
Raises:
93+
ValueError: If the coordinate string format is invalid
3094
"""
3195
if isinstance(v, str):
3296
parts = v.split(",")
3397

3498
if len(parts) != 2:
35-
raise ValueError("Invalid coordinate format")
99+
raise ValueError(
100+
f"Invalid coordinate format. Expected 'longitude,latitude' but got: {v}"
101+
)
36102

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

39105
return v
40106

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)