diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71c857f2..688aa66e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,8 +56,8 @@ jobs: max-parallel: 8 fail-fast: false matrix: - redis-image: [ "redis:6.2.14", "redis:7.0.15", "redis:7.4.0" ] - python-version: [ "3.9", "3.11", "3.12", "3.13" ] + redis-image: [ "redis:6.2.16", "redis:7.4.1" ] + python-version: [ "3.9", "3.12", "3.13" ] redis-py: [ "4.3.6", "4.6.0", "5.0.8", "5.2.1", "5.3.0b3" ] include: - python-version: "3.12" @@ -66,12 +66,12 @@ jobs: extra: "lua" hypothesis: true - python-version: "3.12" - redis-image: "redis/redis-stack-server:6.2.6-v15" + redis-image: "redis/redis-stack-server:6.2.6-v17" redis-py: "5.2.1" extra: "json, bf, lua, cf" hypothesis: true - python-version: "3.12" - redis-image: "redis/redis-stack-server:7.4.0-v0" + redis-image: "redis/redis-stack-server:7.4.0-v1" redis-py: "5.2.1" extra: "json, bf, lua, cf" coverage: true diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 779ba8b6..0c5b8f92 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -11,9 +11,16 @@ toc_depth: 2 ### 🚀 Features +- ACL commands support #338 - Add support disable_decoding in async read_response #349 - Implement support for `SADDEX`, using a new set implementation with support for expiring members #350 +### 🧰 Maintenance + +- Remove end of life python 3.8 from test matrix +- Add python 3.13 to test matrix +- Improve documentation for Dragonfly/Valkey support + ## v2.26.2 ### 🐛 Bug Fixes diff --git a/fakeredis/_basefakesocket.py b/fakeredis/_basefakesocket.py index 81c9d005..e3d09f86 100644 --- a/fakeredis/_basefakesocket.py +++ b/fakeredis/_basefakesocket.py @@ -86,6 +86,8 @@ def __init__(self, server: "FakeServer", db: int, *args: Any, **kwargs: Any) -> self._in_transaction: bool self._pubsub: int self._transaction_failed: bool + self._current_user: bytes = b"default" + self._client_info: bytes = kwargs.pop("client_info", b"") @property def version(self) -> Tuple[int, ...]: @@ -181,6 +183,49 @@ def _parse_commands(self) -> Generator[None, Any, None]: buf = buf[length + 2 :] # +2 to skip the CRLF self._process_command(fields) + def _process_command(self, fields: List[bytes]) -> None: + if not fields: + return + result: Any + cmd, cmd_arguments = _extract_command(fields) + try: + func, sig = self._name_to_func(cmd) + self._server.acl.validate_command(self._current_user, self._client_info, fields) # ACL check + with self._server.lock: + # Clean out old connections + while True: + try: + weak_sock = self._server.closed_sockets.pop() + except IndexError: + break + else: + sock = weak_sock() + if sock: + sock._cleanup(self._server) + now = time.time() + for db in self._server.dbs.values(): + db.time = now + sig.check_arity(cmd_arguments, self.version) + if self._transaction is not None and msgs.FLAG_TRANSACTION not in sig.flags: + self._transaction.append((func, sig, cmd_arguments)) + result = QUEUED + else: + result = self._run_command(func, sig, cmd_arguments, False) + except SimpleError as exc: + if self._transaction is not None: + # TODO: should not apply if the exception is from _run_command + # e.g. watch inside multi + self._transaction_failed = True + if cmd == "exec" and exc.value.startswith("ERR "): + exc.value = "EXECABORT Transaction discarded because of: " + exc.value[4:] + self._transaction = None + self._transaction_failed = False + self._clear_watches() + result = exc + result = self._decode_result(result) + if not isinstance(result, NoResponse): + self.put_response(result) + def _run_command( self, func: Optional[Callable[[Any], Any]], sig: Signature, args: List[Any], from_script: bool ) -> Any: @@ -263,48 +308,6 @@ def sendall(self, data: AnyStr) -> None: data = data.encode("ascii") # type: ignore self._parser.send(data) - def _process_command(self, fields: List[bytes]) -> None: - if not fields: - return - result: Any - cmd, cmd_arguments = _extract_command(fields) - try: - func, sig = self._name_to_func(cmd) - with self._server.lock: - # Clean out old connections - while True: - try: - weak_sock = self._server.closed_sockets.pop() - except IndexError: - break - else: - sock = weak_sock() - if sock: - sock._cleanup(self._server) - now = time.time() - for db in self._server.dbs.values(): - db.time = now - sig.check_arity(cmd_arguments, self.version) - if self._transaction is not None and msgs.FLAG_TRANSACTION not in sig.flags: - self._transaction.append((func, sig, cmd_arguments)) - result = QUEUED - else: - result = self._run_command(func, sig, cmd_arguments, False) - except SimpleError as exc: - if self._transaction is not None: - # TODO: should not apply if the exception is from _run_command - # e.g. watch inside multi - self._transaction_failed = True - if cmd == "exec" and exc.value.startswith("ERR "): - exc.value = "EXECABORT Transaction discarded because of: " + exc.value[4:] - self._transaction = None - self._transaction_failed = False - self._clear_watches() - result = exc - result = self._decode_result(result) - if not isinstance(result, NoResponse): - self.put_response(result) - def _scan(self, keys, cursor, *args): """This is the basis of most of the ``scan`` methods. diff --git a/fakeredis/_connection.py b/fakeredis/_connection.py index 06369026..e50791b8 100644 --- a/fakeredis/_connection.py +++ b/fakeredis/_connection.py @@ -28,7 +28,12 @@ def connect(self) -> None: def _connect(self) -> FakeSocket: if not self._server.connected: raise redis.ConnectionError(msgs.CONNECTION_ERROR_MSG) - return FakeSocket(self._server, db=self.db, lua_modules=self._lua_modules) + return FakeSocket( + self._server, + db=self.db, + lua_modules=self._lua_modules, + client_info=b"id=3 addr=127.0.0.1:57275 laddr=127.0.0.1:6379 fd=8 name= age=16 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 qbuf=48 qbuf-free=16842 argv-mem=25 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=18737 events=r cmd=auth user=default redir=-1 resp=2", + ) def can_read(self, timeout: Optional[float] = 0) -> bool: if not self._server.connected: @@ -62,7 +67,7 @@ def read_response(self, **kwargs: Any) -> Any: # type: ignore raise redis.ConnectionError(msgs.CONNECTION_ERROR_MSG) else: response = self._sock.responses.get() - if isinstance(response, redis.ResponseError): + if isinstance(response, (redis.ResponseError, redis.AuthenticationError)): raise response if kwargs.get("disable_decoding", False): return response @@ -100,6 +105,7 @@ def __init__( for ind, p in enumerate(parameters) if p.default != inspect.Parameter.empty } + kwds["server"] = server if not kwds.get("connection_pool", None): charset = kwds.get("charset", None) errors = kwds.get("errors", None) @@ -114,9 +120,8 @@ def __init__( "host", "port", "db", - # Ignoring because AUTH is not implemented - # 'username', - # 'password', + "username", + "password", "socket_timeout", "encoding", "encoding_errors", @@ -126,10 +131,10 @@ def __init__( "health_check_interval", "client_name", "connected", + "server", } connection_kwargs = { "connection_class": FakeConnection, - "server": server, "version": version, "server_type": server_type, "lua_modules": lua_modules, @@ -150,11 +155,7 @@ def from_url(cls, *args: Any, **kwargs: Any) -> Self: pool = redis.ConnectionPool.from_url(*args, **kwargs) # Now override how it creates connections pool.connection_class = FakeConnection - # Using username and password fails since AUTH is not implemented. - # https://github.com/cunla/fakeredis-py/issues/9 - pool.connection_kwargs.pop("username", None) - pool.connection_kwargs.pop("password", None) - return cls(connection_pool=pool) + return cls(connection_pool=pool, *args, **kwargs) class FakeStrictRedis(FakeRedisMixin, redis.StrictRedis): # type: ignore diff --git a/fakeredis/_fakesocket.py b/fakeredis/_fakesocket.py index 47296d7f..af55aa65 100644 --- a/fakeredis/_fakesocket.py +++ b/fakeredis/_fakesocket.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Optional, Set, Any from fakeredis.commands_mixins import ( BitmapCommandsMixin, @@ -14,6 +14,7 @@ TransactionsCommandsMixin, SetCommandsMixin, StreamsCommandsMixin, + AclCommandsMixin, ) from fakeredis.stack import ( JSONCommandsMixin, @@ -54,11 +55,14 @@ class FakeSocket( TDigestCommandsMixin, TimeSeriesCommandsMixin, DragonflyCommandsMixin, + AclCommandsMixin, ): def __init__( self, server: "FakeServer", db: int, lua_modules: Optional[Set[str]] = None, # noqa: F821 + *args: Any, + **kwargs, ) -> None: - super(FakeSocket, self).__init__(server, db, lua_modules=lua_modules) + super(FakeSocket, self).__init__(server, db, *args, lua_modules=lua_modules, **kwargs) diff --git a/fakeredis/_msgs.py b/fakeredis/_msgs.py index c6984a3d..770302fd 100644 --- a/fakeredis/_msgs.py +++ b/fakeredis/_msgs.py @@ -90,6 +90,9 @@ ) INVALID_OVERFLOW_TYPE = "ERR Invalid OVERFLOW type specified" +# ACL specific errors +AUTH_FAILURE = "WRONGPASS invalid username-password pair or user is disabled." + # TDigest error messages TDIGEST_KEY_EXISTS = "T-Digest: key already exists" TDIGEST_KEY_NOT_EXISTS = "T-Digest: key does not exist" @@ -118,6 +121,12 @@ TIMESERIES_BAD_FILTER_EXPRESSION = "TSDB: failed parsing labels" HEXPIRE_NUMFIELDS_DIFFERENT = "The `numfields` parameter must match the number of arguments" +MISSING_ACLFILE_CONFIG = "ERR This Redis instance is not configured to use an ACL file. You may want to specify users via the ACL SETUSER command and then issue a CONFIG REWRITE (assuming you have a Redis configuration file set) in order to store users in the Redis configuration." + +NO_PERMISSION_ERROR = "NOPERM User {} has no permissions to run the '{}' command" +NO_PERMISSION_KEY_ERROR = "NOPERM No permissions to access a key" +NO_PERMISSION_CHANNEL_ERROR = "NOPERM No permissions to access a channel" + # Command flags FLAG_NO_SCRIPT = "s" # Command not allowed in scripts FLAG_LEAVE_EMPTY_VAL = "v" diff --git a/fakeredis/_server.py b/fakeredis/_server.py index 4409d393..3a67427d 100644 --- a/fakeredis/_server.py +++ b/fakeredis/_server.py @@ -11,6 +11,7 @@ from typing_extensions import Literal +from fakeredis.model import AccessControlList from fakeredis._helpers import Database, FakeSelector LOGGER = logging.getLogger("fakeredis") @@ -31,10 +32,30 @@ def _create_version(v: VersionType) -> Tuple[int, ...]: return v +def _version_to_str(v: VersionType) -> str: + if isinstance(v, tuple): + return ".".join(str(x) for x in v) + return str(v) + + class FakeServer: _servers_map: Dict[str, "FakeServer"] = dict() - def __init__(self, version: VersionType = (7,), server_type: ServerType = "redis") -> None: + def __init__( + self, + version: VersionType = (7,), + server_type: ServerType = "redis", + config: Dict[bytes, bytes] = None, + ) -> None: + """Initialize a new FakeServer instance. + :param version: The version of the server (e.g. 6, 7.4, "7.4.1", can also be a tuple) + :param server_type: The type of server (redis, dragonfly, valkey) + :param config: A dictionary of configuration options. + + Configuration options: + - `requirepass`: The password required to authenticate to the server. + - `aclfile`: The path to the ACL file. + """ self.lock = threading.Lock() self.dbs: Dict[int, Database] = defaultdict(lambda: Database(self.lock)) # Maps channel/pattern to a weak set of sockets @@ -49,14 +70,20 @@ def __init__(self, version: VersionType = (7,), server_type: ServerType = "redis if server_type not in ("redis", "dragonfly", "valkey"): raise ValueError(f"Unsupported server type: {server_type}") self.server_type: str = server_type + self.config: Dict[bytes, bytes] = config or dict() + self.acl: AccessControlList = AccessControlList() @staticmethod - def get_server(key: str, version: VersionType, server_type: str) -> "FakeServer": - return FakeServer._servers_map.setdefault(key, FakeServer(version=version, server_type=server_type)) + def get_server(key: str, version: VersionType, server_type: ServerType) -> "FakeServer": + if key not in FakeServer._servers_map: + FakeServer._servers_map[key] = FakeServer(version=version, server_type=server_type) + return FakeServer._servers_map[key] class FakeBaseConnectionMixin(object): - def __init__(self, *args: Any, version: VersionType = (7, 0), server_type: str = "redis", **kwargs: Any) -> None: + def __init__( + self, *args: Any, version: VersionType = (7, 0), server_type: ServerType = "redis", **kwargs: Any + ) -> None: self.client_name: Optional[str] = None self.server_key: str self._sock = None @@ -71,7 +98,7 @@ def __init__(self, *args: Any, version: VersionType = (7, 0), server_type: str = else: host, port = kwargs.get("host"), kwargs.get("port") self.server_key = f"{host}:{port}" - self.server_key += f":{server_type}:v{version}" + self.server_key += f":{server_type}:v{_version_to_str(version)[0]}" self._server = FakeServer.get_server(self.server_key, server_type=server_type, version=version) self._server.connected = connected super().__init__(*args, **kwargs) diff --git a/fakeredis/_tcp_server.py b/fakeredis/_tcp_server.py index 4fe990ae..f5ca1846 100644 --- a/fakeredis/_tcp_server.py +++ b/fakeredis/_tcp_server.py @@ -9,6 +9,7 @@ from fakeredis._server import ServerType LOGGER = logging.getLogger("fakeredis") +LOGGER.setLevel(logging.DEBUG) def to_bytes(value) -> bytes: diff --git a/fakeredis/commands.json b/fakeredis/commands.json index 632cc185..2a762c7f 100644 --- a/fakeredis/commands.json +++ b/fakeredis/commands.json @@ -1 +1 @@ -{"append": ["append", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "bgsave": ["bgsave", -1, ["admin", "noscript", "no_async_loading"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "bitcount": ["bitcount", -2, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "bitfield": ["bitfield", -2, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], [["bitfield_ro", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []]]], "bitop": ["bitop", -4, ["write", "denyoom"], 2, 3, 1, ["@write", "@bitmap", "@slow"], [], [], []], "bitpos": ["bitpos", -3, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "blmove": ["blmove", 6, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blmpop": ["blmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blpop": ["blpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "brpop": ["brpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], [["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []]]], "brpoplpush": ["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "bzmpop": ["bzmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@sortedset", "@slow", "@blocking"], [], [], []], "bzpopmax": ["bzpopmax", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "bzpopmin": ["bzpopmin", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "command": ["command", -1, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|count", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|docs", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|getkeys", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], ["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|info", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|list", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], "dbsize": ["dbsize", 1, ["readonly", "fast"], 0, 0, 0, ["@keyspace", "@read", "@fast"], [], [], []], "decr": ["decr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "decrby": ["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "del": ["del", -2, ["write"], 1, 1, 1, ["@keyspace", "@write", "@slow"], [], [], []], "discard": ["discard", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "dump": ["dump", 2, ["readonly"], 1, 1, 1, ["@keyspace", "@read", "@slow"], [], [], []], "echo": ["echo", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "eval": ["eval", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], ["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []], ["eval_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "evalsha": ["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "exec": ["exec", 1, ["noscript", "loading", "stale", "skip_slowlog"], 0, 0, 0, ["@slow", "@transaction"], [], [], []], "exists": ["exists", -2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "expire": ["expire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["expiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "expireat": ["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "expiretime": ["expiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "flushall": ["flushall", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "flushdb": ["flushdb", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "geoadd": ["geoadd", -5, ["write", "denyoom"], 1, 1, 1, ["@write", "@geo", "@slow"], [], [], []], "geodist": ["geodist", -4, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geohash": ["geohash", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geopos": ["geopos", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius": ["georadius", -6, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember": ["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember_ro": ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius_ro": ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geosearch": ["geosearch", -7, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], [["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []]]], "geosearchstore": ["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []], "get": ["get", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], [["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "getbit": ["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], "getdel": ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getex": ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getrange": ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "getset": ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "hdel": ["hdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hexists": ["hexists", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hexpire": ["hexpire", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], ["hexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []]]], "hexpireat": ["hexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hexpiretime": ["hexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hget": ["hget", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], [["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], ["hgetf", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hgetall": ["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hincrby": ["hincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hincrbyfloat": ["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hkeys": ["hkeys", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hlen": ["hlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmget": ["hmget", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmset": ["hmset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hpersist": ["hpersist", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hpexpire": ["hpexpire", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hpexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], ["hpexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []]]], "hpexpireat": ["hpexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hpexpiretime": ["hpexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hpttl": ["hpttl", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hrandfield": ["hrandfield", -2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hscan": ["hscan", -3, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hset": ["hset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hsetf", -6, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], ["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hsetnx": ["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hstrlen": ["hstrlen", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "httl": ["httl", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hvals": ["hvals", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "incr": ["incr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrby": ["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrbyfloat": ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "keys": ["keys", 2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow", "@dangerous"], [], [], []], "lastsave": ["lastsave", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@admin", "@fast", "@dangerous"], [], [], []], "lcs": ["lcs", -3, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "lindex": ["lindex", 3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "linsert": ["linsert", 5, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "llen": ["llen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@list", "@fast"], [], [], []], "lmove": ["lmove", 5, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "lmpop": ["lmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lpop": ["lpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lpos": ["lpos", -3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lpush": ["lpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "lpushx": ["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lrange": ["lrange", 4, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lrem": ["lrem", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lset": ["lset", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "ltrim": ["ltrim", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "mget": ["mget", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "move": ["move", 3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "mset": ["mset", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], [["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []]]], "msetnx": ["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []], "multi": ["multi", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "persist": ["persist", 2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pexpire": ["pexpire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["pexpiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "pexpireat": ["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pexpiretime": ["pexpiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "pfadd": ["pfadd", -2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hyperloglog", "@fast"], [], [], []], "pfcount": ["pfcount", -2, ["readonly"], 1, 1, 1, ["@read", "@hyperloglog", "@slow"], [], [], []], "pfmerge": ["pfmerge", -2, ["write", "denyoom"], 1, 2, 1, ["@write", "@hyperloglog", "@slow"], [], [], []], "ping": ["ping", -1, ["fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "psetex": ["psetex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "psubscribe": ["psubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pttl": ["pttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "publish": ["publish", 3, ["pubsub", "loading", "stale", "fast"], 0, 0, 0, ["@pubsub", "@fast"], [], [], []], "pubsub": ["pubsub", -2, [], 0, 0, 0, ["@slow"], [], [], [["pubsub|channels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["pubsub|numpat", 2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|numsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardchannels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardnumsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []]]], "punsubscribe": ["punsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "randomkey": ["randomkey", 1, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "rename": ["rename", 3, ["write"], 1, 2, 1, ["@keyspace", "@write", "@slow"], [], [], [["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []]]], "renamenx": ["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []], "restore": ["restore", -4, ["write", "denyoom"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], [["restore-asking", -4, ["write", "denyoom", "asking"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []]]], "rpop": ["rpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []]]], "rpoplpush": ["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "rpush": ["rpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "rpushx": ["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "sadd": ["sadd", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "save": ["save", 1, ["admin", "noscript", "no_async_loading", "no_multi"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "scan": ["scan", -2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "scard": ["scard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "script": ["script", -2, [], 0, 0, 0, ["@slow"], [], [], [["script|debug", 3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|exists", -3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|flush", -2, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|kill", 2, ["noscript", "allow_busy"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|load", 3, ["noscript", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []]]], "sdiff": ["sdiff", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sdiffstore": ["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "select": ["select", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "set": ["set", -3, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], [["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []]]], "setbit": ["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], "setex": ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "setnx": ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "setrange": ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "sinter": ["sinter", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sintercard": ["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "sinterstore": ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sismember": ["sismember", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smembers": ["smembers", 2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "smismember": ["smismember", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smove": ["smove", 4, ["write", "fast"], 1, 2, 1, ["@write", "@set", "@fast"], [], [], []], "sort": ["sort", -2, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], [["sort_ro", -2, ["readonly", "movablekeys"], 1, 0, 1, ["@read", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], []]]], "sort_ro": ["sort_ro", -2, ["readonly", "movablekeys"], 1, 0, 1, ["@read", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], []], "spop": ["spop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "spublish": ["spublish", 3, ["pubsub", "loading", "stale", "fast"], 1, 1, 1, ["@pubsub", "@fast"], [], [], []], "srandmember": ["srandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "srem": ["srem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "sscan": ["sscan", -3, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "ssubscribe": ["ssubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "strlen": ["strlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "subscribe": ["subscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "substr": ["substr", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "sunion": ["sunion", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sunionstore": ["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sunsubscribe": ["sunsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "swapdb": ["swapdb", 3, ["write", "fast"], 0, 0, 0, ["@keyspace", "@write", "@fast", "@dangerous"], [], [], []], "time": ["time", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@fast"], [], [], []], "ttl": ["ttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "type": ["type", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "unlink": ["unlink", -2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "unsubscribe": ["unsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "unwatch": ["unwatch", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "watch": ["watch", -2, ["noscript", "loading", "stale", "fast", "allow_busy"], 1, 1, 1, ["@fast", "@transaction"], [], [], []], "xack": ["xack", -4, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xadd": ["xadd", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xautoclaim": ["xautoclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xclaim": ["xclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xdel": ["xdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xlen": ["xlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@stream", "@fast"], [], [], []], "xpending": ["xpending", -3, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xrange": ["xrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xread": ["xread", -4, ["readonly", "blocking", "movablekeys"], 0, 0, 1, ["@read", "@stream", "@slow", "@blocking"], [], [], [["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []]]], "xreadgroup": ["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []], "xrevrange": ["xrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xtrim": ["xtrim", -4, ["write"], 1, 1, 1, ["@write", "@stream", "@slow"], [], [], []], "zadd": ["zadd", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zcard": ["zcard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zcount": ["zcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zdiff": ["zdiff", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zdiffstore": ["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zincrby": ["zincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zinter": ["zinter", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zintercard": ["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zinterstore": ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zlexcount": ["zlexcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zmpop": ["zmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zmscore": ["zmscore", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zpopmax": ["zpopmax", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zpopmin": ["zpopmin", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zrandmember": ["zrandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrange": ["zrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zrangebylex": ["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangebyscore": ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangestore": ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrank": ["zrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zrem": ["zrem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], [["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zremrangebylex": ["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyrank": ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyscore": ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrevrange": ["zrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []]]], "zrevrangebylex": ["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrangebyscore": ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrank": ["zrevrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zscan": ["zscan", -3, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zscore": ["zscore", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zunion": ["zunion", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zunionstore": ["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "json.del": ["json.del", -1, [], 0, 0, 0, [], [], [], []], "json.forget": ["json.forget", -1, [], 0, 0, 0, [], [], [], []], "json.get": ["json.get", -1, [], 0, 0, 0, [], [], [], []], "json.toggle": ["json.toggle", -1, [], 0, 0, 0, [], [], [], []], "json.clear": ["json.clear", -1, [], 0, 0, 0, [], [], [], []], "json.set": ["json.set", -1, [], 0, 0, 0, [], [], [], []], "json.mset": ["json.mset", -1, [], 0, 0, 0, [], [], [], []], "json.merge": ["json.merge", -1, [], 0, 0, 0, [], [], [], []], "json.mget": ["json.mget", -1, [], 0, 0, 0, [], [], [], []], "json.numincrby": ["json.numincrby", -1, [], 0, 0, 0, [], [], [], []], "json.nummultby": ["json.nummultby", -1, [], 0, 0, 0, [], [], [], []], "json.strappend": ["json.strappend", -1, [], 0, 0, 0, [], [], [], []], "json.strlen": ["json.strlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrappend": ["json.arrappend", -1, [], 0, 0, 0, [], [], [], []], "json.arrindex": ["json.arrindex", -1, [], 0, 0, 0, [], [], [], []], "json.arrinsert": ["json.arrinsert", -1, [], 0, 0, 0, [], [], [], []], "json.arrlen": ["json.arrlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrpop": ["json.arrpop", -1, [], 0, 0, 0, [], [], [], []], "json.arrtrim": ["json.arrtrim", -1, [], 0, 0, 0, [], [], [], []], "json.objkeys": ["json.objkeys", -1, [], 0, 0, 0, [], [], [], []], "json.objlen": ["json.objlen", -1, [], 0, 0, 0, [], [], [], []], "json.type": ["json.type", -1, [], 0, 0, 0, [], [], [], []], "ts.create": ["ts.create", -1, [], 0, 0, 0, [], [], [], [["ts.createrule", -1, [], 0, 0, 0, [], [], [], []]]], "ts.del": ["ts.del", -1, [], 0, 0, 0, [], [], [], [["ts.deleterule", -1, [], 0, 0, 0, [], [], [], []]]], "ts.alter": ["ts.alter", -1, [], 0, 0, 0, [], [], [], []], "ts.add": ["ts.add", -1, [], 0, 0, 0, [], [], [], []], "ts.madd": ["ts.madd", -1, [], 0, 0, 0, [], [], [], []], "ts.incrby": ["ts.incrby", -1, [], 0, 0, 0, [], [], [], []], "ts.decrby": ["ts.decrby", -1, [], 0, 0, 0, [], [], [], []], "ts.createrule": ["ts.createrule", -1, [], 0, 0, 0, [], [], [], []], "ts.deleterule": ["ts.deleterule", -1, [], 0, 0, 0, [], [], [], []], "ts.range": ["ts.range", -1, [], 0, 0, 0, [], [], [], []], "ts.revrange": ["ts.revrange", -1, [], 0, 0, 0, [], [], [], []], "ts.mrange": ["ts.mrange", -1, [], 0, 0, 0, [], [], [], []], "ts.mrevrange": ["ts.mrevrange", -1, [], 0, 0, 0, [], [], [], []], "ts.get": ["ts.get", -1, [], 0, 0, 0, [], [], [], []], "ts.mget": ["ts.mget", -1, [], 0, 0, 0, [], [], [], []], "ts.info": ["ts.info", -1, [], 0, 0, 0, [], [], [], []], "ts.queryindex": ["ts.queryindex", -1, [], 0, 0, 0, [], [], [], []], "bf.reserve": ["bf.reserve", -1, [], 0, 0, 0, [], [], [], []], "bf.add": ["bf.add", -1, [], 0, 0, 0, [], [], [], []], "bf.madd": ["bf.madd", -1, [], 0, 0, 0, [], [], [], []], "bf.insert": ["bf.insert", -1, [], 0, 0, 0, [], [], [], []], "bf.exists": ["bf.exists", -1, [], 0, 0, 0, [], [], [], []], "bf.mexists": ["bf.mexists", -1, [], 0, 0, 0, [], [], [], []], "bf.scandump": ["bf.scandump", -1, [], 0, 0, 0, [], [], [], []], "bf.loadchunk": ["bf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "bf.info": ["bf.info", -1, [], 0, 0, 0, [], [], [], []], "bf.card": ["bf.card", -1, [], 0, 0, 0, [], [], [], []], "cf.reserve": ["cf.reserve", -1, [], 0, 0, 0, [], [], [], []], "cf.add": ["cf.add", -1, [], 0, 0, 0, [], [], [], [["cf.addnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.addnx": ["cf.addnx", -1, [], 0, 0, 0, [], [], [], []], "cf.insert": ["cf.insert", -1, [], 0, 0, 0, [], [], [], [["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.insertnx": ["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []], "cf.exists": ["cf.exists", -1, [], 0, 0, 0, [], [], [], []], "cf.mexists": ["cf.mexists", -1, [], 0, 0, 0, [], [], [], []], "cf.del": ["cf.del", -1, [], 0, 0, 0, [], [], [], []], "cf.count": ["cf.count", -1, [], 0, 0, 0, [], [], [], []], "cf.scandump": ["cf.scandump", -1, [], 0, 0, 0, [], [], [], []], "cf.loadchunk": ["cf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "cf.info": ["cf.info", -1, [], 0, 0, 0, [], [], [], []], "cms.initbydim": ["cms.initbydim", -1, [], 0, 0, 0, [], [], [], []], "cms.initbyprob": ["cms.initbyprob", -1, [], 0, 0, 0, [], [], [], []], "cms.incrby": ["cms.incrby", -1, [], 0, 0, 0, [], [], [], []], "cms.query": ["cms.query", -1, [], 0, 0, 0, [], [], [], []], "cms.merge": ["cms.merge", -1, [], 0, 0, 0, [], [], [], []], "cms.info": ["cms.info", -1, [], 0, 0, 0, [], [], [], []], "topk.reserve": ["topk.reserve", -1, [], 0, 0, 0, [], [], [], []], "topk.add": ["topk.add", -1, [], 0, 0, 0, [], [], [], []], "topk.incrby": ["topk.incrby", -1, [], 0, 0, 0, [], [], [], []], "topk.query": ["topk.query", -1, [], 0, 0, 0, [], [], [], []], "topk.count": ["topk.count", -1, [], 0, 0, 0, [], [], [], []], "topk.list": ["topk.list", -1, [], 0, 0, 0, [], [], [], []], "topk.info": ["topk.info", -1, [], 0, 0, 0, [], [], [], []], "tdigest.create": ["tdigest.create", -1, [], 0, 0, 0, [], [], [], []], "tdigest.reset": ["tdigest.reset", -1, [], 0, 0, 0, [], [], [], []], "tdigest.add": ["tdigest.add", -1, [], 0, 0, 0, [], [], [], []], "tdigest.merge": ["tdigest.merge", -1, [], 0, 0, 0, [], [], [], []], "tdigest.min": ["tdigest.min", -1, [], 0, 0, 0, [], [], [], []], "tdigest.max": ["tdigest.max", -1, [], 0, 0, 0, [], [], [], []], "tdigest.quantile": ["tdigest.quantile", -1, [], 0, 0, 0, [], [], [], []], "tdigest.cdf": ["tdigest.cdf", -1, [], 0, 0, 0, [], [], [], []], "tdigest.trimmed_mean": ["tdigest.trimmed_mean", -1, [], 0, 0, 0, [], [], [], []], "tdigest.rank": ["tdigest.rank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.revrank": ["tdigest.revrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrank": ["tdigest.byrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrevrank": ["tdigest.byrevrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.info": ["tdigest.info", -1, [], 0, 0, 0, [], [], [], []]} \ No newline at end of file +{"acl cat": ["acl|cat", -2, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], "acl": ["acl", -1, [], 0, 0, 0, [], [], [], [["acl|cat", -2, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["acl|deluser", -3, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|genpass", -2, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["acl|getuser", 3, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|list", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|load", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|log", -2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|save", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|setuser", -3, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|users", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], ["acl|whoami", 2, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow"], [], [], []]]], "acl deluser": ["acl|deluser", -3, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl genpass": ["acl|genpass", -2, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], "acl getuser": ["acl|getuser", 3, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl list": ["acl|list", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl load": ["acl|load", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl log": ["acl|log", -2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl save": ["acl|save", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl setuser": ["acl|setuser", -3, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl users": ["acl|users", 2, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "acl whoami": ["acl|whoami", 2, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], "append": ["append", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "auth": ["auth", -2, ["noscript", "loading", "stale", "fast", "no_auth", "allow_busy"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "bgsave": ["bgsave", -1, ["admin", "noscript", "no_async_loading"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "bitcount": ["bitcount", -2, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "bitfield": ["bitfield", -2, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], [["bitfield_ro", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []]]], "bitop": ["bitop", -4, ["write", "denyoom"], 2, 3, 1, ["@write", "@bitmap", "@slow"], [], [], []], "bitpos": ["bitpos", -3, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "blmove": ["blmove", 6, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blmpop": ["blmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blpop": ["blpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "brpop": ["brpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], [["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []]]], "brpoplpush": ["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "bzmpop": ["bzmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@sortedset", "@slow", "@blocking"], [], [], []], "bzpopmax": ["bzpopmax", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "bzpopmin": ["bzpopmin", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "client setinfo": ["client|setinfo", 4, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], "client": ["client", -1, [], 0, 0, 0, [], [], [], [["client|setinfo", 4, ["noscript", "loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], "command": ["command", -1, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|count", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|docs", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|getkeys", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], ["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|info", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|list", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|count", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|info", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], "command count": ["command|count", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], "command info": ["command|info", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], "config set": ["config|set", -4, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "config": ["config", -1, [], 0, 0, 0, [], [], [], [["config|set", -4, ["admin", "noscript", "loading", "stale"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []]]], "dbsize": ["dbsize", 1, ["readonly", "fast"], 0, 0, 0, ["@keyspace", "@read", "@fast"], [], [], []], "decr": ["decr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "decrby": ["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "del": ["del", -2, ["write"], 1, 1, 1, ["@keyspace", "@write", "@slow"], [], [], []], "discard": ["discard", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "dump": ["dump", 2, ["readonly"], 1, 1, 1, ["@keyspace", "@read", "@slow"], [], [], []], "echo": ["echo", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "eval": ["eval", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], ["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []], ["eval_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "evalsha": ["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "exec": ["exec", 1, ["noscript", "loading", "stale", "skip_slowlog"], 0, 0, 0, ["@slow", "@transaction"], [], [], []], "exists": ["exists", -2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "expire": ["expire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["expiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "expireat": ["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "expiretime": ["expiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "flushall": ["flushall", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "flushdb": ["flushdb", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "geoadd": ["geoadd", -5, ["write", "denyoom"], 1, 1, 1, ["@write", "@geo", "@slow"], [], [], []], "geodist": ["geodist", -4, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geohash": ["geohash", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geopos": ["geopos", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius": ["georadius", -6, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember": ["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember_ro": ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius_ro": ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geosearch": ["geosearch", -7, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], [["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []]]], "geosearchstore": ["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []], "get": ["get", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], [["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "getbit": ["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], "getdel": ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getex": ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getrange": ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "getset": ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "hdel": ["hdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hexists": ["hexists", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hexpire": ["hexpire", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], ["hexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []]]], "hexpireat": ["hexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hexpiretime": ["hexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hget": ["hget", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], [["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], ["hgetf", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hgetall": ["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hincrby": ["hincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hincrbyfloat": ["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hkeys": ["hkeys", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hlen": ["hlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmget": ["hmget", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmset": ["hmset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hpersist": ["hpersist", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hpexpire": ["hpexpire", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hpexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], ["hpexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []]]], "hpexpireat": ["hpexpireat", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hpexpiretime": ["hpexpiretime", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hpttl": ["hpttl", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hrandfield": ["hrandfield", -2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hscan": ["hscan", -3, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hset": ["hset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hsetf", -6, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], ["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hsetnx": ["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hstrlen": ["hstrlen", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "httl": ["httl", -4, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hvals": ["hvals", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "incr": ["incr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrby": ["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrbyfloat": ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "keys": ["keys", 2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow", "@dangerous"], [], [], []], "lastsave": ["lastsave", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@admin", "@fast", "@dangerous"], [], [], []], "lcs": ["lcs", -3, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "lindex": ["lindex", 3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "linsert": ["linsert", 5, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "llen": ["llen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@list", "@fast"], [], [], []], "lmove": ["lmove", 5, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "lmpop": ["lmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lpop": ["lpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lpos": ["lpos", -3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lpush": ["lpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "lpushx": ["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lrange": ["lrange", 4, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lrem": ["lrem", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lset": ["lset", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "ltrim": ["ltrim", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "mget": ["mget", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "move": ["move", 3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "mset": ["mset", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], [["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []]]], "msetnx": ["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []], "multi": ["multi", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "persist": ["persist", 2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pexpire": ["pexpire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["pexpiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "pexpireat": ["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pexpiretime": ["pexpiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "pfadd": ["pfadd", -2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hyperloglog", "@fast"], [], [], []], "pfcount": ["pfcount", -2, ["readonly"], 1, 1, 1, ["@read", "@hyperloglog", "@slow"], [], [], []], "pfmerge": ["pfmerge", -2, ["write", "denyoom"], 1, 2, 1, ["@write", "@hyperloglog", "@slow"], [], [], []], "ping": ["ping", -1, ["fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "psetex": ["psetex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "psubscribe": ["psubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pttl": ["pttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "publish": ["publish", 3, ["pubsub", "loading", "stale", "fast"], 0, 0, 0, ["@pubsub", "@fast"], [], [], []], "pubsub": ["pubsub", -2, [], 0, 0, 0, ["@slow"], [], [], [["pubsub|channels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["pubsub|numpat", 2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|numsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardchannels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardnumsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|channels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["pubsub|numpat", 2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|numsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardchannels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardnumsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []]]], "pubsub channels": ["pubsub|channels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pubsub help": ["pubsub|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], "pubsub numpat": ["pubsub|numpat", 2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pubsub numsub": ["pubsub|numsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pubsub shardchannels": ["pubsub|shardchannels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pubsub shardnumsub": ["pubsub|shardnumsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "punsubscribe": ["punsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "randomkey": ["randomkey", 1, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "rename": ["rename", 3, ["write"], 1, 2, 1, ["@keyspace", "@write", "@slow"], [], [], [["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []]]], "renamenx": ["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []], "restore": ["restore", -4, ["write", "denyoom"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], [["restore-asking", -4, ["write", "denyoom", "asking"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []]]], "rpop": ["rpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []]]], "rpoplpush": ["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "rpush": ["rpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "rpushx": ["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "sadd": ["sadd", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "save": ["save", 1, ["admin", "noscript", "no_async_loading", "no_multi"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "scan": ["scan", -2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "scard": ["scard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "script": ["script", -2, [], 0, 0, 0, ["@slow"], [], [], [["script|debug", 3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|exists", -3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|flush", -2, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|kill", 2, ["noscript", "allow_busy"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|load", 3, ["noscript", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|exists", -3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|flush", -2, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|load", 3, ["noscript", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []]]], "script exists": ["script|exists", -3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], "script flush": ["script|flush", -2, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], "script help": ["script|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], "script load": ["script|load", 3, ["noscript", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], "sdiff": ["sdiff", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sdiffstore": ["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "select": ["select", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "set": ["set", -3, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], [["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []]]], "setbit": ["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], "setex": ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "setnx": ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "setrange": ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "sinter": ["sinter", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sintercard": ["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "sinterstore": ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sismember": ["sismember", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smembers": ["smembers", 2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "smismember": ["smismember", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smove": ["smove", 4, ["write", "fast"], 1, 2, 1, ["@write", "@set", "@fast"], [], [], []], "sort": ["sort", -2, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], [["sort_ro", -2, ["readonly", "movablekeys"], 1, 0, 1, ["@read", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], []]]], "sort_ro": ["sort_ro", -2, ["readonly", "movablekeys"], 1, 0, 1, ["@read", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], []], "spop": ["spop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "spublish": ["spublish", 3, ["pubsub", "loading", "stale", "fast"], 1, 1, 1, ["@pubsub", "@fast"], [], [], []], "srandmember": ["srandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "srem": ["srem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "sscan": ["sscan", -3, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "ssubscribe": ["ssubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "strlen": ["strlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "subscribe": ["subscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "substr": ["substr", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "sunion": ["sunion", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sunionstore": ["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sunsubscribe": ["sunsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "swapdb": ["swapdb", 3, ["write", "fast"], 0, 0, 0, ["@keyspace", "@write", "@fast", "@dangerous"], [], [], []], "time": ["time", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@fast"], [], [], []], "ttl": ["ttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "type": ["type", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "unlink": ["unlink", -2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "unsubscribe": ["unsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "unwatch": ["unwatch", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "watch": ["watch", -2, ["noscript", "loading", "stale", "fast", "allow_busy"], 1, 1, 1, ["@fast", "@transaction"], [], [], []], "xack": ["xack", -4, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xadd": ["xadd", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xautoclaim": ["xautoclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xclaim": ["xclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xdel": ["xdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xgroup create": ["xgroup|create", -5, ["write", "denyoom"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], [["xgroup|createconsumer", 5, ["write", "denyoom"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []]]], "xgroup": ["xgroup", -1, [], 0, 0, 0, [], [], [], [["xgroup|create", -5, ["write", "denyoom"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], [["xgroup|createconsumer", 5, ["write", "denyoom"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []]]], ["xgroup|createconsumer", 5, ["write", "denyoom"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], ["xgroup|delconsumer", 5, ["write"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], ["xgroup|destroy", 4, ["write"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], ["xgroup|setid", -5, ["write"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []]]], "xgroup createconsumer": ["xgroup|createconsumer", 5, ["write", "denyoom"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], "xgroup delconsumer": ["xgroup|delconsumer", 5, ["write"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], "xgroup destroy": ["xgroup|destroy", 4, ["write"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], "xgroup setid": ["xgroup|setid", -5, ["write"], 2, 2, 1, ["@write", "@stream", "@slow"], [], [], []], "xinfo consumers": ["xinfo|consumers", 4, ["readonly"], 2, 2, 1, ["@read", "@stream", "@slow"], [], [], []], "xinfo": ["xinfo", -1, [], 0, 0, 0, [], [], [], [["xinfo|consumers", 4, ["readonly"], 2, 2, 1, ["@read", "@stream", "@slow"], [], [], []], ["xinfo|groups", 3, ["readonly"], 2, 2, 1, ["@read", "@stream", "@slow"], [], [], []], ["xinfo|stream", -3, ["readonly"], 2, 2, 1, ["@read", "@stream", "@slow"], [], [], []]]], "xinfo groups": ["xinfo|groups", 3, ["readonly"], 2, 2, 1, ["@read", "@stream", "@slow"], [], [], []], "xinfo stream": ["xinfo|stream", -3, ["readonly"], 2, 2, 1, ["@read", "@stream", "@slow"], [], [], []], "xlen": ["xlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@stream", "@fast"], [], [], []], "xpending": ["xpending", -3, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xrange": ["xrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xread": ["xread", -4, ["readonly", "blocking", "movablekeys"], 0, 0, 1, ["@read", "@stream", "@slow", "@blocking"], [], [], [["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []]]], "xreadgroup": ["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []], "xrevrange": ["xrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xtrim": ["xtrim", -4, ["write"], 1, 1, 1, ["@write", "@stream", "@slow"], [], [], []], "zadd": ["zadd", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zcard": ["zcard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zcount": ["zcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zdiff": ["zdiff", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zdiffstore": ["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zincrby": ["zincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zinter": ["zinter", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zintercard": ["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zinterstore": ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zlexcount": ["zlexcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zmpop": ["zmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zmscore": ["zmscore", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zpopmax": ["zpopmax", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zpopmin": ["zpopmin", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zrandmember": ["zrandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrange": ["zrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zrangebylex": ["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangebyscore": ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangestore": ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrank": ["zrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zrem": ["zrem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], [["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zremrangebylex": ["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyrank": ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyscore": ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrevrange": ["zrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []]]], "zrevrangebylex": ["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrangebyscore": ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrank": ["zrevrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zscan": ["zscan", -3, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zscore": ["zscore", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zunion": ["zunion", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zunionstore": ["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "json.del": ["json.del", -1, [], 0, 0, 0, [], [], [], []], "json.forget": ["json.forget", -1, [], 0, 0, 0, [], [], [], []], "json.get": ["json.get", -1, [], 0, 0, 0, [], [], [], []], "json.toggle": ["json.toggle", -1, [], 0, 0, 0, [], [], [], []], "json.clear": ["json.clear", -1, [], 0, 0, 0, [], [], [], []], "json.set": ["json.set", -1, [], 0, 0, 0, [], [], [], []], "json.mset": ["json.mset", -1, [], 0, 0, 0, [], [], [], []], "json.merge": ["json.merge", -1, [], 0, 0, 0, [], [], [], []], "json.mget": ["json.mget", -1, [], 0, 0, 0, [], [], [], []], "json.numincrby": ["json.numincrby", -1, [], 0, 0, 0, [], [], [], []], "json.nummultby": ["json.nummultby", -1, [], 0, 0, 0, [], [], [], []], "json.strappend": ["json.strappend", -1, [], 0, 0, 0, [], [], [], []], "json.strlen": ["json.strlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrappend": ["json.arrappend", -1, [], 0, 0, 0, [], [], [], []], "json.arrindex": ["json.arrindex", -1, [], 0, 0, 0, [], [], [], []], "json.arrinsert": ["json.arrinsert", -1, [], 0, 0, 0, [], [], [], []], "json.arrlen": ["json.arrlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrpop": ["json.arrpop", -1, [], 0, 0, 0, [], [], [], []], "json.arrtrim": ["json.arrtrim", -1, [], 0, 0, 0, [], [], [], []], "json.objkeys": ["json.objkeys", -1, [], 0, 0, 0, [], [], [], []], "json.objlen": ["json.objlen", -1, [], 0, 0, 0, [], [], [], []], "json.type": ["json.type", -1, [], 0, 0, 0, [], [], [], []], "ts.create": ["ts.create", -1, [], 0, 0, 0, [], [], [], [["ts.createrule", -1, [], 0, 0, 0, [], [], [], []]]], "ts.del": ["ts.del", -1, [], 0, 0, 0, [], [], [], [["ts.deleterule", -1, [], 0, 0, 0, [], [], [], []]]], "ts.alter": ["ts.alter", -1, [], 0, 0, 0, [], [], [], []], "ts.add": ["ts.add", -1, [], 0, 0, 0, [], [], [], []], "ts.madd": ["ts.madd", -1, [], 0, 0, 0, [], [], [], []], "ts.incrby": ["ts.incrby", -1, [], 0, 0, 0, [], [], [], []], "ts.decrby": ["ts.decrby", -1, [], 0, 0, 0, [], [], [], []], "ts.createrule": ["ts.createrule", -1, [], 0, 0, 0, [], [], [], []], "ts.deleterule": ["ts.deleterule", -1, [], 0, 0, 0, [], [], [], []], "ts.range": ["ts.range", -1, [], 0, 0, 0, [], [], [], []], "ts.revrange": ["ts.revrange", -1, [], 0, 0, 0, [], [], [], []], "ts.mrange": ["ts.mrange", -1, [], 0, 0, 0, [], [], [], []], "ts.mrevrange": ["ts.mrevrange", -1, [], 0, 0, 0, [], [], [], []], "ts.get": ["ts.get", -1, [], 0, 0, 0, [], [], [], []], "ts.mget": ["ts.mget", -1, [], 0, 0, 0, [], [], [], []], "ts.info": ["ts.info", -1, [], 0, 0, 0, [], [], [], []], "ts.queryindex": ["ts.queryindex", -1, [], 0, 0, 0, [], [], [], []], "bf.reserve": ["bf.reserve", -1, [], 0, 0, 0, [], [], [], []], "bf.add": ["bf.add", -1, [], 0, 0, 0, [], [], [], []], "bf.madd": ["bf.madd", -1, [], 0, 0, 0, [], [], [], []], "bf.insert": ["bf.insert", -1, [], 0, 0, 0, [], [], [], []], "bf.exists": ["bf.exists", -1, [], 0, 0, 0, [], [], [], []], "bf.mexists": ["bf.mexists", -1, [], 0, 0, 0, [], [], [], []], "bf.scandump": ["bf.scandump", -1, [], 0, 0, 0, [], [], [], []], "bf.loadchunk": ["bf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "bf.info": ["bf.info", -1, [], 0, 0, 0, [], [], [], []], "bf.card": ["bf.card", -1, [], 0, 0, 0, [], [], [], []], "cf.reserve": ["cf.reserve", -1, [], 0, 0, 0, [], [], [], []], "cf.add": ["cf.add", -1, [], 0, 0, 0, [], [], [], [["cf.addnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.addnx": ["cf.addnx", -1, [], 0, 0, 0, [], [], [], []], "cf.insert": ["cf.insert", -1, [], 0, 0, 0, [], [], [], [["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.insertnx": ["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []], "cf.exists": ["cf.exists", -1, [], 0, 0, 0, [], [], [], []], "cf.mexists": ["cf.mexists", -1, [], 0, 0, 0, [], [], [], []], "cf.del": ["cf.del", -1, [], 0, 0, 0, [], [], [], []], "cf.count": ["cf.count", -1, [], 0, 0, 0, [], [], [], []], "cf.scandump": ["cf.scandump", -1, [], 0, 0, 0, [], [], [], []], "cf.loadchunk": ["cf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "cf.info": ["cf.info", -1, [], 0, 0, 0, [], [], [], []], "cms.initbydim": ["cms.initbydim", -1, [], 0, 0, 0, [], [], [], []], "cms.initbyprob": ["cms.initbyprob", -1, [], 0, 0, 0, [], [], [], []], "cms.incrby": ["cms.incrby", -1, [], 0, 0, 0, [], [], [], []], "cms.query": ["cms.query", -1, [], 0, 0, 0, [], [], [], []], "cms.merge": ["cms.merge", -1, [], 0, 0, 0, [], [], [], []], "cms.info": ["cms.info", -1, [], 0, 0, 0, [], [], [], []], "topk.reserve": ["topk.reserve", -1, [], 0, 0, 0, [], [], [], []], "topk.add": ["topk.add", -1, [], 0, 0, 0, [], [], [], []], "topk.incrby": ["topk.incrby", -1, [], 0, 0, 0, [], [], [], []], "topk.query": ["topk.query", -1, [], 0, 0, 0, [], [], [], []], "topk.count": ["topk.count", -1, [], 0, 0, 0, [], [], [], []], "topk.list": ["topk.list", -1, [], 0, 0, 0, [], [], [], []], "topk.info": ["topk.info", -1, [], 0, 0, 0, [], [], [], []], "tdigest.create": ["tdigest.create", -1, [], 0, 0, 0, [], [], [], []], "tdigest.reset": ["tdigest.reset", -1, [], 0, 0, 0, [], [], [], []], "tdigest.add": ["tdigest.add", -1, [], 0, 0, 0, [], [], [], []], "tdigest.merge": ["tdigest.merge", -1, [], 0, 0, 0, [], [], [], []], "tdigest.min": ["tdigest.min", -1, [], 0, 0, 0, [], [], [], []], "tdigest.max": ["tdigest.max", -1, [], 0, 0, 0, [], [], [], []], "tdigest.quantile": ["tdigest.quantile", -1, [], 0, 0, 0, [], [], [], []], "tdigest.cdf": ["tdigest.cdf", -1, [], 0, 0, 0, [], [], [], []], "tdigest.trimmed_mean": ["tdigest.trimmed_mean", -1, [], 0, 0, 0, [], [], [], []], "tdigest.rank": ["tdigest.rank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.revrank": ["tdigest.revrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrank": ["tdigest.byrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrevrank": ["tdigest.byrevrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.info": ["tdigest.info", -1, [], 0, 0, 0, [], [], [], []]} \ No newline at end of file diff --git a/fakeredis/commands_mixins/__init__.py b/fakeredis/commands_mixins/__init__.py index 440c9643..5efe5e2b 100644 --- a/fakeredis/commands_mixins/__init__.py +++ b/fakeredis/commands_mixins/__init__.py @@ -1,5 +1,6 @@ from typing import Any +from .acl_mixin import AclCommandsMixin from .bitmap_mixin import BitmapCommandsMixin from .connection_mixin import ConnectionCommandsMixin from .generic_mixin import GenericCommandsMixin @@ -11,6 +12,7 @@ from .set_mixin import SetCommandsMixin from .streams_mixin import StreamsCommandsMixin from .string_mixin import StringCommandsMixin +from .transactions_mixin import TransactionsCommandsMixin try: from .scripting_mixin import ScriptingCommandsMixin @@ -22,8 +24,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super(ScriptingCommandsMixin, self).__init__(*args, **kwargs) # type: ignore -from .transactions_mixin import TransactionsCommandsMixin - __all__ = [ "BitmapCommandsMixin", "ConnectionCommandsMixin", @@ -38,4 +38,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "SetCommandsMixin", "StreamsCommandsMixin", "StringCommandsMixin", + "AclCommandsMixin", ] diff --git a/fakeredis/commands_mixins/acl_mixin.py b/fakeredis/commands_mixins/acl_mixin.py new file mode 100644 index 00000000..cbba5e30 --- /dev/null +++ b/fakeredis/commands_mixins/acl_mixin.py @@ -0,0 +1,188 @@ +import secrets +from typing import Any, Tuple, List, Callable, Dict, Optional, Union + +from fakeredis import _msgs as msgs +from fakeredis._commands import command, Int +from fakeredis._helpers import SimpleError, OK, casematch, SimpleString +from fakeredis.model import AccessControlList +from fakeredis.model import get_categories, get_commands_by_category + + +class AclCommandsMixin: + _get_command_info: Callable[[bytes], List[Any]] + + def __init(self, *args: Any, **kwargs: Any) -> None: + super(AclCommandsMixin).__init__(*args, **kwargs) + self.version: Tuple[int] + self._server: Any + self._current_user: bytes + self._client_info: bytes + + @property + def _server_config(self) -> Dict[bytes, bytes]: + return self._server.config + + @property + def _acl(self) -> AccessControlList: + return self._server.acl + + def _check_user_password(self, username: bytes, password: Optional[bytes]) -> bool: + return self._acl.get_user_acl(username).check_password(password) + + def _set_user_acl(self, username: bytes, *args: bytes) -> None: + user_acl = self._acl.get_user_acl(username) + for arg in args: + if casematch(arg, b"resetchannels"): + user_acl.reset_channels_patterns() + continue + elif casematch(arg, b"resetkeys"): + user_acl.reset_key_patterns() + continue + elif casematch(arg, b"on"): + user_acl.enabled = True + continue + elif casematch(arg, b"off"): + user_acl.enabled = False + continue + elif casematch(arg, b"nopass"): + user_acl.set_nopass() + continue + elif casematch(arg, b"reset"): + user_acl.reset() + continue + elif casematch(arg, b"nocommands"): + arg = b"-@all" + elif casematch(arg, b"allcommands"): + arg = b"+@all" + elif casematch(arg, b"allkeys"): + arg = b"~*" + elif casematch(arg, b"allchannels"): + arg = b"&*" + elif arg[0] == ord("(") and arg[-1] == ord(")"): + user_acl.add_selector(arg[1:-1]) + continue + + prefix = arg[0] + if prefix == ord(">"): + user_acl.add_password(arg[1:]) + elif prefix == ord("<"): + user_acl.remove_password(arg[1:]) + elif prefix == ord("#"): + user_acl.add_password_hex(arg[1:]) + elif prefix == ord("!"): + user_acl.remove_password_hex(arg[1:]) + elif prefix == ord("+") or prefix == ord("-"): + user_acl.add_command_or_category(arg) + elif prefix == ord("~"): + user_acl.add_key_pattern(arg[1:]) + elif prefix == ord("&"): + user_acl.add_channel_pattern(arg[1:]) + + @command(name="CONFIG SET", fixed=(bytes, bytes), repeat=(bytes, bytes)) + def config_set(self, *args: bytes): + if len(args) % 2 != 0: + raise SimpleError(msgs.WRONG_ARGS_MSG6.format("CONFIG SET")) + for i in range(0, len(args), 2): + self._server_config[args[i]] = args[i + 1] + return OK + + @command(name="AUTH", fixed=(), repeat=(bytes,)) + def auth(self, *args: bytes) -> bytes: + if not 1 <= len(args) <= 2: + raise SimpleError(msgs.WRONG_ARGS_MSG6.format("AUTH")) + username = None if len(args) == 1 else args[0] + password = args[1] if len(args) == 2 else args[0] + if (username is None or username == b"default") and (password == self._server_config.get(b"requirepass", b"")): + self._current_user = b"default" + return OK + if len(args) >= 1 and self._check_user_password(username, password): + self._current_user = username + return OK + self._acl.add_log_record(b"auth", b"auth", b"AUTH", username, self._client_info) + raise SimpleError(msgs.AUTH_FAILURE) + + @command(name="ACL CAT", fixed=(), repeat=(bytes,)) + def acl_cat(self, *category: bytes) -> List[bytes]: + if len(category) == 0: + res = get_categories() + else: + res = get_commands_by_category(category[0]) + res = [cmd.replace(b" ", b"|") for cmd in res] + return res + + @command(name="ACL GENPASS", fixed=(), repeat=(bytes,)) + def acl_genpass(self, *args: bytes) -> bytes: + bits = Int.decode(args[0]) if len(args) > 0 else 256 + bits = bits + bits % 4 # Round to 4 + nbytes: int = bits // 8 + return secrets.token_hex(nbytes).encode() + + @command(name="ACL SETUSER", fixed=(bytes,), repeat=(bytes,)) + def acl_setuser(self, username: bytes, *args: bytes) -> bytes: + self._set_user_acl(username, *args) + return OK + + @command(name="ACL LIST", fixed=(), repeat=()) + def acl_list(self) -> List[bytes]: + return self._acl.as_rules() + + @command(name="ACL DELUSER", fixed=(bytes,), repeat=()) + def acl_deluser(self, username: bytes) -> bytes: + self._acl.del_user(username) + return OK + + @command(name="ACL GETUSER", fixed=(bytes,), repeat=()) + def acl_getuser(self, username: bytes) -> List[bytes]: + res = self._acl.get_user_acl(username).as_array() + return res + + @command(name="ACL USERS", fixed=(), repeat=()) + def acl_users(self) -> List[bytes]: + res = self._acl.get_users() + return res + + @command(name="ACL WHOAMI", fixed=(), repeat=()) + def acl_whoami(self) -> bytes: + return self._current_user + + @command(name="ACL SAVE", fixed=(), repeat=()) + def acl_save(self) -> SimpleString: + if b"aclfile" not in self._server_config: + raise SimpleError(msgs.MISSING_ACLFILE_CONFIG) + acl_filename = self._server_config[b"aclfile"] + with open(acl_filename, "wb") as f: + f.write(b"\n".join(self._acl.as_rules())) + return OK + + @command(name="ACL LOAD", fixed=(), repeat=()) + def acl_load(self) -> SimpleString: + if b"aclfile" not in self._server_config: + raise SimpleError(msgs.MISSING_ACLFILE_CONFIG) + acl_filename = self._server_config[b"aclfile"] + with open(acl_filename, "rb") as f: + rules_list = f.readlines() + for rule in rules_list: + if not rule.startswith(b"user "): + continue + splitted = rule.split(b" ") + components = list() + i = 1 + while i < len(splitted): + current_component = splitted[i] + if current_component.startswith(b"("): + while not current_component.endswith(b")"): + i += 1 + current_component += b" " + splitted[i] + components.append(current_component) + i += 1 + + self._set_user_acl(components[0], *components[1:]) + return OK + + @command(name="ACL LOG", fixed=(), repeat=(bytes,)) + def acl_log(self, *args: bytes) -> Union[SimpleString, List[List[bytes]]]: + if len(args) == 1 and casematch(args[0], b"RESET"): + self._acl.reset_log() + return OK + count = Int.decode(args[0]) if len(args) == 1 else 0 + return self._acl.log(count) diff --git a/fakeredis/commands_mixins/hash_mixin.py b/fakeredis/commands_mixins/hash_mixin.py index 082e789f..6c8b2609 100644 --- a/fakeredis/commands_mixins/hash_mixin.py +++ b/fakeredis/commands_mixins/hash_mixin.py @@ -233,23 +233,25 @@ def hpersist(self, key: CommandItem, *args: bytes) -> List[int]: res.append(-1) return res - @command(name="HEXPIRETIME", fixed=(Key(Hash),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + @command( + name="HEXPIRETIME", fixed=(Key(Hash),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE, server_types=("redis",) + ) def hexpiretime(self, key: CommandItem, *args: bytes) -> List[int]: res = self._get_expireat(b"HEXPIRETIME", key, *args) return [(i // 1000 if i > 0 else i) for i in res] - @command(name="HPEXPIRETIME", fixed=(Key(Hash),), repeat=(bytes,)) + @command(name="HPEXPIRETIME", fixed=(Key(Hash),), repeat=(bytes,), server_types=("redis",)) def hpexpiretime(self, key: CommandItem, *args: bytes) -> List[float]: res = self._get_expireat(b"HEXPIRETIME", key, *args) return res - @command(name="HTTL", fixed=(Key(Hash),), repeat=(bytes,)) + @command(name="HTTL", fixed=(Key(Hash),), repeat=(bytes,), server_types=("redis",)) def httl(self, key: CommandItem, *args: bytes) -> List[int]: curr_expireat_ms = self._get_expireat(b"HEXPIRETIME", key, *args) curr_time_ms = current_time() return [((i - curr_time_ms) // 1000) if i > 0 else i for i in curr_expireat_ms] - @command(name="HPTTL", fixed=(Key(Hash),), repeat=(bytes,)) + @command(name="HPTTL", fixed=(Key(Hash),), repeat=(bytes,), server_types=("redis",)) def hpttl(self, key: CommandItem, *args: bytes) -> List[int]: curr_expireat_ms = self._get_expireat(b"HEXPIRETIME", key, *args) curr_time_ms = current_time() diff --git a/fakeredis/commands_mixins/server_mixin.py b/fakeredis/commands_mixins/server_mixin.py index f097d70a..37406558 100644 --- a/fakeredis/commands_mixins/server_mixin.py +++ b/fakeredis/commands_mixins/server_mixin.py @@ -1,30 +1,10 @@ -import json -import os import time -from typing import Any, List, Optional, Dict +from typing import Any, List, Optional from fakeredis import _msgs as msgs from fakeredis._commands import command, DbIndex from fakeredis._helpers import OK, SimpleError, casematch, BGSAVE_STARTED, Database, SimpleString - -_COMMAND_INFO: Optional[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)) +from fakeredis.model import get_command_info, get_all_commands_info class ServerCommandsMixin: @@ -37,10 +17,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: @staticmethod def _get_command_info(cmd: bytes) -> Optional[List[Any]]: - _load_command_info() - if _COMMAND_INFO is None or cmd not in _COMMAND_INFO: - return None - return _COMMAND_INFO.get(cmd, None) + return get_command_info(cmd) @command((), (bytes,), flags=msgs.FLAG_NO_SCRIPT) def bgsave(self, *args: bytes) -> SimpleString: @@ -100,13 +77,9 @@ def command_info(self, *commands: bytes) -> List[Any]: @command(name="COMMAND COUNT", fixed=(), repeat=()) def command_count(self) -> int: - _load_command_info() - return len(_COMMAND_INFO) if _COMMAND_INFO is not None else 0 + return len(get_all_commands_info()) @command(name="COMMAND", fixed=(), repeat=()) def command_(self) -> List[Any]: - _load_command_info() - if _COMMAND_INFO is None: - return [] - res = [self._get_command_info(cmd) for cmd in _COMMAND_INFO] + res = [self._get_command_info(cmd) for cmd in get_all_commands_info()] return res diff --git a/fakeredis/model/__init__.py b/fakeredis/model/__init__.py index 1c79c346..9b194664 100644 --- a/fakeredis/model/__init__.py +++ b/fakeredis/model/__init__.py @@ -5,6 +5,14 @@ from ._topk import HeavyKeeper from ._zset import ZSet +from ._acl import AccessControlList +from ._command_info import ( + get_all_commands_info, + get_command_info, + get_categories, + get_commands_by_category, +) + __all__ = [ "XStream", "StreamRangeTest", @@ -17,4 +25,9 @@ "HeavyKeeper", "Hash", "ExpiringMembersSet", + "get_all_commands_info", + "get_command_info", + "get_categories", + "get_commands_by_category", + "AccessControlList", ] diff --git a/fakeredis/model/_acl.py b/fakeredis/model/_acl.py new file mode 100644 index 00000000..d4738367 --- /dev/null +++ b/fakeredis/model/_acl.py @@ -0,0 +1,365 @@ +import fnmatch +import hashlib +from typing import Dict, Set, List, Union, Optional, Any + +from fakeredis import _msgs as msgs +from ._command_info import get_commands_by_category, get_command_info +from .._helpers import SimpleError, current_time + + +class Selector: + def __init__(self, command: bytes, allowed: bool, keys: bytes, channels: bytes): + self.command: bytes = command + self.allowed: bool = allowed + self.keys: bytes = keys + self.channels: bytes = channels + + def as_array(self) -> List[bytes]: + return [b"+" if self.allowed else b"-", self.command, b"keys", self.keys, b"channels", self.channels] + + @classmethod + def from_bytes(cls, data: bytes) -> "Selector": + keys = b"" + channels = b"" + command = b"" + allowed = False + data = data.split(b" ") + for item in data: + if item.startswith(b"&"): # channels + channels = item + continue + if item.startswith(b"%RW"): # keys + item = item[3:] + key = item + if key.startswith(b"%"): + key = key[2:] + if key.startswith(b"~"): + keys = item + continue + # command + if item[0] == ord("+") or item[0] == ord("-"): + command = item[1:] + allowed = item[0] == ord("+") + + return cls(command, allowed, keys, channels) + + +class UserAccessControlList: + def __init__(self): + self._passwords: Set[bytes] = set() + self.enabled: bool = True + self._nopass: bool = False + self._key_patterns: Set[bytes] = set() + self._channel_patterns: Set[bytes] = set() + self._commands: Dict[bytes, bool] = {b"@all": False} + self._selectors: Dict[bytes, Selector] = dict() + + def reset(self): + self.enabled = False + self._nopass = False + self._commands = {b"@all": False} + self._passwords.clear() + self._key_patterns.clear() + self._channel_patterns.clear() + self._selectors.clear() + + @staticmethod + def _get_command_info(fields: List[bytes]) -> Optional[List[Any]]: + command = fields[0].lower() + command_info = get_command_info(command) + if not command_info and len(fields) > 1: + command = command + b" " + fields[1].lower() + command_info = get_command_info(command) + return command_info + + def command_allowed(self, command_info: Optional[List[Any]], fields: List[bytes]) -> bool: + res = fields[0].lower() == b"auth" or self._commands.get(fields[0].lower(), False) + res = res or self._commands.get(b"@all", False) + if not command_info: + return res + for category in command_info[6]: + res = res or self._commands.get(category, False) + return res + + def _get_keys(self, command_info: Optional[List[Any]], fields: List[bytes]) -> List[bytes]: + if not command_info: + return [] + first_key, last_key, step = command_info[3:6] + if first_key == 0: + return [] + last_key = (last_key + 1) if last_key >= 0 else last_key + step = step + 1 + return fields[first_key : last_key + 1 : step] + + def keys_not_allowed(self, command_info: Optional[List[Any]], fields: List[bytes]) -> List[bytes]: + if len(self._key_patterns) == 0: + return [] + keys = self._get_keys(command_info, fields) + res = set() + for pat in self._key_patterns: + res = res.union(fnmatch.filter(keys, pat)) + return list(set(keys) - res) + + def channels_not_allowed(self, command_info: Optional[List[Any]], fields: List[bytes]) -> List[bytes]: + if len(self._key_patterns) == 0: + return [] + channels = fields[1:2] + res = set() + for pat in self._channel_patterns: + res = res.union(fnmatch.filter(channels, pat)) + return list(set(channels) - res) + + def set_nopass(self) -> None: + self._nopass = True + self._passwords.clear() + + def check_password(self, password: Optional[bytes]) -> bool: + if self._nopass: + return True + if not password: + return False + password_hex = hashlib.sha256(password).hexdigest().encode() + return password_hex in self._passwords and self.enabled + + def add_password_hex(self, password_hex: bytes) -> None: + self._nopass = False + self._passwords.add(password_hex) + + def add_password(self, password: bytes) -> None: + self._nopass = False + password_hex = hashlib.sha256(password).hexdigest().encode() + self.add_password_hex(password_hex) + + def remove_password_hex(self, password_hex: bytes) -> None: + self._passwords.discard(password_hex) + + def remove_password(self, password: bytes) -> None: + password_hex = hashlib.sha256(password).hexdigest().encode() + self.remove_password_hex(password_hex) + + def add_command_or_category(self, selector: bytes) -> None: + enabled, command = selector[0] == ord("+"), selector[1:] + if command[0] == ord("@"): + self._commands[command] = enabled + category_commands = get_commands_by_category(command[1:]) + for command in category_commands: + if command in self._commands: + del self._commands[command] + else: + self._commands[command] = enabled + + def add_key_pattern(self, key_pattern: bytes) -> None: + self._key_patterns.add(key_pattern) + + def reset_key_patterns(self) -> None: + self._key_patterns.clear() + + def reset_channels_patterns(self): + self._channel_patterns.clear() + + def add_channel_pattern(self, channel_pattern: bytes) -> None: + self._channel_patterns.add(channel_pattern) + + def add_selector(self, selector: bytes) -> None: + selector = Selector.from_bytes(selector) + self._selectors[selector.command] = selector + + def _get_selectors(self) -> List[List[bytes]]: + results = [] + for command, selector in self._selectors.items(): + s = b"-@all " + (b"+" if selector.allowed else b"-") + command + results.append([b"commands", s, b"keys", selector.keys, b"channels", selector.channels]) + return results + + def _get_commands(self) -> List[bytes]: + res = list() + for command, enabled in self._commands.items(): + inc = b"+" if enabled else b"-" + res.append(inc + command) + return res + + def _get_key_patterns(self) -> List[bytes]: + return [b"~" + key_pattern for key_pattern in self._key_patterns] + + def _get_channel_patterns(self): + return [b"&" + channel_pattern for channel_pattern in self._channel_patterns] + + def _get_flags(self) -> List[bytes]: + flags = list() + flags.append(b"on" if self.enabled else b"off") + if self._nopass: + flags.append(b"nopass") + if "*" in self._key_patterns: + flags.append(b"allkeys") + if "*" in self._channel_patterns: + flags.append(b"allchannels") + return flags + + def as_array(self) -> List[Union[bytes, List[bytes]]]: + results: List[Union[bytes, List[bytes]]] = list() + results.extend( + [ + b"flags", + self._get_flags(), + b"passwords", + list(self._passwords), + b"commands", + b" ".join(self._get_commands()), + b"keys", + b" ".join(self._get_key_patterns()), + b"channels", + b" ".join(self._get_channel_patterns()), + b"selectors", + self._get_selectors(), + ] + ) + return results + + def _get_selectors_for_rule(self) -> List[bytes]: + results: List[bytes] = list() + for command, selector in self._selectors.items(): + s = b"-@all " + (b"+" if selector.allowed else b"-") + command + channels = b"resetchannels" + ((b" " + selector.channels) if selector.channels != b"" else b"") + results.append(b"(" + b" ".join([selector.keys, channels, s]) + b")") + return results + + def as_rule(self) -> bytes: + selectors = self._get_selectors_for_rule() + channels = self._get_channel_patterns() + if channels != [b"&*"]: + channels = [b"resetchannels"] + channels + rule_parts: List[bytes] = ( + self._get_flags() + + [b"#" + password for password in self._passwords] + + self._get_commands() + + self._get_key_patterns() + + channels + + selectors + ) + return b" ".join(rule_parts) + + +class AclLogRecord: + def __init__( + self, + count: int, + reason: bytes, + context: bytes, + _object: bytes, + username: bytes, + created_ts: int, + updated_ts: int, + client_info: bytes, + entry_id: int, + ): + self.count: int = count + self.reason: bytes = reason # command, key, channel, or auth + self.context: bytes = context # toplevel, multi, lua, or module + self.object: bytes = _object # resource user couldn't access. AUTH when the reason is auth + self.username: bytes = username + self.created_ts: int = created_ts # milliseconds + self.updated_ts: int = updated_ts + self.client_info: bytes = client_info + self.entry_id: int = entry_id + + def as_array(self) -> List[bytes]: + age_seconds = (current_time() - self.created_ts) / 1000 + return [ + b"count", + str(self.count).encode(), + b"reason", + self.reason, + b"context", + self.context, + b"object", + self.object, + b"username", + self.username, + b"age-seconds", + f"{age_seconds:.3f}".encode(), + b"client-info", + self.client_info, + b"entry-id", + str(self.entry_id).encode(), + b"timestamp-created", + str(self.created_ts).encode(), + b"timestamp-last-updated", + str(self.updated_ts).encode(), + ] + + +class AccessControlList: + + def __init__(self): + self._user_acl: Dict[bytes, UserAccessControlList] = dict() + self._log: List[AclLogRecord] = list() + + def get_users(self) -> List[bytes]: + return list(self._user_acl.keys()) + + def get_user_acl(self, username: bytes) -> UserAccessControlList: + return self._user_acl.setdefault(username, UserAccessControlList()) + + def as_rules(self) -> List[bytes]: + res: List[bytes] = list() + for username, user_acl in self._user_acl.items(): + rule_str = b"user " + username + b" " + user_acl.as_rule() + res.append(rule_str) + return res + + def del_user(self, username: bytes) -> None: + self._user_acl.pop(username, None) + + def reset_log(self) -> None: + self._log.clear() + + def log(self, count: int) -> List[List[bytes]]: + if count > len(self._log) or count < 0: + count = 0 + res = [x.as_array() for x in self._log[-count:]] + res.reverse() + return res + + def add_log_record( + self, + reason: bytes, + context: bytes, + _object: bytes, + username: bytes, + client_info: bytes, + ) -> None: + if len(self._log) > 0: + last_entry = self._log[-1] + if ( + last_entry.reason == reason + and last_entry.context == context + and last_entry.object == _object + and last_entry.username == username + ): + last_entry.count += 1 + last_entry.updated_ts = current_time() + return + entry = AclLogRecord( + 1, reason, context, _object, username, current_time(), current_time(), client_info, len(self._log) + 1 + ) + self._log.append(entry) + + def validate_command(self, username: bytes, client_info: bytes, fields: List[bytes]): + if username not in self._user_acl: + return + user_acl = self._user_acl[username] + if not user_acl.enabled: + raise SimpleError("User disabled") + command_info = UserAccessControlList._get_command_info(fields) + if not user_acl.command_allowed(command_info, fields): + self.add_log_record(b"command", b"toplevel", fields[0], username, client_info) + raise SimpleError(msgs.NO_PERMISSION_ERROR.format(username.decode(), fields[0].lower().decode())) + keys_not_allowed = user_acl.keys_not_allowed(command_info, fields) + if len(keys_not_allowed) > 0: + self.add_log_record(b"key", b"toplevel", keys_not_allowed[0], username, client_info) + raise SimpleError(msgs.NO_PERMISSION_KEY_ERROR) + if b"@pubsub" in command_info[6]: + channels_not_allowed = user_acl.channels_not_allowed(command_info, fields) + if len(channels_not_allowed) > 0: + self.add_log_record(b"channel", b"toplevel", channels_not_allowed[0], username, client_info) + raise SimpleError(msgs.NO_PERMISSION_CHANNEL_ERROR) diff --git a/fakeredis/model/_command_info.py b/fakeredis/model/_command_info.py new file mode 100644 index 00000000..e6e7579c --- /dev/null +++ b/fakeredis/model/_command_info.py @@ -0,0 +1,60 @@ +import json +import os +from typing import Optional, Dict, List, Any, Union + +_COMMAND_INFO: Optional[Dict[bytes, List[Any]]] = None + + +def _encode_obj(obj: Any) -> Any: + if isinstance(obj, str): + return obj.encode() + if isinstance(obj, list): + return [_encode_obj(x) for x in obj] + if isinstance(obj, dict): + return {_encode_obj(k): _encode_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 = _encode_obj(json.load(f)) + + +def get_all_commands_info() -> Dict[bytes, List[Any]]: + _load_command_info() + return _COMMAND_INFO + + +def get_command_info(cmd: bytes) -> Optional[List[Any]]: + _load_command_info() + if _COMMAND_INFO is None or cmd not in _COMMAND_INFO: + return None + return _COMMAND_INFO.get(cmd, None) + + +def get_categories() -> List[bytes]: + _load_command_info() + if _COMMAND_INFO is None: + return [] + categories = set() + for info in _COMMAND_INFO.values(): + categories.update(info[6]) + categories = {x[1:] for x in categories} + return list(categories) + + +def get_commands_by_category(category: Union[str, bytes]) -> List[bytes]: + _load_command_info() + if _COMMAND_INFO is None: + return [] + if isinstance(category, str): + category = category.encode() + if category[0] != b"@": + category = b"@" + category + commands = [] + for cmd, info in _COMMAND_INFO.items(): + if category in info[6]: + commands.append(cmd) + return commands diff --git a/fakeredis/model/_timeseries_model.py b/fakeredis/model/_timeseries_model.py index a224bbf4..589dbb8e 100644 --- a/fakeredis/model/_timeseries_model.py +++ b/fakeredis/model/_timeseries_model.py @@ -5,7 +5,6 @@ class TimeSeries: - def __init__( self, name: bytes, diff --git a/poetry.lock b/poetry.lock index f2f8de98..1a82d129 100644 --- a/poetry.lock +++ b/poetry.lock @@ -511,13 +511,13 @@ dev = ["pyTest", "pyTest-cov"] [[package]] name = "hypothesis" -version = "6.122.3" +version = "6.122.5" description = "A library for property-based testing" optional = false python-versions = ">=3.9" files = [ - {file = "hypothesis-6.122.3-py3-none-any.whl", hash = "sha256:f0f57036d3b95b979491602b32c95b6725c3af678cccb6165d8de330857f3c83"}, - {file = "hypothesis-6.122.3.tar.gz", hash = "sha256:f4c927ce0ec739fa6266e4572949d0b54e24a14601a2bc5fec8f78e16af57918"}, + {file = "hypothesis-6.122.5-py3-none-any.whl", hash = "sha256:c50b104d9d5163ebdeb09ccd93626343664942570bcb067c41adf10394a81caf"}, + {file = "hypothesis-6.122.5.tar.gz", hash = "sha256:e0994f04331251d51e18040f497c839a52b37669b422fe4cfef85a54d41405bf"}, ] [package.dependencies] @@ -797,49 +797,49 @@ files = [ [[package]] name = "mypy" -version = "1.13.0" +version = "1.14.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, + {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, + {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, + {file = "mypy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1628c5c3ce823d296e41e2984ff88c5861499041cb416a8809615d0c1f41740e"}, + {file = "mypy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fadb29b77fc14a0dd81304ed73c828c3e5cde0016c7e668a86a3e0dfc9f3af3"}, + {file = "mypy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fa76988dc760da377c1e5069200a50d9eaaccf34f4ea18428a3337034ab5a44"}, + {file = "mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a"}, + {file = "mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc"}, + {file = "mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015"}, + {file = "mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb"}, + {file = "mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc"}, + {file = "mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd"}, + {file = "mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1"}, + {file = "mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63"}, + {file = "mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d"}, + {file = "mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba"}, + {file = "mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741"}, + {file = "mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7"}, + {file = "mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8"}, + {file = "mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc"}, + {file = "mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f"}, + {file = "mypy-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b16738b1d80ec4334654e89e798eb705ac0c36c8a5c4798496cd3623aa02286"}, + {file = "mypy-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10065fcebb7c66df04b05fc799a854b1ae24d9963c8bb27e9064a9bdb43aa8ad"}, + {file = "mypy-1.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb7d683fa6bdecaa106e8368aa973ecc0ddb79a9eaeb4b821591ecd07e9e03c"}, + {file = "mypy-1.14.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3498cb55448dc5533e438cd13d6ddd28654559c8c4d1fd4b5ca57a31b81bac01"}, + {file = "mypy-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c7b243408ea43755f3a21a0a08e5c5ae30eddb4c58a80f415ca6b118816e60aa"}, + {file = "mypy-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14117b9da3305b39860d0aa34b8f1ff74d209a368829a584eb77524389a9c13e"}, + {file = "mypy-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af98c5a958f9c37404bd4eef2f920b94874507e146ed6ee559f185b8809c44cc"}, + {file = "mypy-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b343a1d3989547024377c2ba0dca9c74a2428ad6ed24283c213af8dbb0710b"}, + {file = "mypy-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cdb5563c1726c85fb201be383168f8c866032db95e1095600806625b3a648cb7"}, + {file = "mypy-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e925649c1ee0a79aa7448baf2668d81cc287dc5782cff6a04ee93f40fb8d3f"}, + {file = "mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab"}, + {file = "mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] diff --git a/pyproject.toml b/pyproject.toml index 1ad1eb25..8a646ccb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,7 @@ exclude = ''' | build | dist )/ + | .*/\.hypothesis/.* | .*/__pycache__/.* | /fakeredis/commands.json ) diff --git a/redis-conf/redis-stack.conf b/redis-conf/redis-stack.conf new file mode 100644 index 00000000..efc6cecc --- /dev/null +++ b/redis-conf/redis-stack.conf @@ -0,0 +1,2 @@ +aclfile /etc/redis/users.acl + diff --git a/redis-conf/users.acl b/redis-conf/users.acl new file mode 100644 index 00000000..16a5b4f2 --- /dev/null +++ b/redis-conf/users.acl @@ -0,0 +1 @@ +user default on nopass sanitize-payload ~* &* +@all diff --git a/scripts/generate_command_info.py b/scripts/generate_command_info.py index 7eaed586..f9dea2bd 100644 --- a/scripts/generate_command_info.py +++ b/scripts/generate_command_info.py @@ -16,6 +16,7 @@ } that is used for the `COMMAND` redis command. """ + import json import os from typing import Any, List, Dict @@ -28,10 +29,10 @@ 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') - if 'eval' not in res: - raise ValueError('Make sure lupa is installed to get accurate documentation') + if "json.type" not in res: + raise ValueError("Make sure jsonpath_ng is installed to get accurate documentation") + if "eval" not in res: + raise ValueError("Make sure lupa is installed to get accurate documentation") return res @@ -64,17 +65,17 @@ def get_command_info(cmd_name: str, all_commands: Dict[str, Any]) -> List[Any]: 9 Key specifications (as of Redis 7.0) 10 Subcommands (as of Redis 7.0) """ - print(f'Command {cmd_name}') + print(f"Command {cmd_name}") cmd_info = all_commands[cmd_name] - 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) + 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 = [get_command_info(cmd, all_commands) - for cmd in all_commands - if cmd_name != cmd and cmd.startswith(cmd_name)] # todo + subcommands = [ + get_command_info(cmd, all_commands) for cmd in all_commands if cmd_name != cmd and cmd.startswith(cmd_name) + ] # todo res = [ - cmd_name.lower().replace(' ', '|'), + cmd_name.lower().replace(" ", "|"), cmd_info.get("arity", -1), cmd_info.get("command_flags", []), first_key, @@ -88,15 +89,23 @@ def get_command_info(cmd_name: str, all_commands: Dict[str, Any]) -> List[Any]: return res -if __name__ == '__main__': +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 or ' ' in cmd: + if cmd not in implemented: continue command_info_dict[cmd] = get_command_info(cmd, cmds) + subcommand = cmd.split(" ") + if len(subcommand) > 1: + ( + command_info_dict.setdefault(subcommand[0], [subcommand[0], -1, [], 0, 0, 0, [], [], [], []])[ + 9 + ].append(command_info_dict[cmd]) + ) + print(command_info_dict[cmd]) - with open(os.path.join(os.path.dirname(__file__), '..', 'fakeredis', 'commands.json'), 'w') as f: + with open(os.path.join(os.path.dirname(__file__), "..", "fakeredis", "commands.json"), "w") as f: json.dump(command_info_dict, f) diff --git a/test/conftest.py b/test/conftest.py index f0c0291e..d65e450b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,4 +1,4 @@ -from typing import Callable, Tuple, Union, Optional +from typing import Callable, Tuple, Union, Optional, Type import pytest import pytest_asyncio @@ -7,6 +7,8 @@ import fakeredis from fakeredis._server import _create_version +ServerDetails = Type[Tuple[str, Union[None, Tuple[int, ...]]]] + def _check_lua_module_supported() -> bool: redis = fakeredis.FakeRedis(lua_modules={"cjson"}) @@ -18,7 +20,7 @@ def _check_lua_module_supported() -> bool: @pytest_asyncio.fixture(scope="session") -def real_redis_version() -> Tuple[str, Union[None, Tuple[int, ...]]]: +def real_server_details() -> ServerDetails: """Returns server's version or None if server is not running""" client = None try: @@ -39,8 +41,8 @@ def real_redis_version() -> Tuple[str, Union[None, Tuple[int, ...]]]: @pytest_asyncio.fixture(name="fake_server") -def _fake_server(request, real_redis_version) -> fakeredis.FakeServer: - server_type, _ = real_redis_version +def _fake_server(request, real_server_details: ServerDetails) -> fakeredis.FakeServer: + server_type, _ = real_server_details min_server_marker = request.node.get_closest_marker("min_server") server_version = min_server_marker.args[0] if min_server_marker else "6.2" server = fakeredis.FakeServer(server_type=server_type, version=server_version) @@ -49,7 +51,7 @@ def _fake_server(request, real_redis_version) -> fakeredis.FakeServer: @pytest_asyncio.fixture -def r(request, create_redis) -> redis.Redis: +def r(request, create_redis: Callable[[int], redis.Redis]) -> redis.Redis: rconn = create_redis(db=2) connected = request.node.get_closest_marker("disconnected") is None if connected: @@ -77,7 +79,7 @@ def _marker_version_value(request, marker_name: str): ) def _create_connection(request) -> Callable[[int], redis.Redis]: cls_name = request.param - server_type, server_version = request.getfixturevalue("real_redis_version") + server_type, server_version = request.getfixturevalue("real_server_details") if not cls_name.startswith("Fake") and not server_version: pytest.skip("Redis is not running") unsupported_server_types = request.node.get_closest_marker("unsupported_server_types") @@ -112,7 +114,7 @@ def factory(db=2): params=[pytest.param("fake", marks=pytest.mark.fake), pytest.param("real", marks=pytest.mark.real)], ) async def _req_aioredis2(request) -> redis.asyncio.Redis: - server_type, server_version = request.getfixturevalue("real_redis_version") + server_type, server_version = request.getfixturevalue("real_server_details") if request.param != "fake" and not server_version: pytest.skip("Redis is not running") diff --git a/test/test_internals/test_acl_save_load.py b/test/test_internals/test_acl_save_load.py new file mode 100644 index 00000000..2d3760e9 --- /dev/null +++ b/test/test_internals/test_acl_save_load.py @@ -0,0 +1,59 @@ +import os + +from fakeredis import FakeServer, FakeStrictRedis + + +def test_acl_save_load(): + acl_filename = b"./users.acl" + server = FakeServer(config={b"aclfile": acl_filename}) + r = FakeStrictRedis(server=server) + username = "fakeredis-user" + assert r.acl_setuser( + username, + enabled=True, + reset=True, + passwords=["+pass1", "+pass2"], + categories=["+set", "+@hash", "-geo"], + commands=["+get", "+mget", "-hset"], + keys=["cache:*", "objects:*"], + channels=["message:*"], + selectors=[("+set", "%W~app*"), ("+get", "%RW~app* &x"), ("-hset", "%W~app*")], + ) + r.acl_save() + + # assert acl file contains all data + with open(acl_filename, "r") as f: + lines = f.readlines() + assert len(lines) == 1 + user_rule = lines[0] + assert user_rule.startswith("user fakeredis-user") + assert "nopass" not in user_rule + assert "#e6c3da5b206634d7f3f3586d747ffdb36b5c675757b380c6a5fe5c570c714349" in user_rule + assert "#1ba3d16e9881959f8c9a9762854f72c6e6321cdd44358a10a4e939033117eab9" in user_rule + assert "on" in user_rule + assert "~cache:*" in user_rule + assert "~objects:*" in user_rule + assert "resetchannels &message:*" in user_rule + assert "(%W~app* resetchannels -@all +set)" in user_rule + assert "(~app* resetchannels &x -@all +get)" in user_rule + assert "(%W~app* resetchannels -@all -hset)" in user_rule + + # assert acl file is loaded correctly + server2 = FakeServer(config={b"aclfile": acl_filename}) + r2 = FakeStrictRedis(server=server2) + r2.acl_load() + rules = r2.acl_list() + user_rule = next(filter(lambda x: x.startswith(f"user {username}"), rules), None) + assert user_rule.startswith("user fakeredis-user") + assert "nopass" not in user_rule + assert "#e6c3da5b206634d7f3f3586d747ffdb36b5c675757b380c6a5fe5c570c714349" in user_rule + assert "#1ba3d16e9881959f8c9a9762854f72c6e6321cdd44358a10a4e939033117eab9" in user_rule + assert "on" in user_rule + assert "~cache:*" in user_rule + assert "~objects:*" in user_rule + assert "resetchannels &message:*" in user_rule + assert "(%W~app* resetchannels -@all +set)" in user_rule + assert "(~app* resetchannels &x -@all +get)" in user_rule + assert "(%W~app* resetchannels -@all -hset)" in user_rule + + os.remove(acl_filename) diff --git a/test/test_internals/test_init_args.py b/test/test_internals/test_init_args.py index 06cf5a0a..0e472fbf 100644 --- a/test/test_internals/test_init_args.py +++ b/test/test_internals/test_init_args.py @@ -28,18 +28,38 @@ def test_host_init_arg(self): db.set("foo", "bar") assert db.get("foo") == b"bar" + def test_with_user_password(self): + username = "fakeredis-user" + password = "fakeredis-password" + db = fakeredis.FakeStrictRedis(host="localhost") + db.acl_setuser(username, enabled=True, passwords=[f"+{password}"], commands=["+set", "+get"]) + + db = fakeredis.FakeStrictRedis(host="localhost", username=username, password=password) + db.set("foo", "bar") + assert db.get("foo") == b"bar" + def test_from_url(self): db = fakeredis.FakeStrictRedis.from_url("redis://localhost:6390/0") db.set("foo", "bar") assert db.get("foo") == b"bar" def test_from_url_user(self): - db = fakeredis.FakeStrictRedis.from_url("redis://user@localhost:6390/0") + username = "fakeredis-user" + db = fakeredis.FakeStrictRedis(host="localhost", port=6390, db=0) + db.acl_setuser(username, enabled=True, nopass=True, commands=["+set", "+get"]) + + db = fakeredis.FakeStrictRedis.from_url(f"redis://{username}@localhost:6390/0") db.set("foo", "bar") assert db.get("foo") == b"bar" def test_from_url_user_password(self): - db = fakeredis.FakeStrictRedis.from_url("redis://user:password@localhost:6390/0") + username = "fakeredis-user" + password = "fakeredis-password" + server = fakeredis.FakeServer() + db = fakeredis.FakeStrictRedis(host="localhost", port=6390, server=server) + db.acl_setuser(username, enabled=True, passwords=[f"+{password}"], commands=["+set", "+get"]) + + db = fakeredis.FakeStrictRedis.from_url(f"redis://{username}:{password}@localhost:6390/0", server=server) db.set("foo", "bar") assert db.get("foo") == b"bar" diff --git a/test/test_mixins/test_acl_commands.py b/test/test_mixins/test_acl_commands.py new file mode 100644 index 00000000..c0c58ee3 --- /dev/null +++ b/test/test_mixins/test_acl_commands.py @@ -0,0 +1,403 @@ +import pytest +import redis +from redis import exceptions + +from fakeredis.model import get_categories, get_commands_by_category +from test import testtools +from test.conftest import ServerDetails + +pytestmark = [] +pytestmark.extend([pytest.mark.min_server("7"), testtools.run_test_if_redispy_ver("gte", "5")]) + +_VALKEY_UNSUPPORTED_COMMANDS = { + "hexpiretime", + "hexpireat", + "hpexpireat", + "hexpire", + "hpttl", + "hpexpire", + "hpexpiretime", + "httl", +} + + +def test_acl_cat(r: redis.Redis, real_server_details: ServerDetails): + categories = get_categories() + categories = [cat.decode() for cat in categories] + assert set(r.acl_cat()) == set(categories) + for cat in categories: + commands = get_commands_by_category(cat) + commands = {cmd.decode() for cmd in commands} + assert len(commands) > 0 + commands.discard("hpersist") + if real_server_details[0] == "valkey": + commands = commands - _VALKEY_UNSUPPORTED_COMMANDS + commands = {cmd.replace(" ", "|") for cmd in commands} + diff = set(commands) - set(r.acl_cat(cat)) + assert len(diff) == 0, f"Commands not found in category {cat}: {diff}" + + +def test_acl_genpass(r: redis.Redis): + assert len(r.acl_genpass()) == 64 + assert len(r.acl_genpass(128)) == 32 + + +def test_auth(r: redis.Redis): + with pytest.raises(redis.AuthenticationError): + r.auth("some_password") + + with pytest.raises(redis.AuthenticationError): + r.auth("some_password", "some_user") + + # first, test for default user (`username` is supposed to be optional) + default_username = "default" + temp_pass = "temp_pass" + r.config_set("requirepass", temp_pass) + + assert r.auth(temp_pass, default_username) is True + assert r.auth(temp_pass) is True + r.config_set("requirepass", "") + + # test for other users + username = "fakeredis-authuser" + + assert r.acl_setuser(username, enabled=True, passwords=["+strong_password"], commands=["+acl"]) + + assert r.auth(username=username, password="strong_password") is True + + with pytest.raises(redis.AuthenticationError): + r.auth(username=username, password="wrong_password") + + +def test_acl_list(r: redis.Redis): + username = "fakeredis-user" + r.acl_deluser(username) + start = r.acl_list() + + assert r.acl_setuser(username, enabled=False, reset=True) + users = r.acl_list() + assert len(users) == len(start) + 1 + assert r.acl_setuser( + username, + enabled=True, + reset=True, + passwords=["+pass1", "+pass2"], + categories=["+set", "+@hash", "-geo"], + commands=["+get", "+mget", "-hset"], + keys=["cache:*", "objects:*"], + channels=["message:*"], + selectors=[("+set", "%W~app*"), ("+get", "%RW~app* &x"), ("-hset", "%W~app*")], + ) + rules = r.acl_list() + user_rule = next(filter(lambda x: x.startswith(f"user {username}"), rules), None) + assert user_rule is not None + + assert "nopass" not in user_rule + assert "#e6c3da5b206634d7f3f3586d747ffdb36b5c675757b380c6a5fe5c570c714349" in user_rule + assert "#1ba3d16e9881959f8c9a9762854f72c6e6321cdd44358a10a4e939033117eab9" in user_rule + assert "on" in user_rule + assert "~cache:*" in user_rule + assert "~objects:*" in user_rule + assert "resetchannels &message:*" in user_rule + assert "(%W~app* resetchannels -@all +set)" in user_rule + assert "(~app* resetchannels &x -@all +get)" in user_rule + assert "(%W~app* resetchannels -@all -hset)" in user_rule + + +def test_acl_getuser_setuser(r: redis.Redis): + username = "fakeredis-user" + + # test enabled=False + assert r.acl_setuser(username, enabled=False, reset=True) + acl = r.acl_getuser(username) + assert acl["categories"] == ["-@all"] + assert acl["commands"] == [] + assert acl["keys"] == [] + assert acl["passwords"] == [] + assert "off" in acl["flags"] + assert acl["enabled"] is False + + # test nopass=True + assert r.acl_setuser(username, enabled=True, reset=True, nopass=True) + acl = r.acl_getuser(username) + assert acl["categories"] == ["-@all"] + assert acl["commands"] == [] + assert acl["keys"] == [] + assert acl["passwords"] == [] + assert "on" in acl["flags"] + assert "nopass" in acl["flags"] + assert acl["enabled"] is True + + # test all args + assert r.acl_setuser( + username, + enabled=True, + reset=True, + passwords=["+pass1", "+pass2"], + categories=["+set", "+@hash", "-@geo"], + commands=["+get", "+mget", "-hset"], + keys=["cache:*", "objects:*"], + ) + acl = r.acl_getuser(username) + assert set(acl["categories"]) == {"+@hash", "+@set", "-@all", "-@geo"} + assert set(acl["commands"]) == {"+get", "+mget", "-hset"} + assert acl["enabled"] is True + assert "on" in acl["flags"] + assert set(acl["keys"]) == {"~cache:*", "~objects:*"} + assert len(acl["passwords"]) == 2 + + # # test reset=False keeps existing ACL and applies new ACL on top + assert r.acl_setuser( + username, + enabled=True, + reset=True, + passwords=["+pass1"], + categories=["+@set"], + commands=["+get"], + keys=["cache:*"], + ) + assert r.acl_setuser( + username, + enabled=True, + passwords=["+pass2"], + categories=["+@hash"], + commands=["+mget"], + keys=["objects:*"], + ) + acl = r.acl_getuser(username) + assert set(acl["commands"]) == {"+get", "+mget"} + assert acl["enabled"] is True + assert "on" in acl["flags"] + assert set(acl["keys"]) == {"~cache:*", "~objects:*"} + assert len(acl["passwords"]) == 2 + + # # test removal of passwords + assert r.acl_setuser(username, enabled=True, reset=True, passwords=["+pass1", "+pass2"]) + assert len(r.acl_getuser(username)["passwords"]) == 2 + assert r.acl_setuser(username, enabled=True, passwords=["-pass2"]) + assert len(r.acl_getuser(username)["passwords"]) == 1 + + # # Resets and tests that hashed passwords are set properly. + hashed_password = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + assert r.acl_setuser(username, enabled=True, reset=True, hashed_passwords=["+" + hashed_password]) + acl = r.acl_getuser(username) + assert acl["passwords"] == [hashed_password] + + # test removal of hashed passwords + assert r.acl_setuser( + username, + enabled=True, + reset=True, + hashed_passwords=["+" + hashed_password], + passwords=["+pass1"], + ) + assert len(r.acl_getuser(username)["passwords"]) == 2 + assert r.acl_setuser(username, enabled=True, hashed_passwords=["-" + hashed_password]) + assert len(r.acl_getuser(username)["passwords"]) == 1 + + # # test selectors + assert r.acl_setuser( + username, + enabled=True, + reset=True, + passwords=["+pass1", "+pass2"], + categories=["+set", "+@hash", "-geo"], + commands=["+get", "+mget", "-hset"], + keys=["cache:*", "objects:*"], + channels=["message:*"], + selectors=[("+set", "%W~app*")], + ) + acl = r.acl_getuser(username) + assert set(acl["categories"]) == {"+@hash", "+@set", "-@all", "-@geo"} + assert set(acl["commands"]) == {"+get", "+mget", "-hset"} + assert acl["enabled"] is True + assert "on" in acl["flags"] + assert set(acl["keys"]) == {"~cache:*", "~objects:*"} + assert len(acl["passwords"]) == 2 + assert set(acl["channels"]) == {"&message:*"} + r.acl_deluser(username) + assert acl["selectors"] == [["commands", "-@all +set", "keys", "%W~app*", "channels", ""]] + + assert r.acl_setuser( + username, + enabled=True, + reset=True, + passwords=["+pass1", "+pass2"], + categories=["+set", "+@hash", "-geo"], + commands=["+get", "+mget", "-hset"], + keys=["cache:*", "objects:*"], + channels=["message:*"], + selectors=[("+set", "%W~app*"), ("+get", "%RW~app* &x"), ("-hset", "%W~app*")], + ) + acl = r.acl_getuser(username) + assert acl["selectors"] == [ + ["commands", "-@all +set", "keys", "%W~app*", "channels", ""], + ["commands", "-@all +get", "keys", "~app*", "channels", "&x"], + ["commands", "-@all -hset", "keys", "%W~app*", "channels", ""], + ] + + +def test_acl_users(r: redis.Redis): + username = "fakeredis-user" + r.acl_deluser(username) + start = r.acl_users() + + assert r.acl_setuser(username, enabled=False, reset=True) + users = r.acl_users() + assert len(users) == len(start) + 1 + assert username in users + + +def test_acl_whoami(r: redis.Redis): + # first, test for default user (`username` is supposed to be optional) + default_username = "default" + temp_pass = "temp_pass" + r.config_set("requirepass", temp_pass) + + assert r.auth(temp_pass, default_username) is True + assert r.auth(temp_pass) is True + assert r.acl_whoami() == default_username + + username = "fakeredis-authuser" + r.acl_deluser(username) + r.acl_setuser(username, enabled=True, passwords=["+strong_password"], commands=["+acl"]) + r.auth(username=username, password="strong_password") + assert r.acl_whoami() == username + assert r.auth(temp_pass, default_username) is True + r.config_set("requirepass", "") + + +def test_acl_log_auth_exist(r: redis.Redis, request): + username = "fredis-py-user" + + def teardown(): + r.auth("", username="default") + r.acl_deluser(username) + + request.addfinalizer(teardown) + r.acl_setuser( + username, + enabled=True, + reset=True, + commands=["+get", "+set", "+select"], + keys=["cache:*"], + passwords=["+pass1"], + ) + r.acl_log_reset() + + with pytest.raises(exceptions.AuthenticationError): + r.auth("xxx", username=username) + r.auth("pass1", username=username) + + # Valid operation and key + assert r.set("cache:0", 1) + assert r.get("cache:0") == b"1" + + r.auth("", "default") + log = r.acl_log() + assert isinstance(log, list) + assert len(log) == 1 + assert len(r.acl_log(count=1)) == 1 + assert isinstance(log[0], dict) + + auth_record = log[0] + assert auth_record["username"] == username + assert auth_record["reason"] == "auth" + assert auth_record["object"] == "AUTH" + + +def test_acl_log_invalid_key(r: redis.Redis, request): + username = "fredis-py-user" + + def teardown(): + r.auth("", username="default") + r.acl_deluser(username) + + request.addfinalizer(teardown) + r.acl_setuser( + username, + enabled=True, + reset=True, + commands=["+get", "+set", "+select"], + keys=["cache:*"], + nopass=True, + ) + r.acl_log_reset() + + r.auth("", username=username) + + # Valid operation and key + assert r.set("cache:0", 1) + assert r.get("cache:0") == b"1" + + # Invalid operation + with pytest.raises(exceptions.NoPermissionError) as ctx: + r.hset("cache:0", "hkey", "hval") + + assert str(ctx.value) == "User fredis-py-user has no permissions to run the 'hset' command" + + # Invalid key + with pytest.raises(exceptions.NoPermissionError) as ctx: + r.get("violated_cache:0") + + assert str(ctx.value) == "No permissions to access a key" + + r.auth("", "default") + log = r.acl_log() + assert isinstance(log, list) + assert len(log) == 2 + assert len(r.acl_log(count=1)) == 1 + assert isinstance(log[0], dict) + + bad_key_record = log[0] + assert bad_key_record["username"] == username + assert bad_key_record["reason"] == "key" + assert bad_key_record["object"] == "violated_cache:0" + + bad_command_record = log[1] + assert bad_command_record["username"] == username + assert bad_command_record["reason"] == "command" + assert bad_command_record["object"].lower() == "hset" + + +def test_acl_log_invalid_channel(r: redis.Redis, request): + username = "fredis-py-user" + + def teardown(): + r.auth("", username="default") + r.acl_deluser(username) + + request.addfinalizer(teardown) + r.acl_setuser( + username, + enabled=True, + reset=True, + commands=["+get", "+set", "+select", "+publish"], + channels=["message:*"], + keys=["cache:*"], + nopass=True, + ) + r.acl_log_reset() + + r.auth("", username=username) + + # Valid operation and key + assert r.set("cache:0", 1) + assert r.get("cache:0") == b"1" + + with pytest.raises(exceptions.NoPermissionError) as ctx: + r.publish("invalid-channel", "message") + + assert str(ctx.value) == "No permissions to access a channel" + + r.auth("", "default") + log = r.acl_log() + assert isinstance(log, list) + assert len(log) == 1 + assert len(r.acl_log(count=1)) == 1 + assert isinstance(log[0], dict) + + log_record = r.acl_log(count=1)[0] + assert log_record["username"] == username + assert log_record["reason"] == "channel" + assert log_record["object"].lower() == "invalid-channel"