Skip to content

Commit 0f8d0dc

Browse files
GEOSEARCH and GEOSEARCHSTORE (#1526)
* GEOSEARCH and GEOSEARCHSTORE * negative test * change georadius_generic to geosearch_generic * add documentations to the functions * add docstring to the parser * farest
1 parent e53227c commit 0f8d0dc

File tree

3 files changed

+370
-86
lines changed

3 files changed

+370
-86
lines changed

redis/client.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -472,18 +472,23 @@ def parse_cluster_nodes(response, **options):
472472
return dict(_parse_node_line(line) for line in raw_lines)
473473

474474

475-
def parse_georadius_generic(response, **options):
475+
def parse_geosearch_generic(response, **options):
476+
"""
477+
Parse the response of 'GEOSEARCH', GEORADIUS' and 'GEORADIUSBYMEMBER'
478+
commands according to 'withdist', 'withhash' and 'withcoord' labels.
479+
"""
476480
if options['store'] or options['store_dist']:
477-
# `store` and `store_diff` cant be combined
481+
# `store` and `store_dist` cant be combined
478482
# with other command arguments.
483+
# relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER'
479484
return response
480485

481486
if type(response) != list:
482487
response_list = [response]
483488
else:
484489
response_list = response
485490

486-
if not options['withdist'] and not options['withcoord']\
491+
if not options['withdist'] and not options['withcoord'] \
487492
and not options['withhash']:
488493
# just a bunch of places
489494
return response_list
@@ -695,8 +700,9 @@ class Redis(Commands, object):
695700
'GEOPOS': lambda r: list(map(lambda ll: (float(ll[0]),
696701
float(ll[1]))
697702
if ll is not None else None, r)),
698-
'GEORADIUS': parse_georadius_generic,
699-
'GEORADIUSBYMEMBER': parse_georadius_generic,
703+
'GEOSEARCH': parse_geosearch_generic,
704+
'GEORADIUS': parse_geosearch_generic,
705+
'GEORADIUSBYMEMBER': parse_geosearch_generic,
700706
'HGETALL': lambda r: r and pairs_to_dict(r) or {},
701707
'HSCAN': parse_hscan,
702708
'INFO': parse_info,

redis/commands.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2924,6 +2924,136 @@ def _georadiusgeneric(self, command, *args, **kwargs):
29242924

29252925
return self.execute_command(command, *pieces, **kwargs)
29262926

2927+
def geosearch(self, name, member=None, longitude=None, latitude=None,
2928+
unit='m', radius=None, width=None, height=None, sort=None,
2929+
count=None, any=False, withcoord=False,
2930+
withdist=False, withhash=False):
2931+
"""
2932+
Return the members of specified key identified by the
2933+
``name`` argument, which are within the borders of the
2934+
area specified by a given shape. This command extends the
2935+
GEORADIUS command, so in addition to searching within circular
2936+
areas, it supports searching within rectangular areas.
2937+
This command should be used in place of the deprecated
2938+
GEORADIUS and GEORADIUSBYMEMBER commands.
2939+
``member`` Use the position of the given existing
2940+
member in the sorted set. Can't be given with ``longitude``
2941+
and ``latitude``.
2942+
``longitude`` and ``latitude`` Use the position given by
2943+
this coordinates. Can't be given with ``member``
2944+
``radius`` Similar to GEORADIUS, search inside circular
2945+
area according the given radius. Can't be given with
2946+
``height`` and ``width``.
2947+
``height`` and ``width`` Search inside an axis-aligned
2948+
rectangle, determined by the given height and width.
2949+
Can't be given with ``radius``
2950+
``unit`` must be one of the following : m, km, mi, ft.
2951+
`m` for meters (the default value), `km` for kilometers,
2952+
`mi` for miles and `ft` for feet.
2953+
``sort`` indicates to return the places in a sorted way,
2954+
ASC for nearest to farest and DESC for farest to nearest.
2955+
``count`` limit the results to the first count matching items.
2956+
``any`` is set to True, the command will return as soon as
2957+
enough matches are found. Can't be provided without ``count``
2958+
``withdist`` indicates to return the distances of each place.
2959+
``withcoord`` indicates to return the latitude and longitude of
2960+
each place.
2961+
``withhash`` indicates to return the geohash string of each place.
2962+
"""
2963+
2964+
return self._geosearchgeneric('GEOSEARCH',
2965+
name, member=member, longitude=longitude,
2966+
latitude=latitude, unit=unit,
2967+
radius=radius, width=width,
2968+
height=height, sort=sort, count=count,
2969+
any=any, withcoord=withcoord,
2970+
withdist=withdist, withhash=withhash,
2971+
store=None, store_dist=None)
2972+
2973+
def geosearchstore(self, dest, name, member=None, longitude=None,
2974+
latitude=None, unit='m', radius=None, width=None,
2975+
height=None, sort=None, count=None, any=False,
2976+
storedist=False):
2977+
"""
2978+
This command is like GEOSEARCH, but stores the result in
2979+
``dest``. By default, it stores the results in the destination
2980+
sorted set with their geospatial information.
2981+
if ``store_dist`` set to True, the command will stores the
2982+
items in a sorted set populated with their distance from the
2983+
center of the circle or box, as a floating-point number.
2984+
"""
2985+
return self._geosearchgeneric('GEOSEARCHSTORE',
2986+
dest, name, member=member,
2987+
longitude=longitude, latitude=latitude,
2988+
unit=unit, radius=radius, width=width,
2989+
height=height, sort=sort, count=count,
2990+
any=any, withcoord=None,
2991+
withdist=None, withhash=None,
2992+
store=None, store_dist=storedist)
2993+
2994+
def _geosearchgeneric(self, command, *args, **kwargs):
2995+
pieces = list(args)
2996+
2997+
# FROMMEMBER or FROMLONLAT
2998+
if kwargs['member'] is None:
2999+
if kwargs['longitude'] is None or kwargs['latitude'] is None:
3000+
raise DataError("GEOSEARCH must have member or"
3001+
" longitude and latitude")
3002+
if kwargs['member']:
3003+
if kwargs['longitude'] or kwargs['latitude']:
3004+
raise DataError("GEOSEARCH member and longitude or latitude"
3005+
" cant be set together")
3006+
pieces.extend([b'FROMMEMBER', kwargs['member']])
3007+
if kwargs['longitude'] and kwargs['latitude']:
3008+
pieces.extend([b'FROMLONLAT',
3009+
kwargs['longitude'], kwargs['latitude']])
3010+
3011+
# BYRADIUS or BYBOX
3012+
if kwargs['radius'] is None:
3013+
if kwargs['width'] is None or kwargs['height'] is None:
3014+
raise DataError("GEOSEARCH must have radius or"
3015+
" width and height")
3016+
if kwargs['unit'] is None:
3017+
raise DataError("GEOSEARCH must have unit")
3018+
if kwargs['unit'].lower() not in ('m', 'km', 'mi', 'ft'):
3019+
raise DataError("GEOSEARCH invalid unit")
3020+
if kwargs['radius']:
3021+
if kwargs['width'] or kwargs['height']:
3022+
raise DataError("GEOSEARCH radius and width or height"
3023+
" cant be set together")
3024+
pieces.extend([b'BYRADIUS', kwargs['radius'], kwargs['unit']])
3025+
if kwargs['width'] and kwargs['height']:
3026+
pieces.extend([b'BYBOX',
3027+
kwargs['width'], kwargs['height'], kwargs['unit']])
3028+
3029+
# sort
3030+
if kwargs['sort']:
3031+
if kwargs['sort'].upper() == 'ASC':
3032+
pieces.append(b'ASC')
3033+
elif kwargs['sort'].upper() == 'DESC':
3034+
pieces.append(b'DESC')
3035+
else:
3036+
raise DataError("GEOSEARCH invalid sort")
3037+
3038+
# count any
3039+
if kwargs['count']:
3040+
pieces.extend([b'COUNT', kwargs['count']])
3041+
if kwargs['any']:
3042+
pieces.append(b'ANY')
3043+
elif kwargs['any']:
3044+
raise DataError("GEOSEARCH any can't be provided without count")
3045+
3046+
# other properties
3047+
for arg_name, byte_repr in (
3048+
('withdist', b'WITHDIST'),
3049+
('withcoord', b'WITHCOORD'),
3050+
('withhash', b'WITHHASH'),
3051+
('store_dist', b'STOREDIST')):
3052+
if kwargs[arg_name]:
3053+
pieces.append(byte_repr)
3054+
3055+
return self.execute_command(command, *pieces, **kwargs)
3056+
29273057
# MODULE COMMANDS
29283058
def module_load(self, path):
29293059
"""

0 commit comments

Comments
 (0)