Skip to content

Commit

Permalink
Implement COMMAND, COMMAND INFO, COMMAND COUNT (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
cunla authored Oct 21, 2023
1 parent 7f09f4c commit 0153a84
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 125 deletions.
3 changes: 2 additions & 1 deletion docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ description: Change log of all fakeredis releases

### 🚀 Features

- Implement BITFIELD command #247
- Implement `BITFIELD` command #247
- Implement `COMMAND`, `COMMAND INFO`, `COMMAND COUNT` #248

## v2.19.0

Expand Down
30 changes: 15 additions & 15 deletions docs/redis-commands/Redis.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
# Redis commands

## `server` commands (8/70 implemented)
## `server` commands (11/70 implemented)

### [BGSAVE](https://redis.io/commands/bgsave/)

Asynchronously saves the database(s) to disk.

### [COMMAND](https://redis.io/commands/command/)

Returns detailed information about all commands.

### [COMMAND COUNT](https://redis.io/commands/command-count/)

Returns a count of commands.

### [COMMAND INFO](https://redis.io/commands/command-info/)

Returns information about one, multiple or all commands.

### [DBSIZE](https://redis.io/commands/dbsize/)

Returns the number of keys in the database.
Expand Down Expand Up @@ -94,14 +106,6 @@ Returns the authenticated username of the current connection.

Asynchronously rewrites the append-only file to disk.

#### [COMMAND](https://redis.io/commands/command/) <small>(not implemented)</small>

Returns detailed information about all commands.

#### [COMMAND COUNT](https://redis.io/commands/command-count/) <small>(not implemented)</small>

Returns a count of commands.

#### [COMMAND DOCS](https://redis.io/commands/command-docs/) <small>(not implemented)</small>

Returns documentary information about one, multiple or all commands.
Expand All @@ -114,10 +118,6 @@ Extracts the key names from an arbitrary command.

Extracts the key names and access flags for an arbitrary command.

#### [COMMAND INFO](https://redis.io/commands/command-info/) <small>(not implemented)</small>

Returns information about one, multiple or all commands.

#### [COMMAND LIST](https://redis.io/commands/command-list/) <small>(not implemented)</small>

Returns a list of command names.
Expand Down Expand Up @@ -625,7 +625,7 @@ Resets the connection.

Counts the number of set bits (population counting) in a string.

#### [BITFIELD](https://redis.io/commands/bitfield/)
### [BITFIELD](https://redis.io/commands/bitfield/)

Performs arbitrary bitfield integer operations on strings.

Expand All @@ -647,7 +647,7 @@ Sets or clears the bit at offset of the string value. Creates the key if it does


### Unsupported bitmap commands
> To implement support for a command, see [here](../../guides/implement-command/)
> To implement support for a command, see [here](../../guides/implement-command/)
#### [BITFIELD_RO](https://redis.io/commands/bitfield_ro/) <small>(not implemented)</small>

Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
mkdocs==1.5.3
mkdocs-material==9.4.4
mkdocs-material==9.4.6
1 change: 1 addition & 0 deletions fakeredis/commands.json

Large diffs are not rendered by default.

48 changes: 46 additions & 2 deletions fakeredis/commands_mixins/server_mixin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import json
import os
import time
from typing import Any
from typing import Any, List, Optional, Dict

from fakeredis import _msgs as msgs
from fakeredis._commands import command, DbIndex
from fakeredis._commands import command, DbIndex, SUPPORTED_COMMANDS
from fakeredis._helpers import OK, SimpleError, casematch, BGSAVE_STARTED, Database

_COMMAND_INFO: Dict[bytes, List[Any]] = None


def convert_obj(obj: Any) -> Any:
if isinstance(obj, str):
return obj.encode()
if isinstance(obj, list):
return [convert_obj(x) for x in obj]
if isinstance(obj, dict):
return {convert_obj(k): convert_obj(obj[k]) for k in obj}
return obj


def _load_command_info() -> None:
global _COMMAND_INFO
if _COMMAND_INFO is None:
with open(os.path.join(os.path.dirname(__file__), '..', 'commands.json')) as f:
_COMMAND_INFO = convert_obj(json.load(f))


class ServerCommandsMixin:
_server: Any
_db: Database

@staticmethod
def _get_command_info(cmd: str) -> Optional[List[Any]]:
_load_command_info()
if cmd not in SUPPORTED_COMMANDS or cmd.encode() not in _COMMAND_INFO:
return None
return _COMMAND_INFO[cmd.encode()]

@command((), (bytes,), flags=msgs.FLAG_NO_SCRIPT)
def bgsave(self, *args):
if len(args) > 1 or (len(args) == 1 and not casematch(args[0], b"schedule")):
Expand Down Expand Up @@ -60,3 +88,19 @@ def swapdb(self, index1, index2):
db2 = self._server.dbs[index2]
db1.swap(db2)
return OK

@command(name="COMMAND INFO", fixed=(), repeat=(bytes,))
def command_info(self, *commands):
res = [self._get_command_info(cmd) for cmd in commands]
return res

@command(name="COMMAND COUNT", fixed=(), repeat=())
def command_count(self):
_load_command_info()
return len(_COMMAND_INFO)

@command(name="COMMAND", fixed=(), repeat=())
def command(self):
_load_command_info()
res = [self._get_command_info(cmd.decode()) for cmd in _COMMAND_INFO]
return res
20 changes: 18 additions & 2 deletions scripts/create_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
- Set environment variable `GITHUB_TOKEN` to a github token with permissions to create issues.
- Another option is to create `.env` file with `GITHUB_TOKEN`.
"""
import json
import os

import requests
from dotenv import load_dotenv
from github import Github

from supported import download_redis_commands, implemented_commands

load_dotenv() # take environment variables from .env.

THIS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))

IGNORE_GROUPS = {
'suggestion', 'tdigest', 'scripting', 'cf', 'topk',
'hyperloglog', 'graph', 'timeseries', 'connection',
Expand Down Expand Up @@ -48,8 +50,22 @@ def commands_groups(
return implemented, unimplemented


def download_redis_commands() -> dict:
from supported2 import METADATA
cmds = {}
for filename, url in METADATA:
full_filename = os.path.join(THIS_DIR, filename)
if not os.path.exists(full_filename):
contents = requests.get(url).content
open(full_filename, 'wb').write(contents)
curr_cmds = json.load(open(full_filename))
cmds = cmds | {k.lower(): v for k, v in curr_cmds.items()}
return cmds


def get_unimplemented_and_implemented_commands() -> tuple[dict[str, list[str]], dict[str, list[str]]]:
"""Returns 2 dictionaries, one of unimplemented commands and another of implemented commands"""
from supported2 import implemented_commands
commands = download_redis_commands()
implemented_commands_set = implemented_commands()
implemented_dict, unimplemented_dict = commands_groups(commands, implemented_commands_set)
Expand Down
105 changes: 105 additions & 0 deletions scripts/generate_command_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import json
import os
from collections import namedtuple
from typing import Any, List, Dict

import requests

from fakeredis._commands import SUPPORTED_COMMANDS

THIS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
CommandsMeta = namedtuple('CommandsMeta', ['local_filename', 'stack', 'title', 'url', ])
METADATA = [
CommandsMeta('.commands.json', 'Redis', 'Redis',
'https://raw.githubusercontent.com/redis/redis-doc/master/commands.json', ),
CommandsMeta('.json.commands.json', 'RedisJson', 'JSON',
'https://raw.githubusercontent.com/RedisJSON/RedisJSON/master/commands.json', ),
CommandsMeta('.graph.commands.json', 'RedisGraph', 'Graph',
'https://raw.githubusercontent.com/RedisGraph/RedisGraph/master/commands.json', ),
CommandsMeta('.ts.commands.json', 'RedisTimeSeries', 'Time Series',
'https://raw.githubusercontent.com/RedisTimeSeries/RedisTimeSeries/master/commands.json', ),
CommandsMeta('.ft.commands.json', 'RedisSearch', 'Search',
'https://raw.githubusercontent.com/RediSearch/RediSearch/master/commands.json', ),
CommandsMeta('.bloom.commands.json', 'RedisBloom', 'Probabilistic',
'https://raw.githubusercontent.com/RedisBloom/RedisBloom/master/commands.json', ),
]


def download_single_stack_commands(filename, url) -> dict:
full_filename = os.path.join(THIS_DIR, filename)
if not os.path.exists(full_filename):
contents = requests.get(url).content
open(full_filename, 'wb').write(contents)
curr_cmds = json.load(open(full_filename))
cmds = {k.lower(): v for k, v in curr_cmds.items()}
return cmds


def implemented_commands() -> set:
res = set(SUPPORTED_COMMANDS.keys())
if 'json.type' not in res:
raise ValueError('Make sure jsonpath_ng is installed to get accurate documentation')
return res


def dict_deep_get(d: Dict[Any, Any], *keys, default_value: Any = None) -> Any:
res = d
for key in keys:
if isinstance(res, list) and isinstance(key, int):
res = res[key]
else:
res = res.get(key, None)
if res is None:
return default_value
return default_value if res is None else res


def key_specs_array(cmd_info: Dict[str, Any]) -> List[Any]:
return []


def get_command_info(cmd_name: str, cmd_info: Dict[str, Any]) -> List[Any]:
"""Returns a list
1 Name //
2 Arity //
3 Flags //
4 First key //
5 Last key //
6 Step //
7 ACL categories (as of Redis 6.0) //
8 Tips (as of Redis 7.0) //
9 Key specifications (as of Redis 7.0)
10 Subcommands (as of Redis 7.0)
"""
first_key = dict_deep_get(cmd_info, 'key_specs', 0, 'begin_search', 'spec', 'index', default_value=0)
last_key = dict_deep_get(cmd_info, 'key_specs', -1, 'begin_search', 'spec', 'index', default_value=0)
step = dict_deep_get(cmd_info, 'key_specs', 0, 'find_keys', 'spec', 'keystep', default_value=0)
tips = [] # todo
subcommands = [] # todo
res = [
cmd_name.lower(),
cmd_info.get("arity", -1),
cmd_info.get("command_flags", []),
first_key,
last_key,
step,
cmd_info.get("acl_categories", []),
tips,
key_specs_array(cmd_info),
subcommands,
]
return res


if __name__ == '__main__':
implemented = implemented_commands()
command_info_dict: Dict[str, List[Any]] = dict()
for cmd_meta in METADATA:
cmds = download_single_stack_commands(cmd_meta.local_filename, cmd_meta.url)
for cmd in cmds:
if cmd not in implemented:
continue
command_info_dict[cmd] = get_command_info(cmd, cmds[cmd])
print(command_info_dict[cmd])
with open(os.path.join(os.path.dirname(__file__), '..', 'fakeredis', 'commands.json'), 'w') as f:
json.dump(command_info_dict, f)
Loading

0 comments on commit 0153a84

Please sign in to comment.