Skip to content

Commit

Permalink
Merge pull request #430 from laixintao/update-commands
Browse files Browse the repository at this point in the history
GEO and GET related commands, timestamp completer is more powerful!
  • Loading branch information
laixintao authored Jun 25, 2022
2 parents ac637de + c4931b9 commit 0572803
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
- Feature: support new command: `EVAL_RO` and `EVALSHA_RO`.
- Feature: support new command: `EXPIRETIME`.
- Feature: support new command: `FAILOVER`.
- Feature: support new command: `GEOSEARCH`.
- Feature: support new command: `GEOSEARCHRESTORE`.
- Feature: support new command: `GETDEL`.
- Feature: support new command: `GETEX`.
- Feature: `FLUSHDB` and `FLUSHALL` supports `SYNC` option.
- Feature: `GEOADD` supports `CH XX NX` options.
- Feature: Timestamp Completers are now support completion for timestamp fields and milliseconds timestamp fields.
- Deprecate: `GEORADIUS` is deprecated, no auto-complete for this command anymore.
- Deprecate: `GEORADIUSBYMEMBER` is deprecated, no auto-complete for this command anymore.

Expand Down
39 changes: 33 additions & 6 deletions iredis/completers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ class TimestampCompleter(Completer):
The timezone is read from system.
"""

def __init__(self, is_milliseconds, future_time, *args, **kwargs):
if is_milliseconds:
self.factor = 1000
else:
self.factor = 1

self.future_time = future_time
super().__init__(*args, **kwargs)

when_lower_than = {
"year": 20,
"month": 12,
Expand All @@ -96,10 +105,17 @@ def _completion_humanize_time(self, document: Document) -> Iterable[Completion]:
now = pendulum.now()
for unit, minimum in self.when_lower_than.items():
if current <= minimum:
dt = now.subtract(**{f"{unit}s": current})
meta = f"{text} {unit}{'s' if current > 1 else ''} ago ({dt.format('YYYY-MM-DD HH:mm:ss')})"

if self.future_time:
dt = now.add(**{f"{unit}s": current})
offset_text = "later"
else:
dt = now.subtract(**{f"{unit}s": current})
offset_text = "ago"

meta = f"{text} {unit}{'s' if current > 1 else ''} {offset_text} ({dt.format('YYYY-MM-DD HH:mm:ss')})"
yield Completion(
str(dt.int_timestamp * 1000),
str(dt.int_timestamp * self.factor),
start_position=-len(document.text_before_cursor),
display_meta=meta,
)
Expand All @@ -111,7 +127,7 @@ def _completion_formatted_time(self, document: Document) -> Iterable[Completion]
except Exception:
return
yield Completion(
str(dt.int_timestamp * 1000),
str(dt.int_timestamp * self.factor),
start_position=-len(document.text_before_cursor),
display_meta=str(dt),
)
Expand Down Expand Up @@ -296,7 +312,16 @@ def get_completer_mapping(self, hint_on, completion_casing):
config.completer_max, []
)
categoryname_completer = MostRecentlyUsedFirstWordCompleter(100, [])
timestamp_completer = TimestampCompleter()

timestamp_ms_ago_completer = TimestampCompleter(
is_milliseconds=True, future_time=False
)
timestamp_ms_after_completer = TimestampCompleter(
is_milliseconds=True, future_time=True
)
timestamp_after_completer = TimestampCompleter(
is_milliseconds=False, future_time=True
)
integer_type_completer = IntegerTypeCompleter()

completer_mapping.update(
Expand All @@ -317,7 +342,9 @@ def get_completer_mapping(self, hint_on, completion_casing):
# stream groups
"group": group_completer,
# stream id
"stream_id": timestamp_completer,
"stream_id": timestamp_ms_ago_completer,
"timestampms": timestamp_ms_after_completer,
"timestamp": timestamp_after_completer,
"inttype": integer_type_completer,
"categoryname": categoryname_completer,
"username": username_completer,
Expand Down
4 changes: 4 additions & 0 deletions iredis/data/command_syntax.csv
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ geo,GEOHASH,command_key_members,render_list
geo,GEOPOS,command_key_members,render_list
geo,GEORADIUS,command_any,render_list_or_string
geo,GEORADIUSBYMEMBER,command_any,render_list_or_string
geo,GEOSEARCH,command_key_any,render_list
geo,GEOSEARCHSTORE,command_key_key_any,render_list
hash,HDEL,command_key_fields,render_int
hash,HEXISTS,command_key_field,render_int
hash,HGET,command_key_field,render_bulk_string
Expand Down Expand Up @@ -245,7 +247,9 @@ bitmap,BITPOS,command_key_bit_start_end,render_int
string,DECR,command_key,render_int
string,DECRBY,command_key_delta,render_int
string,GET,command_key,render_bulk_string
string,GETEX,command_key_expire,render_bulk_string
string,GETBIT,command_key_offset,render_int
string,GETDEL,command_key,render_bulk_string
string,GETRANGE,command_key_start_end,render_bulk_string
string,GETSET,command_key_value,render_bulk_string
string,INCR,command_key,render_int
Expand Down
19 changes: 16 additions & 3 deletions iredis/redis_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"withscores": "WITHSCORES",
"limit": "LIMIT",
"expiration": "EX PX",
"exat_const": "EXAT",
"pxat_const": "PXAT",
"condition": "NX XX",
"keepttl": "KEEPTTL",
"operation": "AND OR XOR NOT",
Expand Down Expand Up @@ -218,12 +220,12 @@ def c(const_name):
CLIENTIDS = rf"(?P<clientids>{NUM}(\s+{NUM})*)"

SECOND = rf"(?P<second>{NUM})"
TIMESTAMP = rf"(?P<timestamp>{NUM})"
TIMESTAMP = r"(?P<timestamp>[T\d:>+*\-\$]+)"
# TODO test lexer & completer for multi spaces in command
# For now, redis command can have one space at most
COMMAND = r"(\s* (?P<command>[\w -]+))"
MILLISECOND = rf"(?P<millisecond>{NUM})"
TIMESTAMPMS = rf"(?P<timestampms>{NUM})"
TIMESTAMPMS = r"(?P<timestampms>[T\d:>+*\-\$]+)"
ANY = r"(?P<any>.*)" # TODO deleted
START = rf"(?P<start>{NNUM})"
END = rf"(?P<end>{NNUM})"
Expand Down Expand Up @@ -358,6 +360,8 @@ def c(const_name):
TO_CONST = rf"(?P<to_const>{c('to_const')})"
TIMEOUT_CONST = rf"(?P<timeout_const>{c('timeout_const')})"
ABORT_CONST = rf"(?P<abort_const>{c('abort_const')})"
PXAT_CONST = rf"(?P<pxat_const>{c('pxat_const')})"
EXAT_CONST = rf"(?P<exat_const>{c('exat_const')})"

command_grammar = compile(COMMAND)

Expand Down Expand Up @@ -454,6 +458,8 @@ def c(const_name):
"command_destination_keys": rf"\s+ {DESTINATION} \s+ {KEYS} \s*",
"command_object_key": rf"\s+ {OBJECT} \s+ {KEY} \s*",
"command_key_member": rf"\s+ {KEY} \s+ {MEMBER} \s*",
"command_key_any": rf"\s+ {KEY} \s+ {ANY} \s*",
"command_key_key_any": rf"\s+ {KEY} \s+ {KEY} \s+ {ANY} \s*",
"command_key_newkey_member": rf"\s+ {KEY} \s+ {NEWKEY} \s+ {MEMBER} \s*",
"command_key_count_x": rf"\s+ {KEY} (\s+ {COUNT})? \s*",
"command_key_min_max": rf"\s+ {KEY} \s+ {MIN} \s+ {MAX} \s*",
Expand Down Expand Up @@ -645,7 +651,14 @@ def c(const_name):
(\s+ {TO_CONST} \s+ {HOST} \s+ {PORT} (\s+ {FORCE})? )?
(\s+ {ABORT_CONST})?
(\s+ {TIMEOUT_CONST} \s+ {MILLISECOND})?
\s*""",
"command_key_expire": rf"""
\s+ {KEY}
(
(\s+ {EXPIRATION} \s+ {MILLISECOND})|
(\s+ {PXAT_CONST} \s+ {TIMESTAMPMS})|
(\s+ {EXAT_CONST} \s+ {TIMESTAMP})
)?
\s*""",
}

Expand Down
21 changes: 21 additions & 0 deletions tests/unittests/command_parse/test_generic_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,24 @@ def test_copy(judge_command):
{"command": "COPY", "key": ["foo", "bar"], "replace_const": "REPLACE"},
)
judge_command("COPY foo bar", {"command": "COPY", "key": ["foo", "bar"]})


def test_getex(judge_command):
judge_command("GETEX foo", {"command": "GETEX", "key": "foo"})
judge_command(
"GETEX bar ex 5",
{"command": "GETEX", "key": "bar", "expiration": "ex", "millisecond": "5"},
)
judge_command(
"GETEX bar px 5",
{"command": "GETEX", "key": "bar", "expiration": "px", "millisecond": "5"},
)
judge_command(
"GETEX bar pxat 5",
{"command": "GETEX", "key": "bar", "pxat_const": "pxat", "timestampms": "5"},
)
judge_command(
"GETEX bar exat 5",
{"command": "GETEX", "key": "bar", "exat_const": "exat", "timestamp": "5"},
)
judge_command("GETEX bar ex 5 exat 5", None)
22 changes: 22 additions & 0 deletions tests/unittests/command_parse/test_geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,25 @@ def test_geoadd(judge_command):
"member": '"Catania"',
},
)


def test_geosearch(judge_command):
judge_command(
"GEOSEARCH Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST",
{
"command": "GEOSEARCH",
"key": "Sicily",
"any": "FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST",
},
)


def test_geosearchstore(judge_command):
judge_command(
"GEOSEARCHSTORE key2 Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC COUNT 3 STOREDIST",
{
"command": "GEOSEARCHSTORE",
"key": ["Sicily", "key2"],
"any": "FROMLONLAT 15 37 BYBOX 400 400 km ASC COUNT 3 STOREDIST",
},
)
93 changes: 88 additions & 5 deletions tests/unittests/test_completers.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def test_group_completer():
@patch("iredis.completers.pendulum.now")
def test_timestamp_completer_humanize_time_completion(fake_now):
fake_now.return_value = pendulum.from_timestamp(1578487013)
c = TimestampCompleter()
c = TimestampCompleter(is_milliseconds=True, future_time=False)

fake_document = MagicMock()
fake_document.text = fake_document.text_before_cursor = "30"
Expand Down Expand Up @@ -260,8 +260,87 @@ def test_timestamp_completer_humanize_time_completion(fake_now):
]


@patch("iredis.completers.pendulum.now")
def test_timestamp_completer_humanize_time_completion_seconds(fake_now):
fake_now.return_value = pendulum.from_timestamp(1578487013)
c = TimestampCompleter(is_milliseconds=False, future_time=False)

fake_document = MagicMock()
fake_document.text = fake_document.text_before_cursor = "30"
completions = list(c.get_completions(fake_document, None))

assert completions == [
Completion(
text="1575895013",
start_position=-2,
display=FormattedText([("", "1575895013")]),
display_meta="30 days ago (2019-12-09 12:36:53)",
),
Completion(
text="1578379013",
start_position=-2,
display=FormattedText([("", "1578379013")]),
display_meta="30 hours ago (2020-01-07 06:36:53)",
),
Completion(
text="1578485213",
start_position=-2,
display=FormattedText([("", "1578485213")]),
display_meta="30 minutes ago (2020-01-08 12:06:53)",
),
Completion(
text="1578486983",
start_position=-2,
display=FormattedText([("", "1578486983")]),
display_meta="30 seconds ago (2020-01-08 12:36:23)",
),
]


@patch("iredis.completers.pendulum.now")
def test_timestamp_completer_humanize_time_completion_seconds_future_time(fake_now):
fake_now.return_value = pendulum.from_timestamp(1578487013)
c = TimestampCompleter(is_milliseconds=False, future_time=True)

fake_document = MagicMock()
fake_document.text = fake_document.text_before_cursor = "30"
completions = list(c.get_completions(fake_document, None))

print(completions)
for c in completions:
print(c.text)
print(c.display)
print(c.display_meta)
assert completions == [
Completion(
text="1578487043",
start_position=-2,
display=FormattedText([("", "1578487043")]),
display_meta="30 seconds later (2020-01-08 12:37:23)",
),
Completion(
text="1578488813",
start_position=-2,
display=FormattedText([("", "1578488813")]),
display_meta="30 minutes later (2020-01-08 13:06:53)",
),
Completion(
text="1578595013",
start_position=-2,
display=FormattedText([("", "1578595013")]),
display_meta="30 hours later (2020-01-09 18:36:53)",
),
Completion(
text="1581079013",
start_position=-2,
display=FormattedText([("", "1581079013")]),
display_meta="30 days later (2020-02-07 12:36:53)",
),
]


def test_timestamp_completer_datetime_format_time_completion():
c = TimestampCompleter()
c = TimestampCompleter(is_milliseconds=True, future_time=False)
fake_document = MagicMock()
fake_document.text = fake_document.text_before_cursor = "2020-02-07"
completions = list(c.get_completions(fake_document, None))
Expand Down Expand Up @@ -299,34 +378,38 @@ def test_completion_casing():
completion.text for completion in c.get_completions(fake_document, None)
] == [
"get",
"getex",
"getset",
"getdel",
"getbit",
"geopos",
"geoadd",
"geohash",
"geodist",
"getrange",
"geosearch",
"georadius",
"geosearchstore",
"georadiusbymember",
]

c = IRedisCompleter(completion_casing="auto")
fake_document.text = fake_document.text_before_cursor = "GET"
assert [
completion.text for completion in c.get_completions(fake_document, None)
] == ["GET", "GETSET", "GETBIT", "GETRANGE"]
] == ["GET", "GETEX", "GETSET", "GETDEL", "GETBIT", "GETRANGE"]

c = IRedisCompleter(completion_casing="upper")
fake_document.text = fake_document.text_before_cursor = "get"
assert [
completion.text for completion in c.get_completions(fake_document, None)
] == ["GET", "GETSET", "GETBIT", "GETRANGE"]
] == ["GET", "GETEX", "GETSET", "GETDEL", "GETBIT", "GETRANGE"]

c = IRedisCompleter(completion_casing="lower")
fake_document.text = fake_document.text_before_cursor = "GET"
assert [
completion.text for completion in c.get_completions(fake_document, None)
] == ["get", "getset", "getbit", "getrange"]
] == ["get", "getex", "getset", "getdel", "getbit", "getrange"]


def test_username_completer():
Expand Down

0 comments on commit 0572803

Please sign in to comment.