Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cunla/fakeredis-py
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.11.1
Choose a base ref
...
head repository: cunla/fakeredis-py
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: refs/heads/master
Choose a head ref

Commits on Apr 27, 2023

  1. Copy the full SHA
    e69c5d0 View commit details
  2. Add test for xread

    cunla committed Apr 27, 2023
    Copy the full SHA
    7fe686f View commit details
  3. Error msgs fix (#143)

    cunla authored Apr 27, 2023
    Copy the full SHA
    dfb686d View commit details

Commits on May 1, 2023

  1. Update rule (#144)

    cunla authored May 1, 2023
    Copy the full SHA
    bb8e91e View commit details

Commits on May 2, 2023

  1. Add codeowners

    cunla authored May 2, 2023
    Copy the full SHA
    3e2f439 View commit details
  2. Update dependencies

    cunla committed May 2, 2023
    Copy the full SHA
    97674e2 View commit details

Commits on May 8, 2023

  1. Implement XREAD (#147)

    cunla authored May 8, 2023
    Copy the full SHA
    f3fe513 View commit details
  2. Update rule

    cunla committed May 8, 2023
    Copy the full SHA
    73f7d19 View commit details
  3. Update rule

    cunla committed May 8, 2023
    Copy the full SHA
    737d027 View commit details

Commits on May 9, 2023

  1. Add async documentation

    cunla authored May 9, 2023
    Copy the full SHA
    0f81ec3 View commit details
  2. Removing some test scenarios

    cunla committed May 9, 2023
    Copy the full SHA
    78a1f39 View commit details
  3. Copy the full SHA
    4a0d40d View commit details
  4. Fix gha

    cunla committed May 9, 2023
    Copy the full SHA
    55ff23d View commit details
  5. Fix gha

    cunla committed May 9, 2023
    Copy the full SHA
    93c361f View commit details
  6. Fix gha

    cunla committed May 9, 2023
    Copy the full SHA
    620e22e View commit details
  7. Copy the full SHA
    21d68bb View commit details
  8. Add 5.0.0b3 support

    cunla committed May 9, 2023
    Copy the full SHA
    58c8d6e View commit details
  9. Update README.md

    cunla authored May 9, 2023
    Copy the full SHA
    124cace View commit details
  10. Fix documentation

    cunla committed May 9, 2023
    Copy the full SHA
    e9294bf View commit details
  11. Fix documentation

    cunla committed May 9, 2023
    Copy the full SHA
    99dbc5d View commit details
  12. update changelog

    cunla committed May 9, 2023
    Copy the full SHA
    7add100 View commit details
  13. Adding state for scan commands (#99)

    Fix #95
    
    Co-authored-by: Daniel M <cunla@users.noreply.github.com>
    matanper and cunla authored May 9, 2023
    Copy the full SHA
    cf3f315 View commit details
  14. Update scan behavior

    cunla committed May 9, 2023
    Copy the full SHA
    52e921e View commit details
  15. Mark test as supposed to fail

    cunla committed May 9, 2023
    Copy the full SHA
    fac20f5 View commit details
  16. Fix sortedset

    cunla committed May 9, 2023
    Copy the full SHA
    1aacd8d View commit details
  17. Move scan tests

    cunla committed May 9, 2023
    Copy the full SHA
    97f406e View commit details
  18. Move scan tests

    cunla committed May 9, 2023
    Copy the full SHA
    a0ca413 View commit details
  19. Move scan tests

    cunla committed May 9, 2023
    Copy the full SHA
    c09d6f9 View commit details

Commits on May 10, 2023

  1. Scan documentation

    cunla committed May 10, 2023
    Copy the full SHA
    32c8d25 View commit details
  2. Scan documentation

    cunla committed May 10, 2023
    Copy the full SHA
    005388b View commit details
  3. Fix scan

    cunla committed May 10, 2023
    Copy the full SHA
    da26cd9 View commit details

Commits on May 11, 2023

  1. Update changelog.md

    cunla committed May 11, 2023
    Copy the full SHA
    c9d5414 View commit details
  2. Update version

    cunla committed May 11, 2023
    Copy the full SHA
    053ae9e View commit details

Commits on May 19, 2023

  1. Fix xadd timestamp

    * Fixed xadd timestamp
    * Add test for redis7+
    Fix #151
    ---------
    
    Co-authored-by: Daniel M <cunla@users.noreply.github.com>
    alexporter8013 and cunla authored May 19, 2023
    Copy the full SHA
    cc8709f View commit details
  2. Fix security issues

    cunla committed May 19, 2023
    Copy the full SHA
    01b2cac View commit details
  3. Fix test

    cunla committed May 19, 2023
    Copy the full SHA
    57cd457 View commit details
  4. Add typing to tests

    cunla committed May 19, 2023
    Copy the full SHA
    84da87b View commit details
  5. Add python versions to tests

    cunla committed May 19, 2023
    Copy the full SHA
    30c30ce View commit details
  6. Simplify code

    cunla committed May 19, 2023
    Copy the full SHA
    24d1b6c View commit details
  7. Fix conftest.py

    cunla committed May 19, 2023
    Copy the full SHA
    a641ceb View commit details
  8. Fix conftest.py

    cunla committed May 19, 2023
    Copy the full SHA
    fdf3ecf View commit details
  9. Fix config

    cunla committed May 19, 2023
    Copy the full SHA
    5517976 View commit details
  10. Fix config

    cunla committed May 19, 2023
    Copy the full SHA
    6ce5dea View commit details

Commits on May 21, 2023

  1. Update changelog

    cunla committed May 21, 2023
    Copy the full SHA
    8e90639 View commit details
  2. Update changelog

    cunla committed May 21, 2023
    Copy the full SHA
    66e7a37 View commit details
  3. Implement XDEL (#153)

    Fix #150
    cunla authored May 21, 2023
    Copy the full SHA
    814a7b0 View commit details
  4. Update changelog

    cunla committed May 21, 2023
    Copy the full SHA
    749ab40 View commit details

Commits on May 22, 2023

  1. Fix async tests #154

    cunla committed May 22, 2023
    Copy the full SHA
    ced6ff6 View commit details
  2. Update documentation

    cunla committed May 22, 2023
    Copy the full SHA
    9642aac View commit details
  3. Update gh-issues script

    cunla committed May 22, 2023
    Copy the full SHA
    594ad97 View commit details
Showing with 21,351 additions and 11,070 deletions.
  1. +0 −2 .flake8
  2. +1 −0 .github/CODEOWNERS
  3. +100 −46 .github/CONTRIBUTING.md
  4. +1 −0 .github/FUNDING.yml
  5. +1 −1 .github/actions/test-coverage/action.yml
  6. +16 −0 .github/dependabot.yml
  7. +35 −0 .github/workflows/publish-documentation.yml
  8. +35 −0 .github/workflows/publish-pypi.yml
  9. +0 −39 .github/workflows/publish.yml
  10. +79 −0 .github/workflows/test-dragonfly.yml
  11. +40 −29 .github/workflows/test.yml
  12. +33 −52 CODE_OF_CONDUCT.md
  13. +11 −12 README.md
  14. +4 −4 SECURITY.md
  15. +397 −31 docs/about/changelog.md
  16. +99 −58 docs/about/contributing.md
  17. +1 −0 docs/ads.txt
  18. +53 −0 docs/dragonfly-support.md
  19. +18 −14 docs/guides/implement-command.md
  20. +5 −4 docs/guides/test-case.md
  21. +255 −87 docs/index.md
  22. +6 −0 docs/overrides/main.html
  23. +20 −0 docs/overrides/partials/toc-item.html
  24. +0 −1,580 docs/redis-commands/Redis.md
  25. +0 −225 docs/redis-commands/RedisBloom.md
  26. +0 −53 docs/redis-commands/RedisGraph.md
  27. +0 −105 docs/redis-commands/RedisJson.md
  28. +0 −77 docs/redis-commands/RedisTimeSeries.md
  29. +111 −6 docs/redis-stack.md
  30. +2 −2 docs/requirements.txt
  31. +16 −0 docs/supported-commands/DRAGONFLY.md
  32. +28 −0 docs/supported-commands/Redis/BITMAP.md
  33. +133 −0 docs/supported-commands/Redis/CLUSTER.md
  34. +103 −0 docs/supported-commands/Redis/CONNECTION.md
  35. +111 −0 docs/supported-commands/Redis/GENERIC.md
  36. +44 −0 docs/supported-commands/Redis/GEO.md
  37. +115 −0 docs/supported-commands/Redis/HASH.md
  38. +16 −0 docs/supported-commands/Redis/HYPERLOGLOG.md
  39. +92 −0 docs/supported-commands/Redis/LIST.md
  40. +64 −0 docs/supported-commands/Redis/PUBSUB.md
  41. +95 −0 docs/supported-commands/Redis/SCRIPTING.md
  42. +287 −0 docs/supported-commands/Redis/SERVER.md
  43. +72 −0 docs/supported-commands/Redis/SET.md
  44. +144 −0 docs/supported-commands/Redis/SORTED-SET.md
  45. +84 −0 docs/supported-commands/Redis/STREAM.md
  46. +92 −0 docs/supported-commands/Redis/STRING.md
  47. +24 −0 docs/supported-commands/Redis/TRANSACTIONS.md
  48. +44 −0 docs/supported-commands/RedisBloom/BF.md
  49. +52 −0 docs/supported-commands/RedisBloom/CF.md
  50. +28 −0 docs/supported-commands/RedisBloom/CMS.md
  51. +60 −0 docs/supported-commands/RedisBloom/TDIGEST.md
  52. +32 −0 docs/supported-commands/RedisBloom/TOPK.md
  53. +92 −0 docs/supported-commands/RedisJson/JSON.md
  54. +1 −26 docs/{redis-commands/RedisSearch.md → supported-commands/RedisSearch/SEARCH.md}
  55. +21 −0 docs/supported-commands/RedisSearch/SUGGESTION.md
  56. +72 −0 docs/supported-commands/RedisTimeSeries/TIMESERIES.md
  57. +11 −0 docs/supported-commands/index.md
  58. +40 −0 docs/valkey-support.md
  59. +41 −4 fakeredis/__init__.py
  60. +224 −140 fakeredis/_basefakesocket.py
  61. +56 −59 fakeredis/_command_args_parsing.py
  62. +196 −142 fakeredis/_commands.py
  63. +166 −0 fakeredis/_connection.py
  64. +46 −17 fakeredis/_fakesocket.py
  65. +73 −66 fakeredis/_helpers.py
  66. +81 −13 fakeredis/_msgs.py
  67. +81 −184 fakeredis/_server.py
  68. +0 −127 fakeredis/_stream.py
  69. +129 −0 fakeredis/_tcp_server.py
  70. +50 −0 fakeredis/_valkey.py
  71. +0 −87 fakeredis/_zset.py
  72. +127 −111 fakeredis/aioredis.py
  73. +1 −0 fakeredis/commands.json
  74. +42 −0 fakeredis/commands_mixins/__init__.py
  75. +188 −0 fakeredis/commands_mixins/acl_mixin.py
  76. +187 −54 fakeredis/commands_mixins/bitmap_mixin.py
  77. +24 −11 fakeredis/commands_mixins/connection_mixin.py
  78. +215 −98 fakeredis/commands_mixins/generic_mixin.py
  79. +144 −75 fakeredis/commands_mixins/geo_mixin.py
  80. +190 −30 fakeredis/commands_mixins/hash_mixin.py
  81. +138 −33 fakeredis/commands_mixins/list_mixin.py
  82. +106 −66 fakeredis/commands_mixins/pubsub_mixin.py
  83. +130 −91 fakeredis/commands_mixins/scripting_mixin.py
  84. +42 −17 fakeredis/commands_mixins/server_mixin.py
  85. +64 −60 fakeredis/commands_mixins/set_mixin.py
  86. +310 −78 fakeredis/commands_mixins/sortedset_mixin.py
  87. +338 −39 fakeredis/commands_mixins/streams_mixin.py
  88. +146 −110 fakeredis/commands_mixins/string_mixin.py
  89. +25 −15 fakeredis/commands_mixins/transactions_mixin.py
  90. +8 −0 fakeredis/geo/__init__.py
  91. +26 −16 fakeredis/geo/geohash.py
  92. +1 −19 fakeredis/geo/haversine.py
  93. +33 −0 fakeredis/model/__init__.py
  94. +365 −0 fakeredis/model/_acl.py
  95. +60 −0 fakeredis/model/_command_info.py
  96. +89 −0 fakeredis/model/_expiring_members_set.py
  97. +80 −0 fakeredis/model/_hash.py
  98. +529 −0 fakeredis/model/_stream.py
  99. +281 −0 fakeredis/model/_timeseries_model.py
  100. +103 −0 fakeredis/model/_topk.py
  101. +100 −0 fakeredis/model/_zset.py
  102. +5 −0 fakeredis/server_specific_commands/__init__.py
  103. +24 −0 fakeredis/server_specific_commands/dragonfly_mixin.py
  104. +38 −2 fakeredis/stack/__init__.py
  105. +184 −0 fakeredis/stack/_bf_mixin.py
  106. +206 −0 fakeredis/stack/_cf_mixin.py
  107. +142 −0 fakeredis/stack/_cms_mixin.py
  108. +296 −155 fakeredis/stack/_json_mixin.py
  109. +298 −0 fakeredis/stack/_tdigest_mixin.py
  110. +561 −0 fakeredis/stack/_timeseries_mixin.py
  111. +100 −0 fakeredis/stack/_topk_mixin.py
  112. +36 −10 mkdocs.yml
  113. +925 −1,053 poetry.lock
  114. +80 −28 pyproject.toml
  115. +2 −0 redis-conf/redis-stack.conf
  116. +1 −0 redis-conf/users.acl
  117. +57 −45 scripts/create_issues.py
  118. +111 −0 scripts/generate_command_info.py
  119. +154 −0 scripts/generate_supported_commands_doc.py
  120. +0 −104 scripts/supported.py
  121. +0 −87 scripts/supported2.py
  122. +118 −51 test/conftest.py
  123. +233 −0 test/test_asyncredis.py
  124. +0 −103 test/test_extract_args.py
  125. +0 −25 test/test_general.py
  126. +290 −318 test/test_hypothesis.py
  127. +0 −138 test/test_init_args.py
  128. 0 test/test_internals/__init__.py
  129. +59 −0 test/test_internals/test_acl_save_load.py
  130. +139 −0 test/test_internals/test_asyncredis.py
  131. +140 −0 test/test_internals/test_extract_args.py
  132. +166 −0 test/test_internals/test_init_args.py
  133. +73 −0 test/test_internals/test_lua_modules.py
  134. +2 −2 test/{ → test_internals}/test_mock.py
  135. +45 −0 test/test_internals/test_transactions.py
  136. +46 −0 test/test_internals/test_xstream.py
  137. +250 −109 test/test_json/test_json.py
  138. +392 −121 test/test_json/test_json_arr_commands.py
  139. +9 −157 test/test_json/test_json_commands.py
  140. +403 −0 test/test_mixins/test_acl_commands.py
  141. +334 −125 test/test_mixins/test_bitmap_commands.py
  142. +124 −119 test/{ → test_mixins}/test_connection.py
  143. +453 −511 test/test_mixins/test_generic_commands.py
  144. +201 −67 test/test_mixins/test_geo_commands.py
  145. +185 −162 test/test_mixins/test_hash_commands.py
  146. +333 −0 test/test_mixins/test_hash_expire_commands.py
  147. +461 −380 test/test_mixins/test_list_commands.py
  148. +255 −136 test/test_mixins/test_pubsub_commands.py
  149. +202 −0 test/test_mixins/test_scan.py
  150. +570 −26 test/test_mixins/test_scripting.py
  151. +52 −28 test/test_mixins/test_server_commands.py
  152. +286 −252 test/test_mixins/test_set_commands.py
  153. +829 −755 test/test_mixins/test_sortedset_commands.py
  154. +688 −76 test/test_mixins/test_streams_commands.py
  155. +323 −303 test/test_mixins/test_string_commands.py
  156. +0 −309 test/test_mixins/test_transactions_commands.py
  157. +174 −0 test/test_mixins/test_zadd.py
  158. +0 −429 test/test_redis_asyncio.py
  159. +0 −484 test/test_scripting_lua_only.py
  160. 0 test/test_stack/__init__.py
  161. +189 −0 test/test_stack/test_bloomfilter.py
  162. +131 −0 test/test_stack/test_cms.py
  163. +37 −0 test/test_stack/test_cuckoofilter.py
  164. +133 −0 test/test_stack/test_tdigest.py
  165. +864 −0 test/test_stack/test_timeseries.py
  166. +38 −0 test/test_stack/test_topk.py
  167. 0 test/test_tcp_server/__init__.py
  168. +23 −0 test/test_tcp_server/test_connectivity.py
  169. +321 −0 test/test_transactions.py
  170. +0 −160 test/test_zadd.py
  171. +17 −13 test/testtools.py
  172. +12 −12 tox.ini
2 changes: 0 additions & 2 deletions .flake8

This file was deleted.

1 change: 1 addition & 0 deletions .github/CODEOWNERS
This CODEOWNERS file is valid.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @cunla
146 changes: 100 additions & 46 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,51 @@
<!-- omit in toc -->

# Contributing to fakeredis

First off, thanks for taking the time to contribute! ❤️

All types of contributions are encouraged and valued.
See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles
them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us
maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉

> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support
> the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc -->

## Table of Contents

- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)


## Code of Conduct

This project and everyone participating in it is governed by the
[fakeredis Code of Conduct](https://github.com/cunla/fakeredis-py/blob/main/CODE_OF_CONDUCT.md).
[fakeredis Code of Conduct](https://github.com/cunla/fakeredis-py/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to <daniel.maruani@gmail.com>.

to <daniel@moransoftware.ca>.

## I Have a Question

> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/cunla/fakeredis-py).
> If you want to ask a question, we assume that you have read the
> available [Documentation](https://github.com/cunla/fakeredis-py).
Before you ask a question, it is best to search for existing [Issues](https://github.com/cunla/fakeredis-py/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
Before you ask a question, it is best to search for existing [Issues](https://github.com/cunla/fakeredis-py/issues) that
might help you. In case you have found a suitable issue and still need clarification, you can write your question in
this issue. It is also advisable to search the internet for answers first.

If you then still feel the need to ask a question and need clarification, we recommend the following:

@@ -67,11 +73,13 @@ Depending on how large the project is, you may want to outsource the questioning
## I Want To Contribute

> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the
> necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs

<!-- omit in toc -->

#### Before Submitting a Bug Report

A good bug report shouldn't leave others needing to chase you up for more information.
@@ -80,113 +88,159 @@ Please complete the following steps in advance to help us fix any potential bug

- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible
environment components/versions (Make sure that you have read the [documentation](https://github.com/cunla/fakeredis-py).
environment components/versions (Make sure that you have read
the [documentation](https://github.com/cunla/fakeredis-py).
If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having,
check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/cunla/fakeredis-py/issues?q=label%3Abug).
check if there is not already a bug report existing for your bug or error in
the [bug tracker](https://github.com/cunla/fakeredis-py/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside the GitHub
community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?

<!-- omit in toc -->

#### How Do I Submit a Good Bug Report?

> You must never report security related issues, vulnerabilities or bugs including sensitive information
> to the issue tracker, or elsewhere in public.
> Instead sensitive bugs must be sent by email to <daniel.maruani@gmail.com>.
> Instead sensitive bugs must be sent by email to <daniel@moransoftware.ca>.
We use GitHub issues to track bugs and errors. If you run into an issue with the project:

- Open an [Issue](https://github.com/cunla/fakeredis-py/issues/new).
(Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and
not to label the issue.)
- Follow the issue template and provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own.
- Follow the issue template and provide as much context as possible and describe the *reproduction steps* that someone
else can follow to recreate the issue on their own.
This usually includes your code.
For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.

Once it's filed:

- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no
obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs
with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such
as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).

<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->


### Suggesting Enhancements

This section guides you through submitting an enhancement suggestion for fakeredis, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
This section guides you through submitting an enhancement suggestion for fakeredis, **including completely new features
and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community
to understand your suggestion and find related suggestions.

<!-- omit in toc -->

#### Before Submitting an Enhancement

- Make sure that you are using the latest version.
- Read the [documentation](https://github.com/cunla/fakeredis-py) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/cunla/fakeredis-py/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
- Read the [documentation](https://github.com/cunla/fakeredis-py) carefully and find out if the functionality is already
covered, maybe by an individual configuration.
- Perform a [search](https://github.com/cunla/fakeredis-py/issues) to see if the enhancement has already been suggested.
If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to
convince the project's developers of the merits of this feature. Keep in mind that we want features that will be
useful to the majority of our users and not just a small subset. If you're just targeting a minority of users,
consider writing an add-on/plugin library.

<!-- omit in toc -->

#### How Do I Submit a Good Enhancement Suggestion?

Enhancement suggestions are tracked as [GitHub issues](https://github.com/cunla/fakeredis-py/issues).

- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most fakeredis users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point
you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part
which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS
and Windows, and [this tool](https://github.com/colinkeenan/silentcast)
or [this tool](https://github.com/GNOME/byzanz) on
Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most fakeredis users. You may also want to point out the other
projects that solved it better and which could serve as inspiration.

<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->

### Your First Code Contribution

Unsure where to begin contributing? You can start by looking through
[help-wanted issues](https://github.com/cunla/fakeredis-py/labels/help%20wanted).

Never contributed to open source before? Here are a couple of friendly
tutorials:

- <http://makeapullrequest.com/>
- <http://www.firsttimersonly.com/>
- <http://makeapullrequest.com/>
- <http://www.firsttimersonly.com/>

### Getting started

- Create your own fork of the repository
- Do the changes in your fork
- Setup poetry `pip install poetry`
- Let poetry install everything required for a local environment `poetry install`
- To run all tests, use: `poetry run pytest -v`
- Note: In order to run the tests, a real redis server should be running.
The tests are comparing the results of each command between fakeredis and a real redis.
- You can use `docker-compose up redis6` or `docker-compose up redis7` to run redis.
- You can use `docker-compose up redis6` or `docker-compose up redis7` to run redis.
- Run test with coverage using `poetry run pytest -v --cov=fakeredis --cov-branch`
and then you can run `coverage report`.

### Improving The Documentation

- Create your own fork of the repository
- Do the changes in your fork, probably in `README.md`
- Create a pull request with the changes.

## Styleguides

### Commit Messages
Taken from [The seven rules of a great Git commit message](https://cbea.ms/git-commit/):

1. Separate subject from body with a blank line
2. Limit the subject line to 50 characters
3. Capitalize the subject line
4. Do not end the subject line with a period
5. Use the imperative mood in the subject line
6. Wrap the body at 72 characters
7. Use the body to explain what and why vs. how
Taken from [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)

```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```

The commit contains the following structural elements, to communicate intent to the consumers of your library:

* fix: a commit of the type fix patches a bug in your codebase (this correlates with `PATCH` in Semantic Versioning).
* feat: a commit of the type feat introduces a new feature to the codebase (this correlates with `MINOR` in Semantic
Versioning).
* BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a
breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any
type.
* types other than `fix:` and `feat:` are allowed, for example @commitlint/config-conventional (based on the Angular
convention) recommends `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others.
* footers other than `BREAKING CHANGE: <description>` may be provided and follow a convention similar to
[git trailer format](https://git-scm.com/docs/git-interpret-trailers).

Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic
Versioning (unless they include a BREAKING CHANGE). A scope may be provided to a commit’s type, to provide additional
contextual information and is contained within parenthesis, e.g., feat(parser): add ability to parse arrays.

## Join The Project Team
If you wish to be added to the project team as a collaborator, please send
a message to daniel.maruani@gmail.com with explanation.

If you wish to be added to the project team as a collaborator, please send
a message to daniel@moransoftware.ca with explanation.

<!-- omit in toc -->

## Attribution

This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
tidelift: "pypi/fakeredis"
github: cunla
polar: cunla
2 changes: 1 addition & 1 deletion .github/actions/test-coverage/action.yml
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ runs:
echo "COVERAGE=$(jq '.totals.percent_covered_display|tonumber' coverage.json)" >> $GITHUB_ENV
- name: Create coverage badge
if: ${{ github.event_name == 'push' }}
uses: schneegans/dynamic-badges-action@v1.6.0
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ inputs.gist-secret }}
gistID: b756396efb895f0e34558c980f1ca0c7
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

35 changes: 35 additions & 0 deletions .github/workflows/publish-documentation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---

name: Generate and publish documentation

on:
release:
types: [published]
workflow_dispatch:

jobs:
publish_documentation:
runs-on: ubuntu-latest
permissions:
contents: write
environment:
name: pypi
url: https://pypi.org/p/fakeredis
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- name: Publish documentation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }}
run: |
pip install -r docs/requirements.txt
mkdocs gh-deploy --force
mkdocs --version
35 changes: 35 additions & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---

name: Upload Python Package to PyPI

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
environment:
name: pypi
url: https://pypi.org/p/fakeredis
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
env:
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build

- name: Publish package to pypi
uses: pypa/gh-action-pypi-publish@v1.9.0
with:
print-hash: true
39 changes: 0 additions & 39 deletions .github/workflows/publish.yml

This file was deleted.

79 changes: 79 additions & 0 deletions .github/workflows/test-dragonfly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: Test Dragonfly

on:
workflow_dispatch:


concurrency:
group: dragon-fly-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
tests:
- "test_json"
- "test_mixins"
- "test_stack"
- "test_connection.py"
- "test_asyncredis.py"
- "test_general.py"
- "test_scan.py"
- "test_zadd.py"
- "test_translations.py"
- "test_sortedset_commands.py"
permissions:
pull-requests: write
services:
redis:
image: docker.dragonflydb.io/dragonflydb/dragonfly:latest
ports:
- 6390:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
cache-dependency-path: poetry.lock
python-version: 3.12
- name: Install dependencies
env:
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
run: |
python -m pip --quiet install poetry
echo "$HOME/.poetry/bin" >> $GITHUB_PATH
poetry install
poetry run pip install "fakeredis[json,bf,cf,lua]"
- name: Test without coverage
run: |
poetry run pytest test/${{ matrix.tests }} \
--html=report-${{ matrix.tests }}.html \
--self-contained-html \
-v
- name: Upload Tests Result
if: always()
uses: actions/upload-artifact@v4
with:
name: tests-result-${{ matrix.tests }}
path: report-${{ matrix.tests }}.html

upload-results:
needs: test
if: always()
runs-on: ubuntu-latest
steps:
- name: Collect Tests Result
uses: actions/upload-artifact/merge@v4
with:
delete-merged: true
69 changes: 40 additions & 29 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -10,19 +10,19 @@ on:
- master

concurrency:
group: test-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: "flake8 on code"
name: "Code linting (flake8/black)"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
cache-dependency-path: poetry.lock
python-version: "3.10"
python-version: "3.12"
- name: Install dependencies
env:
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
@@ -34,45 +34,56 @@ jobs:
shell: bash
run: |
poetry run flake8 fakeredis/
- name: Run black
shell: bash
run: |
poetry run black --check --verbose fakeredis test
- name: Test import
run: |
poetry build
pip install dist/fakeredis-*.tar.gz
python -c "import fakeredis"
test:
name: >
redis tests
py:${{ matrix.python-version }},${{ matrix.redis-image }},
redis-py:${{ matrix.redis-py }},cov:${{ matrix.coverage }},
lupa:${{ matrix.lupa }}, json:${{matrix.json}}
extra:${{matrix.extra}}
needs:
- "lint"
runs-on: ubuntu-latest
strategy:
max-parallel: 8
fail-fast: false
matrix:
redis-image: [ "redis:6.2.10", "redis:7.0.7" ]
python-version: [ "3.7", "3.8", "3.10", "3.11" ]
redis-py: [ "4.3.6", "4.4.4", "4.5.4" ]
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.10"
redis-image: "redis:6.2.10"
redis-py: "4.5.4"
lupa: true
- python-version: "3.10"
redis-image: "redis/redis-stack:7.0.6-RC3"
redis-py: "4.5.4"
lupa: true
json: true
- python-version: "3.12"
redis-image: "valkey/valkey:8.0"
redis-py: "5.2.1"
extra: "lua"
hypothesis: true
- python-version: "3.12"
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-v1"
redis-py: "5.2.1"
extra: "json, bf, lua, cf"
coverage: true
hypothesis: true

permissions:
pull-requests: write
services:
redis:
image: ${{ matrix.redis-image }}
ports:
- 6379:6379
- 6390:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
@@ -81,8 +92,8 @@ jobs:
outputs:
version: ${{ steps.getVersion.outputs.VERSION }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
cache-dependency-path: poetry.lock
python-version: ${{ matrix.python-version }}
@@ -93,15 +104,15 @@ jobs:
python -m pip --quiet install poetry
echo "$HOME/.poetry/bin" >> $GITHUB_PATH
poetry install
# if python version is below 3.10 and redis-py is 5.0.9 - change it to 5.0.8
if [[ ${{ matrix.python-version }} != "3.10" && ${{ matrix.redis-py }} == "5.0.9" ]]; then
poetry run pip install redis==5.0.8
fi
poetry run pip install redis==${{ matrix.redis-py }}
- name: Install lupa
if: ${{ matrix.lupa }}
run: |
poetry run pip install fakeredis[lua]
- name: Install json
if: ${{ matrix.json }}
if: ${{ matrix.extra }}
run: |
poetry run pip install fakeredis[json]
poetry run pip install "fakeredis[${{ matrix.extra }}]"
- name: Get version
id: getVersion
shell: bash
@@ -111,7 +122,7 @@ jobs:
- name: Test without coverage
if: ${{ !matrix.coverage }}
run: |
poetry run pytest -v
poetry run pytest -v -m "not slow"
- name: Test with coverage
if: ${{ matrix.coverage }}
uses: ./.github/actions/test-coverage
@@ -132,6 +143,6 @@ jobs:
- "test"
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85 changes: 33 additions & 52 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,52 @@
# Code of Conduct - fakeredis
# Code of Conduct fakeredis

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education,
socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:
Examples of behavior that contributes to a positive environment for our community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
advances
* The use of sexualized language or imagery, and sexual attention or advances
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
* Publishing others' private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate,
threatening, offensive, or harmful.
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take
appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for
moderation decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing
the community in public spaces. Examples of representing our community include using an official e-mail address, posting
via an official social media account, or acting as an appointed representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at <daniel.maruani@gmail.com>.
All complaints will be reviewed and investigated promptly and fairly.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible
for enforcement at <daniel@moransoftware.ca>. All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.

## Enforcement Guidelines

@@ -90,27 +76,22 @@ permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified
period of time. No public or private interaction with the people involved, including unsolicited interaction with those
enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Community Impact**: Demonstrating a pattern a community standards' violation, including sustained inappropriate
behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.
**Consequence**: A permanent ban from any sort of public interaction within the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
[1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
[2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/),
version [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md)
and [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
and was generated by [contributing-gen](https://github.com/bttger/contributing-gen).
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -7,28 +7,27 @@ fakeredis: A fake version of a redis-py
[![badge](https://img.shields.io/pypi/dm/fakeredis)](https://pypi.org/project/fakeredis/)
[![badge](https://img.shields.io/pypi/l/fakeredis)](./LICENSE)
[![Open Source Helpers](https://www.codetriage.com/cunla/fakeredis-py/badges/users.svg)](https://www.codetriage.com/cunla/fakeredis-py)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
--------------------

Documentation is now hosted in https://fakeredis.readthedocs.io/

Documentation is hosted in https://fakeredis.readthedocs.io/

# Intro

fakeredis is a pure-Python implementation of the redis-py python client
that simulates talking to a redis server.
FakeRedis is a pure-Python implementation of the Redis protocol API. It provides enhanced versions of the redis-py Python bindings for Redis.

It enables running tests requiring [Redis](https://redis.io/)/[ValKey](https://github.com/valkey-io/valkey)/[DragonflyDB](https://www.dragonflydb.io/) server without an actual server.

It also enables testing compatibility of different key-value datastores.

This was created originally for a single purpose: **to write tests**.
That provides the following added functionality: A built-in Redis server that is automatically installed, configured and managed when the Redis bindings are used. A single server shared by multiple programs or multiple independent servers. All the servers provided by FakeRedis support all Redis functionality including advanced features such as RedisJson, RedisBloom, GeoCommands.

This module now allows tests to simply use this
module as a reasonable substitute for redis.

See [official documentation](https://fakeredis.readthedocs.io/) for list of supported commands.

# Sponsor

fakeredis-py is developed for free.

You can support this project by becoming a sponsor using [this link](https://github.com/sponsors/cunla).

## Security contact information

To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -6,12 +6,12 @@ Use this section to tell people about which versions of your project are
currently being supported with security updates.

| Version | Supported |
| ------- | ------------------ |
| 2.11.x | :white_check_mark: |
|---------|--------------------|
| 2.18.x | :white_check_mark: |
| 1.10.x | :white_check_mark: |

## Reporting a Vulnerability

To report a security vulnerability, please use the Tidelift security contact.
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.

428 changes: 397 additions & 31 deletions docs/about/changelog.md

Large diffs are not rendered by default.

157 changes: 99 additions & 58 deletions docs/about/contributing.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/ads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
google.com, pub-2802331499006697, DIRECT, f08c47fec0942fa0
53 changes: 53 additions & 0 deletions docs/dragonfly-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Support for Dragonfly

[Dragonfly DB][1] is a drop-in Redis replacement that cuts costs and boosts performance. Designed to fully utilize the
power of modern cloud hardware and deliver on the data demands of modern applications, Dragonfly frees developers from
the limits of traditional in-memory data stores.

FakeRedis can be used as a Dragonfly replacement for testing and development purposes as well.

Since Dragonfly does not have its own unique clients, you can use the `Fakeredis` client to connect to a Dragonfly.

```python
from fakeredis import FakeRedis

client = FakeRedis(server_type="dragonfly")
client.set("key", "value")
print(client.get("key"))
```

Alternatively, you can start a thread with a Fake Valkey server.

```python
from threading import Thread
from fakeredis import TcpFakeServer

server_address = ("127.0.0.1", 6379)
server = TcpFakeServer(server_address, server_type="dragonfly")
t = Thread(target=server.serve_forever, daemon=True)
t.start()

import redis

r = redis.Redis(host=server_address[0], port=server_address[1])
r.set("foo", "bar")
assert r.get("foo") == b"bar"

```

To call Dragonfly specific commands, which are not implemented in the redis-py client, you can use the
`execute_command`, like in this example calling the [`SADDEX`][2] command:

```python
from fakeredis import FakeRedis

client = FakeRedis(server_type="dragonfly")
client.sadd("key", "value")
# The SADDEX command is not implemented in redis-py
client.execute_command("SADDEX", 10, "key", "value")

```

[1]: https://www.dragonflydb.io/

[2]: https://www.dragonflydb.io/docs/command-reference/sets/saddex
32 changes: 18 additions & 14 deletions docs/guides/implement-command.md
Original file line number Diff line number Diff line change
@@ -17,25 +17,29 @@ class FakeSocket(BaseFakeSocket, FakeLuaSocket):
```

## Parsing command arguments
The `extract_args` method should help extracting arguments from `*args`.

The `extract_args` method should help to extract arguments from `*args`.
It extracts from actual arguments which arguments exist and their value if relevant.

Parameters `extract_args` expect:

- `actual_args`
The actual arguments to parse
The actual arguments to parse
- `expected`
Arguments to look for, see below explanation.
Arguments to look for, see below explanation.
- `error_on_unexpected` (default: True)
Should an error be raised when actual_args contain an unexpected argument?
Should an error be raised when actual_args contain an unexpected argument?
- `left_from_first_unexpected` (default: True)
Once reaching an unexpected argument in actual_args,
Should parsing stop?
Once reaching an unexpected argument in actual_args,
Should parsing stop?

It returns two lists:

- List of values for expected arguments.
- List of remaining args.

### Expected argument structure:

- If expected argument has only a name, it will be parsed as boolean
(Whether it exists in actual `*args` or not).
- In order to parse a numerical value following the expected argument,
@@ -53,11 +57,11 @@ method will be triggered to check the validity of syntax and analyze the command

By default, it takes the name of the method as the command name.

If the method implements a subcommand (eg, `SCRIPT LOAD`), a Redis module command (eg, `JSON.GET`),
or a python reserve word where you can not use it as the method name (eg, `EXEC`), then you can supply
explicitly the name parameter.
If the method implements a subcommand (e.g., `SCRIPT LOAD`), a Redis module command (e.g., `JSON.GET`),
or a python reserve word where you can not use it as the method name (e.g., `EXEC`), then you can explicitly supply
the name parameter.

If the command implemented require certain arguments, they can be supplied in the first parameter as a tuple.
If the command implemented requires certain arguments, they can be supplied in the first parameter as a tuple.
When receiving the command through the socket, the bytes will be converted to the argument types
supplied or remain as `bytes`.

@@ -82,12 +86,12 @@ The tests not only assert the validity of output but runs the same test on a rea
to the real server output.

- Create tests in the relevant test file.
- If support for the command was introduced in a certain version of redis-py (
see [redis-py release notes](https://github.com/redis/redis-py/releases/tag/v4.3.4)) you can use the
- If support for the command was introduced in a certain version of redis-py
(see [redis-py release notes](https://github.com/redis/redis-py/releases/tag/v4.3.4)) you can use the
decorator `@testtools.run_test_if_redispy_ver` on your tests. example:

```python
@testtools.run_test_if_redispy_ver('above', '4.2.0') # This will run for redis-py 4.2.0 or above.
@testtools.run_test_if_redispy_ver('gte', '4.2.0') # This will run for redis-py 4.2.0 or above.
def test_expire_should_not_expire__when_no_expire_is_set(r):
r.set('foo', 'bar')
assert r.get('foo') == b'bar'
@@ -100,7 +104,7 @@ Lastly, run from the root of the project the script to regenerate documentation
supported and unsupported commands:

```bash
python scripts/supported.py
python scripts/generate_supported_commands_doc.py
```

Include the changes in the `docs/` directory in your pull request.
9 changes: 5 additions & 4 deletions docs/guides/test-case.md
Original file line number Diff line number Diff line change
@@ -8,19 +8,20 @@ That way parity of real Redis and FakeRedis is ensured.
To write a new test case for a command:

- Determine which mixin the command belongs to and the test file for
the mixin (eg, `string_mixin.py` => `test_string_commands.py`).
the mixin (e.g., `string_mixin.py` => `test_string_commands.py`).
- Tests should support python 3.7 and above.
- Determine when support for the command was introduced
- To limit the redis-server versions it will run on use:
- To limit the redis-server versions, it will run on use:
`@pytest.mark.max_server(version)` and `@pytest.mark.min_server(version)`
- To limit the redis-py version use `@run_test_if_redispy_ver(above/below, version)`
- To limit the redis-py version use `@run_test_if_redispy_ver('gte', version)`
(you can use `ge`/`gte`/`lte`/`lt`/`eq`/`ne`).
- pytest will inject a redis connection to the argument `r` of the test.

Sample of running a test for redis-py v4.2.0 and above, redis-server 7.0 and above.

```python
@pytest.mark.min_server('7')
@testtools.run_test_if_redispy_ver('above', '4.2.0')
@testtools.run_test_if_redispy_ver('gte', '4.2.0')
def test_expire_should_not_expire__when_no_expire_is_set(r):
r.set('foo', 'bar')
assert r.get('foo') == b'bar'
342 changes: 255 additions & 87 deletions docs/index.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/overrides/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block extrahead %}
<meta name="google-adsense-account" content="ca-pub-2802331499006697">
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2802331499006697"
crossorigin="anonymous"></script>
{% endblock %}
20 changes: 20 additions & 0 deletions docs/overrides/partials/toc-item.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<li class="md-nav__item">
<a href="{{ toc_item.url }}" class="md-nav__link">
<span class="md-ellipsis">
{{ toc_item.title }}
</span>
</a>

<!-- Table of contents list -->
{% if toc_item.children %}
<nav class="md-nav" aria-label="{{ toc_item.title | striptags }}">
<ul class="md-nav__list">
{% for toc_item in toc_item.children %}
{% if not page.meta.toc_depth or toc_item.level <= page.meta.toc_depth %}
{% include "partials/toc-item.html" %}
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
</li>
1,580 changes: 0 additions & 1,580 deletions docs/redis-commands/Redis.md

This file was deleted.

225 changes: 0 additions & 225 deletions docs/redis-commands/RedisBloom.md

This file was deleted.

53 changes: 0 additions & 53 deletions docs/redis-commands/RedisGraph.md

This file was deleted.

105 changes: 0 additions & 105 deletions docs/redis-commands/RedisJson.md

This file was deleted.

77 changes: 0 additions & 77 deletions docs/redis-commands/RedisTimeSeries.md

This file was deleted.

117 changes: 111 additions & 6 deletions docs/redis-stack.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# Support for redis-stack

To install all supported modules, you can install fakeredis with `pip install fakeredis[lua,json,bf]`.

## RedisJson support

Currently, Redis Json module is partially implemented (
see [supported commands](./redis-commands/implemented_commands.md#json-commands)).
Support for JSON commands (eg, [`JSON.GET`](https://redis.io/commands/json.get/)) is implemented using
[jsonpath-ng](https://github.com/h2non/jsonpath-ng), you can simply install it using `pip install 'fakeredis[json]'`.
The JSON capability of Redis Stack provides JavaScript Object Notation (JSON) support for Redis. It lets you store,
update, and retrieve JSON values in a Redis database, similar to any other Redis data type. Redis JSON also works
seamlessly with Search and Query to let you index and query JSON documents.

JSONPath's syntax: The following JSONPath syntax table was adapted from Goessner's [path syntax comparison][4].

Currently, Redis Json module is fully implemented (see [supported commands][1]).
Support for JSON commands (e.g., [`JSON.GET`][2]) is implemented using
[jsonpath-ng,][3] you can install it using `pip install 'fakeredis[json]'`.

```pycon
>>> import fakeredis
@@ -18,7 +25,105 @@ Support for JSON commands (eg, [`JSON.GET`](https://redis.io/commands/json.get/)
'bar'
```

## Lua support
## Bloom filter support

Bloom filters are a probabilistic data structure that checks for the presence of an element in a set.

Instead of storing all the elements in the set, Bloom Filters store only the elements' hashed representation, thus
sacrificing some precision. The trade-off is that Bloom Filters are very space-efficient and fast.

You can get a false positive result, but never a false negative, i.e., if the bloom filter says that an element is not
in the set, then it is definitely not in the set. If the bloom filter says that an element is in the set, then it is
most likely in the set, but it is not guaranteed.

Currently, RedisBloom module bloom filter commands are fully implemented using [pybloom-live][5](
see [supported commands][6]).

You can install it using `pip install 'fakeredis[probabilistic]'`.

```pycon
>>> import fakeredis
>>> r = fakeredis.FakeStrictRedis()
>>> r.bf().madd('key', 'v1', 'v2', 'v3') == [1, 1, 1]
>>> r.bf().exists('key', 'v1')
1
>>> r.bf().exists('key', 'v5')
0
```

## [Count-Min Sketch][8] support

Count-min sketch is a probabilistic data structure that estimates the frequency of an element in a data stream.

You can install it using `pip install 'fakeredis[probabilistic]'`.

```pycon
>>> import fakeredis
>>> r = fakeredis.FakeStrictRedis()
>>> r.cms().initbydim("cmsDim", 100, 5)
OK
>>> r.cms().incrby("cmsDim", ["foo"], [3])
[3]
```

## [Cuckoo filter][9] support

Cuckoo filters are a probabilistic data structure that checks for the presence of an element in a set

You can install it using `pip install 'fakeredis[probabilistic]'`.

## [Redis programmability][7]

Redis provides a programming interface that lets you execute custom scripts on the server itself. In Redis 7 and beyond,
you can use Redis Functions to manage and run your scripts. In Redis 6.2 and below, you use Lua scripting with the EVAL
command to program the server.

If you wish to have Lua scripting support (this includes features like ``redis.lock.Lock``, which are implemented in
Lua), you will need [lupa](https://pypi.org/project/lupa/), you can simply install it using `pip install 'fakeredis[lua]'`
Lua), you will need [lupa][10], you can install it using `pip install 'fakeredis[lua]'`

By default, FakeRedis works with LUA version 5.1, to use a different version supported by lupa,
set the `FAKEREDIS_LUA_VERSION` environment variable to the desired version (e.g., `5.4`).

### LUA binary modules

fakeredis supports using LUA binary modules as well. In order to have your FakeRedis instance load a LUA binary module,
you can use the `lua_modules` parameter.

```pycon
>>> import fakeredis
>>> r = fakeredis.FakeStrictRedis(lua_modules={"my_module.so"})
```

The module `.so`/`.dll` file should be in the working directory.

To install LUA modules, you can use [luarocks][11] to install the module and then copy the `.so`/`.dll` file to the
working directory.

For example, to install `lua-cjson`:

```sh
luarocks install lua-cjson
cp /opt/homebrew/lib/lua/5.4/cjson.so `pwd`
```

[1]:./supported-commands/RedisJson/

[2]:https://redis.io/commands/json.get/

[3]:https://github.com/h2non/jsonpath-ng

[4]:https://goessner.net/articles/JsonPath/index.html#e2

[5]:https://github.com/joseph-fox/python-bloomfilter

[6]:./supported-commands/BloomFilter/

[7]:https://redis.io/docs/interact/programmability/

[8]:https://redis.io/docs/data-types/probabilistic/count-min-sketch/

[9]:https://redis.io/docs/data-types/probabilistic/cuckoo-filter/

[10]:https://pypi.org/project/lupa/

[11]:https://luarocks.org/
4 changes: 2 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
mkdocs==1.4.2
mkdocs-material==9.1.8
mkdocs==1.6.1
mkdocs-material==9.5.48
16 changes: 16 additions & 0 deletions docs/supported-commands/DRAGONFLY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Dragonfly specific commands

> To implement support for a command, see [here](/guides/implement-command/)
These are commands that are not implemented in Redis but supported in Dragonfly and FakeRedis. To use these commands,
you can call `execute_command` with the command name and arguments as follows:

```python
client = FakeRedis(server_type="dragonfly")
client.execute_command("SADDEX", 10, "key", "value")
```

## [SADDEX](https://www.dragonflydb.io/docs/command-reference/sets/saddex)

Similar to SADD but adds one or more members that expire after specified number of seconds. An error is returned when
the value stored at key is not a set.
28 changes: 28 additions & 0 deletions docs/supported-commands/Redis/BITMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Redis `bitmap` commands (6/6 implemented)

## [BITCOUNT](https://redis.io/commands/bitcount/)

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

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

Performs arbitrary bitfield integer operations on strings.

## [BITOP](https://redis.io/commands/bitop/)

Performs bitwise operations on multiple strings, and stores the result.

## [BITPOS](https://redis.io/commands/bitpos/)

Finds the first set (1) or clear (0) bit in a string.

## [GETBIT](https://redis.io/commands/getbit/)

Returns a bit value by offset.

## [SETBIT](https://redis.io/commands/setbit/)

Sets or clears the bit at offset of the string value. Creates the key if it doesn't exist.



133 changes: 133 additions & 0 deletions docs/supported-commands/Redis/CLUSTER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@

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

Signals that a cluster client is following an -ASK redirect.

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

A container for Redis Cluster commands.

#### [CLUSTER ADDSLOTS](https://redis.io/commands/cluster-addslots/) <small>(not implemented)</small>

Assigns new hash slots to a node.

#### [CLUSTER ADDSLOTSRANGE](https://redis.io/commands/cluster-addslotsrange/) <small>(not implemented)</small>

Assigns new hash slot ranges to a node.

#### [CLUSTER BUMPEPOCH](https://redis.io/commands/cluster-bumpepoch/) <small>(not implemented)</small>

Advances the cluster config epoch.

#### [CLUSTER COUNT-FAILURE-REPORTS](https://redis.io/commands/cluster-count-failure-reports/) <small>(not implemented)</small>

Returns the number of active failure reports active for a node.

#### [CLUSTER COUNTKEYSINSLOT](https://redis.io/commands/cluster-countkeysinslot/) <small>(not implemented)</small>

Returns the number of keys in a hash slot.

#### [CLUSTER DELSLOTS](https://redis.io/commands/cluster-delslots/) <small>(not implemented)</small>

Sets hash slots as unbound for a node.

#### [CLUSTER DELSLOTSRANGE](https://redis.io/commands/cluster-delslotsrange/) <small>(not implemented)</small>

Sets hash slot ranges as unbound for a node.

#### [CLUSTER FAILOVER](https://redis.io/commands/cluster-failover/) <small>(not implemented)</small>

Forces a replica to perform a manual failover of its master.

#### [CLUSTER FLUSHSLOTS](https://redis.io/commands/cluster-flushslots/) <small>(not implemented)</small>

Deletes all slots information from a node.

#### [CLUSTER FORGET](https://redis.io/commands/cluster-forget/) <small>(not implemented)</small>

Removes a node from the nodes table.

#### [CLUSTER GETKEYSINSLOT](https://redis.io/commands/cluster-getkeysinslot/) <small>(not implemented)</small>

Returns the key names in a hash slot.

#### [CLUSTER HELP](https://redis.io/commands/cluster-help/) <small>(not implemented)</small>

Returns helpful text about the different subcommands.

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

Returns information about the state of a node.

#### [CLUSTER KEYSLOT](https://redis.io/commands/cluster-keyslot/) <small>(not implemented)</small>

Returns the hash slot for a key.

#### [CLUSTER LINKS](https://redis.io/commands/cluster-links/) <small>(not implemented)</small>

Returns a list of all TCP links to and from peer nodes.

#### [CLUSTER MEET](https://redis.io/commands/cluster-meet/) <small>(not implemented)</small>

Forces a node to handshake with another node.

#### [CLUSTER MYID](https://redis.io/commands/cluster-myid/) <small>(not implemented)</small>

Returns the ID of a node.

#### [CLUSTER MYSHARDID](https://redis.io/commands/cluster-myshardid/) <small>(not implemented)</small>

Returns the shard ID of a node.

#### [CLUSTER NODES](https://redis.io/commands/cluster-nodes/) <small>(not implemented)</small>

Returns the cluster configuration for a node.

#### [CLUSTER REPLICAS](https://redis.io/commands/cluster-replicas/) <small>(not implemented)</small>

Lists the replica nodes of a master node.

#### [CLUSTER REPLICATE](https://redis.io/commands/cluster-replicate/) <small>(not implemented)</small>

Configure a node as replica of a master node.

#### [CLUSTER RESET](https://redis.io/commands/cluster-reset/) <small>(not implemented)</small>

Resets a node.

#### [CLUSTER SAVECONFIG](https://redis.io/commands/cluster-saveconfig/) <small>(not implemented)</small>

Forces a node to save the cluster configuration to disk.

#### [CLUSTER SET-CONFIG-EPOCH](https://redis.io/commands/cluster-set-config-epoch/) <small>(not implemented)</small>

Sets the configuration epoch for a new node.

#### [CLUSTER SETSLOT](https://redis.io/commands/cluster-setslot/) <small>(not implemented)</small>

Binds a hash slot to a node.

#### [CLUSTER SHARDS](https://redis.io/commands/cluster-shards/) <small>(not implemented)</small>

Returns the mapping of cluster slots to shards.

#### [CLUSTER SLAVES](https://redis.io/commands/cluster-slaves/) <small>(not implemented)</small>

Lists the replica nodes of a master node.

#### [CLUSTER SLOTS](https://redis.io/commands/cluster-slots/) <small>(not implemented)</small>

Returns the mapping of cluster slots to nodes.

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

Enables read-only queries for a connection to a Redis Cluster replica node.

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

Enables read-write queries for a connection to a Reids Cluster replica node.


103 changes: 103 additions & 0 deletions docs/supported-commands/Redis/CONNECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Redis `connection` commands (4/24 implemented)

## [CLIENT SETINFO](https://redis.io/commands/client-setinfo/)

Sets information specific to the client or connection.

## [ECHO](https://redis.io/commands/echo/)

Returns the given string.

## [PING](https://redis.io/commands/ping/)

Returns the server's liveliness response.

## [SELECT](https://redis.io/commands/select/)

Changes the selected database.


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

Authenticates the connection.

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

A container for client connection commands.

#### [CLIENT CACHING](https://redis.io/commands/client-caching/) <small>(not implemented)</small>

Instructs the server whether to track the keys in the next request.

#### [CLIENT GETNAME](https://redis.io/commands/client-getname/) <small>(not implemented)</small>

Returns the name of the connection.

#### [CLIENT GETREDIR](https://redis.io/commands/client-getredir/) <small>(not implemented)</small>

Returns the client ID to which the connection's tracking notifications are redirected.

#### [CLIENT ID](https://redis.io/commands/client-id/) <small>(not implemented)</small>

Returns the unique client ID of the connection.

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

Returns information about the connection.

#### [CLIENT KILL](https://redis.io/commands/client-kill/) <small>(not implemented)</small>

Terminates open connections.

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

Lists open connections.

#### [CLIENT NO-EVICT](https://redis.io/commands/client-no-evict/) <small>(not implemented)</small>

Sets the client eviction mode of the connection.

#### [CLIENT NO-TOUCH](https://redis.io/commands/client-no-touch/) <small>(not implemented)</small>

Controls whether commands sent by the client affect the LRU/LFU of accessed keys.

#### [CLIENT PAUSE](https://redis.io/commands/client-pause/) <small>(not implemented)</small>

Suspends commands processing.

#### [CLIENT REPLY](https://redis.io/commands/client-reply/) <small>(not implemented)</small>

Instructs the server whether to reply to commands.

#### [CLIENT SETNAME](https://redis.io/commands/client-setname/) <small>(not implemented)</small>

Sets the connection name.

#### [CLIENT TRACKING](https://redis.io/commands/client-tracking/) <small>(not implemented)</small>

Controls server-assisted client-side caching for the connection.

#### [CLIENT TRACKINGINFO](https://redis.io/commands/client-trackinginfo/) <small>(not implemented)</small>

Returns information about server-assisted client-side caching for the connection.

#### [CLIENT UNBLOCK](https://redis.io/commands/client-unblock/) <small>(not implemented)</small>

Unblocks a client blocked by a blocking command from a different connection.

#### [CLIENT UNPAUSE](https://redis.io/commands/client-unpause/) <small>(not implemented)</small>

Resumes processing commands from paused clients.

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

Handshakes with the Redis server.

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

Resets the connection.


111 changes: 111 additions & 0 deletions docs/supported-commands/Redis/GENERIC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Redis `generic` commands (23/26 implemented)

## [DEL](https://redis.io/commands/del/)

Deletes one or more keys.

## [DUMP](https://redis.io/commands/dump/)

Returns a serialized representation of the value stored at a key.

## [EXISTS](https://redis.io/commands/exists/)

Determines whether one or more keys exist.

## [EXPIRE](https://redis.io/commands/expire/)

Sets the expiration time of a key in seconds.

## [EXPIREAT](https://redis.io/commands/expireat/)

Sets the expiration time of a key to a Unix timestamp.

## [EXPIRETIME](https://redis.io/commands/expiretime/)

Returns the expiration time of a key as a Unix timestamp.

## [KEYS](https://redis.io/commands/keys/)

Returns all key names that match a pattern.

## [MOVE](https://redis.io/commands/move/)

Moves a key to another database.

## [PERSIST](https://redis.io/commands/persist/)

Removes the expiration time of a key.

## [PEXPIRE](https://redis.io/commands/pexpire/)

Sets the expiration time of a key in milliseconds.

## [PEXPIREAT](https://redis.io/commands/pexpireat/)

Sets the expiration time of a key to a Unix milliseconds timestamp.

## [PEXPIRETIME](https://redis.io/commands/pexpiretime/)

Returns the expiration time of a key as a Unix milliseconds timestamp.

## [PTTL](https://redis.io/commands/pttl/)

Returns the expiration time in milliseconds of a key.

## [RANDOMKEY](https://redis.io/commands/randomkey/)

Returns a random key name from the database.

## [RENAME](https://redis.io/commands/rename/)

Renames a key and overwrites the destination.

## [RENAMENX](https://redis.io/commands/renamenx/)

Renames a key only when the target key name doesn't exist.

## [RESTORE](https://redis.io/commands/restore/)

Creates a key from the serialized representation of a value.

## [SCAN](https://redis.io/commands/scan/)

Iterates over the key names in the database.

## [SORT](https://redis.io/commands/sort/)

Sorts the elements in a list, a set, or a sorted set, optionally storing the result.

## [SORT_RO](https://redis.io/commands/sort_ro/)

Returns the sorted elements of a list, a set, or a sorted set.

## [TTL](https://redis.io/commands/ttl/)

Returns the expiration time in seconds of a key.

## [TYPE](https://redis.io/commands/type/)

Determines the type of value stored at a key.

## [UNLINK](https://redis.io/commands/unlink/)

Asynchronously deletes one or more keys.


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

Copies the value of a key to a new key.

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

Blocks until the asynchronous replication of all preceding write commands sent by the connection is completed.

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

Blocks until all of the preceding write commands sent by the connection are written to the append-only file of the master and/or replicas.


44 changes: 44 additions & 0 deletions docs/supported-commands/Redis/GEO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Redis `geo` commands (10/10 implemented)

## [GEOADD](https://redis.io/commands/geoadd/)

Adds one or more members to a geospatial index. The key is created if it doesn't exist.

## [GEODIST](https://redis.io/commands/geodist/)

Returns the distance between two members of a geospatial index.

## [GEOHASH](https://redis.io/commands/geohash/)

Returns members from a geospatial index as geohash strings.

## [GEOPOS](https://redis.io/commands/geopos/)

Returns the longitude and latitude of members from a geospatial index.

## [GEORADIUS](https://redis.io/commands/georadius/)

Queries a geospatial index for members within a distance from a coordinate, optionally stores the result.

## [GEORADIUSBYMEMBER](https://redis.io/commands/georadiusbymember/)

Queries a geospatial index for members within a distance from a member, optionally stores the result.

## [GEORADIUSBYMEMBER_RO](https://redis.io/commands/georadiusbymember_ro/)

Returns members from a geospatial index that are within a distance from a member.

## [GEORADIUS_RO](https://redis.io/commands/georadius_ro/)

Returns members from a geospatial index that are within a distance from a coordinate.

## [GEOSEARCH](https://redis.io/commands/geosearch/)

Queries a geospatial index for members inside an area of a box or a circle.

## [GEOSEARCHSTORE](https://redis.io/commands/geosearchstore/)

Queries a geospatial index for members inside an area of a box or a circle, optionally stores the result.



115 changes: 115 additions & 0 deletions docs/supported-commands/Redis/HASH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Redis `hash` commands (25/27 implemented)

## [HDEL](https://redis.io/commands/hdel/)

Deletes one or more fields and their values from a hash. Deletes the hash if no fields remain.

## [HEXISTS](https://redis.io/commands/hexists/)

Determines whether a field exists in a hash.

## [HEXPIRE](https://redis.io/commands/hexpire/)

Set expiry for hash field using relative time to expire (seconds)

## [HEXPIREAT](https://redis.io/commands/hexpireat/)

Set expiry for hash field using an absolute Unix timestamp (seconds)

## [HEXPIRETIME](https://redis.io/commands/hexpiretime/)

Returns the expiration time of a hash field as a Unix timestamp, in seconds.

## [HGET](https://redis.io/commands/hget/)

Returns the value of a field in a hash.

## [HGETALL](https://redis.io/commands/hgetall/)

Returns all fields and values in a hash.

## [HINCRBY](https://redis.io/commands/hincrby/)

Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.

## [HINCRBYFLOAT](https://redis.io/commands/hincrbyfloat/)

Increments the floating point value of a field by a number. Uses 0 as initial value if the field doesn't exist.

## [HKEYS](https://redis.io/commands/hkeys/)

Returns all fields in a hash.

## [HLEN](https://redis.io/commands/hlen/)

Returns the number of fields in a hash.

## [HMGET](https://redis.io/commands/hmget/)

Returns the values of all fields in a hash.

## [HMSET](https://redis.io/commands/hmset/)

Sets the values of multiple fields.

## [HPERSIST](https://redis.io/commands/hpersist/)

Removes the expiration time for each specified field

## [HPEXPIRE](https://redis.io/commands/hpexpire/)

Set expiry for hash field using relative time to expire (milliseconds)

## [HPEXPIREAT](https://redis.io/commands/hpexpireat/)

Set expiry for hash field using an absolute Unix timestamp (milliseconds)

## [HPEXPIRETIME](https://redis.io/commands/hpexpiretime/)

Returns the expiration time of a hash field as a Unix timestamp, in msec.

## [HPTTL](https://redis.io/commands/hpttl/)

Returns the TTL in milliseconds of a hash field.

## [HRANDFIELD](https://redis.io/commands/hrandfield/)

Returns one or more random fields from a hash.

## [HSCAN](https://redis.io/commands/hscan/)

Iterates over fields and values of a hash.

## [HSET](https://redis.io/commands/hset/)

Creates or modifies the value of a field in a hash.

## [HSETNX](https://redis.io/commands/hsetnx/)

Sets the value of a field in a hash only when the field doesn't exist.

## [HSTRLEN](https://redis.io/commands/hstrlen/)

Returns the length of the value of a field.

## [HTTL](https://redis.io/commands/httl/)

Returns the TTL in seconds of a hash field.

## [HVALS](https://redis.io/commands/hvals/)

Returns all values in a hash.


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

For each specified field, returns its value and optionally set the field's remaining expiration time in seconds / milliseconds

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

For each specified field, returns its value and optionally set the field's remaining expiration time in seconds / milliseconds


16 changes: 16 additions & 0 deletions docs/supported-commands/Redis/HYPERLOGLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Redis `hyperloglog` commands (3/3 implemented)

## [PFADD](https://redis.io/commands/pfadd/)

Adds elements to a HyperLogLog key. Creates the key if it doesn't exist.

## [PFCOUNT](https://redis.io/commands/pfcount/)

Returns the approximated cardinality of the set(s) observed by the HyperLogLog key(s).

## [PFMERGE](https://redis.io/commands/pfmerge/)

Merges one or more HyperLogLog values into a single key.



92 changes: 92 additions & 0 deletions docs/supported-commands/Redis/LIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Redis `list` commands (22/22 implemented)

## [BLMOVE](https://redis.io/commands/blmove/)

Pops an element from a list, pushes it to another list and returns it. Blocks until an element is available otherwise. Deletes the list if the last element was moved.

## [BLMPOP](https://redis.io/commands/blmpop/)

Pops the first element from one of multiple lists. Blocks until an element is available otherwise. Deletes the list if the last element was popped.

## [BLPOP](https://redis.io/commands/blpop/)

Removes and returns the first element in a list. Blocks until an element is available otherwise. Deletes the list if the last element was popped.

## [BRPOP](https://redis.io/commands/brpop/)

Removes and returns the last element in a list. Blocks until an element is available otherwise. Deletes the list if the last element was popped.

## [BRPOPLPUSH](https://redis.io/commands/brpoplpush/)

Pops an element from a list, pushes it to another list and returns it. Block until an element is available otherwise. Deletes the list if the last element was popped.

## [LINDEX](https://redis.io/commands/lindex/)

Returns an element from a list by its index.

## [LINSERT](https://redis.io/commands/linsert/)

Inserts an element before or after another element in a list.

## [LLEN](https://redis.io/commands/llen/)

Returns the length of a list.

## [LMOVE](https://redis.io/commands/lmove/)

Returns an element after popping it from one list and pushing it to another. Deletes the list if the last element was moved.

## [LMPOP](https://redis.io/commands/lmpop/)

Returns multiple elements from a list after removing them. Deletes the list if the last element was popped.

## [LPOP](https://redis.io/commands/lpop/)

Returns the first elements in a list after removing it. Deletes the list if the last element was popped.

## [LPOS](https://redis.io/commands/lpos/)

Returns the index of matching elements in a list.

## [LPUSH](https://redis.io/commands/lpush/)

Prepends one or more elements to a list. Creates the key if it doesn't exist.

## [LPUSHX](https://redis.io/commands/lpushx/)

Prepends one or more elements to a list only when the list exists.

## [LRANGE](https://redis.io/commands/lrange/)

Returns a range of elements from a list.

## [LREM](https://redis.io/commands/lrem/)

Removes elements from a list. Deletes the list if the last element was removed.

## [LSET](https://redis.io/commands/lset/)

Sets the value of an element in a list by its index.

## [LTRIM](https://redis.io/commands/ltrim/)

Removes elements from both ends a list. Deletes the list if all elements were trimmed.

## [RPOP](https://redis.io/commands/rpop/)

Returns and removes the last elements of a list. Deletes the list if the last element was popped.

## [RPOPLPUSH](https://redis.io/commands/rpoplpush/)

Returns the last element of a list after removing and pushing it to another list. Deletes the list if the last element was popped.

## [RPUSH](https://redis.io/commands/rpush/)

Appends one or more elements to a list. Creates the key if it doesn't exist.

## [RPUSHX](https://redis.io/commands/rpushx/)

Appends an element to a list only when the list exists.



64 changes: 64 additions & 0 deletions docs/supported-commands/Redis/PUBSUB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Redis `pubsub` commands (15/15 implemented)

## [PSUBSCRIBE](https://redis.io/commands/psubscribe/)

Listens for messages published to channels that match one or more patterns.

## [PUBLISH](https://redis.io/commands/publish/)

Posts a message to a channel.

## [PUBSUB](https://redis.io/commands/pubsub/)

A container for Pub/Sub commands.

## [PUBSUB CHANNELS](https://redis.io/commands/pubsub-channels/)

Returns the active channels.

## [PUBSUB HELP](https://redis.io/commands/pubsub-help/)

Returns helpful text about the different subcommands.

## [PUBSUB NUMPAT](https://redis.io/commands/pubsub-numpat/)

Returns a count of unique pattern subscriptions.

## [PUBSUB NUMSUB](https://redis.io/commands/pubsub-numsub/)

Returns a count of subscribers to channels.

## [PUBSUB SHARDCHANNELS](https://redis.io/commands/pubsub-shardchannels/)

Returns the active shard channels.

## [PUBSUB SHARDNUMSUB](https://redis.io/commands/pubsub-shardnumsub/)

Returns the count of subscribers of shard channels.

## [PUNSUBSCRIBE](https://redis.io/commands/punsubscribe/)

Stops listening to messages published to channels that match one or more patterns.

## [SPUBLISH](https://redis.io/commands/spublish/)

Post a message to a shard channel

## [SSUBSCRIBE](https://redis.io/commands/ssubscribe/)

Listens for messages published to shard channels.

## [SUBSCRIBE](https://redis.io/commands/subscribe/)

Listens for messages published to channels.

## [SUNSUBSCRIBE](https://redis.io/commands/sunsubscribe/)

Stops listening to messages posted to shard channels.

## [UNSUBSCRIBE](https://redis.io/commands/unsubscribe/)

Stops listening to messages posted to channels.



95 changes: 95 additions & 0 deletions docs/supported-commands/Redis/SCRIPTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Redis `scripting` commands (7/22 implemented)

## [EVAL](https://redis.io/commands/eval/)

Executes a server-side Lua script.

## [EVALSHA](https://redis.io/commands/evalsha/)

Executes a server-side Lua script by SHA1 digest.

## [SCRIPT](https://redis.io/commands/script/)

A container for Lua scripts management commands.

## [SCRIPT EXISTS](https://redis.io/commands/script-exists/)

Determines whether server-side Lua scripts exist in the script cache.

## [SCRIPT FLUSH](https://redis.io/commands/script-flush/)

Removes all server-side Lua scripts from the script cache.

## [SCRIPT HELP](https://redis.io/commands/script-help/)

Returns helpful text about the different subcommands.

## [SCRIPT LOAD](https://redis.io/commands/script-load/)

Loads a server-side Lua script to the script cache.


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

Executes a read-only server-side Lua script by SHA1 digest.

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

Executes a read-only server-side Lua script.

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

Invokes a function.

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

Invokes a read-only function.

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

A container for function commands.

#### [FUNCTION DELETE](https://redis.io/commands/function-delete/) <small>(not implemented)</small>

Deletes a library and its functions.

#### [FUNCTION DUMP](https://redis.io/commands/function-dump/) <small>(not implemented)</small>

Dumps all libraries into a serialized binary payload.

#### [FUNCTION FLUSH](https://redis.io/commands/function-flush/) <small>(not implemented)</small>

Deletes all libraries and functions.

#### [FUNCTION KILL](https://redis.io/commands/function-kill/) <small>(not implemented)</small>

Terminates a function during execution.

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

Returns information about all libraries.

#### [FUNCTION LOAD](https://redis.io/commands/function-load/) <small>(not implemented)</small>

Creates a library.

#### [FUNCTION RESTORE](https://redis.io/commands/function-restore/) <small>(not implemented)</small>

Restores all libraries from a payload.

#### [FUNCTION STATS](https://redis.io/commands/function-stats/) <small>(not implemented)</small>

Returns information about a function during execution.

#### [SCRIPT DEBUG](https://redis.io/commands/script-debug/) <small>(not implemented)</small>

Sets the debug mode of server-side Lua scripts.

#### [SCRIPT KILL](https://redis.io/commands/script-kill/) <small>(not implemented)</small>

Terminates a server-side Lua script during execution.


287 changes: 287 additions & 0 deletions docs/supported-commands/Redis/SERVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# Redis `server` commands (11/70 implemented)

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

Asynchronously saves the database(s) to disk.

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

Returns detailed information about all commands.

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

Returns a count of commands.

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

Returns information about one, multiple or all commands.

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

Returns the number of keys in the database.

## [FLUSHALL](https://redis.io/commands/flushall/)

Removes all keys from all databases.

## [FLUSHDB](https://redis.io/commands/flushdb/)

Remove all keys from the current database.

## [LASTSAVE](https://redis.io/commands/lastsave/)

Returns the Unix timestamp of the last successful save to disk.

## [SAVE](https://redis.io/commands/save/)

Synchronously saves the database(s) to disk.

## [SWAPDB](https://redis.io/commands/swapdb/)

Swaps two Redis databases.

## [TIME](https://redis.io/commands/time/)

Returns the server time.


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

A container for Access List Control commands.

#### [ACL CAT](https://redis.io/commands/acl-cat/) <small>(not implemented)</small>

Lists the ACL categories, or the commands inside a category.

#### [ACL DELUSER](https://redis.io/commands/acl-deluser/) <small>(not implemented)</small>

Deletes ACL users, and terminates their connections.

#### [ACL DRYRUN](https://redis.io/commands/acl-dryrun/) <small>(not implemented)</small>

Simulates the execution of a command by a user, without executing the command.

#### [ACL GENPASS](https://redis.io/commands/acl-genpass/) <small>(not implemented)</small>

Generates a pseudorandom, secure password that can be used to identify ACL users.

#### [ACL GETUSER](https://redis.io/commands/acl-getuser/) <small>(not implemented)</small>

Lists the ACL rules of a user.

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

Dumps the effective rules in ACL file format.

#### [ACL LOAD](https://redis.io/commands/acl-load/) <small>(not implemented)</small>

Reloads the rules from the configured ACL file.

#### [ACL LOG](https://redis.io/commands/acl-log/) <small>(not implemented)</small>

Lists recent security events generated due to ACL rules.

#### [ACL SAVE](https://redis.io/commands/acl-save/) <small>(not implemented)</small>

Saves the effective ACL rules in the configured ACL file.

#### [ACL SETUSER](https://redis.io/commands/acl-setuser/) <small>(not implemented)</small>

Creates and modifies an ACL user and its rules.

#### [ACL USERS](https://redis.io/commands/acl-users/) <small>(not implemented)</small>

Lists all ACL users.

#### [ACL WHOAMI](https://redis.io/commands/acl-whoami/) <small>(not implemented)</small>

Returns the authenticated username of the current connection.

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

Asynchronously rewrites the append-only file to disk.

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

Returns documentary information about one, multiple or all commands.

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

Extracts the key names from an arbitrary command.

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

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

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

Returns a list of command names.

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

A container for server configuration commands.

#### [CONFIG GET](https://redis.io/commands/config-get/) <small>(not implemented)</small>

Returns the effective values of configuration parameters.

#### [CONFIG RESETSTAT](https://redis.io/commands/config-resetstat/) <small>(not implemented)</small>

Resets the server's statistics.

#### [CONFIG REWRITE](https://redis.io/commands/config-rewrite/) <small>(not implemented)</small>

Persists the effective configuration to file.

#### [CONFIG SET](https://redis.io/commands/config-set/) <small>(not implemented)</small>

Sets configuration parameters in-flight.

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

Starts a coordinated failover from a server to one of its replicas.

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

Returns information and statistics about the server.

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

A container for latency diagnostics commands.

#### [LATENCY DOCTOR](https://redis.io/commands/latency-doctor/) <small>(not implemented)</small>

Returns a human-readable latency analysis report.

#### [LATENCY GRAPH](https://redis.io/commands/latency-graph/) <small>(not implemented)</small>

Returns a latency graph for an event.

#### [LATENCY HELP](https://redis.io/commands/latency-help/) <small>(not implemented)</small>

Returns helpful text about the different subcommands.

#### [LATENCY HISTOGRAM](https://redis.io/commands/latency-histogram/) <small>(not implemented)</small>

Returns the cumulative distribution of latencies of a subset or all commands.

#### [LATENCY HISTORY](https://redis.io/commands/latency-history/) <small>(not implemented)</small>

Returns timestamp-latency samples for an event.

#### [LATENCY LATEST](https://redis.io/commands/latency-latest/) <small>(not implemented)</small>

Returns the latest latency samples for all events.

#### [LATENCY RESET](https://redis.io/commands/latency-reset/) <small>(not implemented)</small>

Resets the latency data for one or more events.

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

Displays computer art and the Redis version

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

A container for memory diagnostics commands.

#### [MEMORY DOCTOR](https://redis.io/commands/memory-doctor/) <small>(not implemented)</small>

Outputs a memory problems report.

#### [MEMORY MALLOC-STATS](https://redis.io/commands/memory-malloc-stats/) <small>(not implemented)</small>

Returns the allocator statistics.

#### [MEMORY PURGE](https://redis.io/commands/memory-purge/) <small>(not implemented)</small>

Asks the allocator to release memory.

#### [MEMORY STATS](https://redis.io/commands/memory-stats/) <small>(not implemented)</small>

Returns details about memory usage.

#### [MEMORY USAGE](https://redis.io/commands/memory-usage/) <small>(not implemented)</small>

Estimates the memory usage of a key.

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

A container for module commands.

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

Returns all loaded modules.

#### [MODULE LOAD](https://redis.io/commands/module-load/) <small>(not implemented)</small>

Loads a module.

#### [MODULE LOADEX](https://redis.io/commands/module-loadex/) <small>(not implemented)</small>

Loads a module using extended parameters.

#### [MODULE UNLOAD](https://redis.io/commands/module-unload/) <small>(not implemented)</small>

Unloads a module.

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

Listens for all requests received by the server in real-time.

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

An internal command used in replication.

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

An internal command for configuring the replication stream.

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

Configures a server as replica of another, or promotes it to a master.

#### [RESTORE-ASKING](https://redis.io/commands/restore-asking/) <small>(not implemented)</small>

An internal command for migrating keys in a cluster.

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

Returns the replication role.

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

Synchronously saves the database(s) to disk and shuts down the Redis server.

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

Sets a Redis server as a replica of another, or promotes it to being a master.

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

A container for slow log commands.

#### [SLOWLOG GET](https://redis.io/commands/slowlog-get/) <small>(not implemented)</small>

Returns the slow log's entries.

#### [SLOWLOG HELP](https://redis.io/commands/slowlog-help/) <small>(not implemented)</small>

Show helpful text about the different subcommands

#### [SLOWLOG LEN](https://redis.io/commands/slowlog-len/) <small>(not implemented)</small>

Returns the number of entries in the slow log.

#### [SLOWLOG RESET](https://redis.io/commands/slowlog-reset/) <small>(not implemented)</small>

Clears all entries from the slow log.

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

An internal command used in replication.


72 changes: 72 additions & 0 deletions docs/supported-commands/Redis/SET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Redis `set` commands (17/17 implemented)

## [SADD](https://redis.io/commands/sadd/)

Adds one or more members to a set. Creates the key if it doesn't exist.

## [SCARD](https://redis.io/commands/scard/)

Returns the number of members in a set.

## [SDIFF](https://redis.io/commands/sdiff/)

Returns the difference of multiple sets.

## [SDIFFSTORE](https://redis.io/commands/sdiffstore/)

Stores the difference of multiple sets in a key.

## [SINTER](https://redis.io/commands/sinter/)

Returns the intersect of multiple sets.

## [SINTERCARD](https://redis.io/commands/sintercard/)

Returns the number of members of the intersect of multiple sets.

## [SINTERSTORE](https://redis.io/commands/sinterstore/)

Stores the intersect of multiple sets in a key.

## [SISMEMBER](https://redis.io/commands/sismember/)

Determines whether a member belongs to a set.

## [SMEMBERS](https://redis.io/commands/smembers/)

Returns all members of a set.

## [SMISMEMBER](https://redis.io/commands/smismember/)

Determines whether multiple members belong to a set.

## [SMOVE](https://redis.io/commands/smove/)

Moves a member from one set to another.

## [SPOP](https://redis.io/commands/spop/)

Returns one or more random members from a set after removing them. Deletes the set if the last member was popped.

## [SRANDMEMBER](https://redis.io/commands/srandmember/)

Get one or multiple random members from a set

## [SREM](https://redis.io/commands/srem/)

Removes one or more members from a set. Deletes the set if the last member was removed.

## [SSCAN](https://redis.io/commands/sscan/)

Iterates over members of a set.

## [SUNION](https://redis.io/commands/sunion/)

Returns the union of multiple sets.

## [SUNIONSTORE](https://redis.io/commands/sunionstore/)

Stores the union of multiple sets in a key.



144 changes: 144 additions & 0 deletions docs/supported-commands/Redis/SORTED-SET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Redis `sorted-set` commands (35/35 implemented)

## [BZMPOP](https://redis.io/commands/bzmpop/)

Removes and returns a member by score from one or more sorted sets. Blocks until a member is available otherwise. Deletes the sorted set if the last element was popped.

## [BZPOPMAX](https://redis.io/commands/bzpopmax/)

Removes and returns the member with the highest score from one or more sorted sets. Blocks until a member available otherwise. Deletes the sorted set if the last element was popped.

## [BZPOPMIN](https://redis.io/commands/bzpopmin/)

Removes and returns the member with the lowest score from one or more sorted sets. Blocks until a member is available otherwise. Deletes the sorted set if the last element was popped.

## [ZADD](https://redis.io/commands/zadd/)

Adds one or more members to a sorted set, or updates their scores. Creates the key if it doesn't exist.

## [ZCARD](https://redis.io/commands/zcard/)

Returns the number of members in a sorted set.

## [ZCOUNT](https://redis.io/commands/zcount/)

Returns the count of members in a sorted set that have scores within a range.

## [ZDIFF](https://redis.io/commands/zdiff/)

Returns the difference between multiple sorted sets.

## [ZDIFFSTORE](https://redis.io/commands/zdiffstore/)

Stores the difference of multiple sorted sets in a key.

## [ZINCRBY](https://redis.io/commands/zincrby/)

Increments the score of a member in a sorted set.

## [ZINTER](https://redis.io/commands/zinter/)

Returns the intersect of multiple sorted sets.

## [ZINTERCARD](https://redis.io/commands/zintercard/)

Returns the number of members of the intersect of multiple sorted sets.

## [ZINTERSTORE](https://redis.io/commands/zinterstore/)

Stores the intersect of multiple sorted sets in a key.

## [ZLEXCOUNT](https://redis.io/commands/zlexcount/)

Returns the number of members in a sorted set within a lexicographical range.

## [ZMPOP](https://redis.io/commands/zmpop/)

Returns the highest- or lowest-scoring members from one or more sorted sets after removing them. Deletes the sorted set if the last member was popped.

## [ZMSCORE](https://redis.io/commands/zmscore/)

Returns the score of one or more members in a sorted set.

## [ZPOPMAX](https://redis.io/commands/zpopmax/)

Returns the highest-scoring members from a sorted set after removing them. Deletes the sorted set if the last member was popped.

## [ZPOPMIN](https://redis.io/commands/zpopmin/)

Returns the lowest-scoring members from a sorted set after removing them. Deletes the sorted set if the last member was popped.

## [ZRANDMEMBER](https://redis.io/commands/zrandmember/)

Returns one or more random members from a sorted set.

## [ZRANGE](https://redis.io/commands/zrange/)

Returns members in a sorted set within a range of indexes.

## [ZRANGEBYLEX](https://redis.io/commands/zrangebylex/)

Returns members in a sorted set within a lexicographical range.

## [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore/)

Returns members in a sorted set within a range of scores.

## [ZRANGESTORE](https://redis.io/commands/zrangestore/)

Stores a range of members from sorted set in a key.

## [ZRANK](https://redis.io/commands/zrank/)

Returns the index of a member in a sorted set ordered by ascending scores.

## [ZREM](https://redis.io/commands/zrem/)

Removes one or more members from a sorted set. Deletes the sorted set if all members were removed.

## [ZREMRANGEBYLEX](https://redis.io/commands/zremrangebylex/)

Removes members in a sorted set within a lexicographical range. Deletes the sorted set if all members were removed.

## [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank/)

Removes members in a sorted set within a range of indexes. Deletes the sorted set if all members were removed.

## [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore/)

Removes members in a sorted set within a range of scores. Deletes the sorted set if all members were removed.

## [ZREVRANGE](https://redis.io/commands/zrevrange/)

Returns members in a sorted set within a range of indexes in reverse order.

## [ZREVRANGEBYLEX](https://redis.io/commands/zrevrangebylex/)

Returns members in a sorted set within a lexicographical range in reverse order.

## [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore/)

Returns members in a sorted set within a range of scores in reverse order.

## [ZREVRANK](https://redis.io/commands/zrevrank/)

Returns the index of a member in a sorted set ordered by descending scores.

## [ZSCAN](https://redis.io/commands/zscan/)

Iterates over members and scores of a sorted set.

## [ZSCORE](https://redis.io/commands/zscore/)

Returns the score of a member in a sorted set.

## [ZUNION](https://redis.io/commands/zunion/)

Returns the union of multiple sorted sets.

## [ZUNIONSTORE](https://redis.io/commands/zunionstore/)

Stores the union of multiple sorted sets in a key.



84 changes: 84 additions & 0 deletions docs/supported-commands/Redis/STREAM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Redis `stream` commands (20/20 implemented)

## [XACK](https://redis.io/commands/xack/)

Returns the number of messages that were successfully acknowledged by the consumer group member of a stream.

## [XADD](https://redis.io/commands/xadd/)

Appends a new message to a stream. Creates the key if it doesn't exist.

## [XAUTOCLAIM](https://redis.io/commands/xautoclaim/)

Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to as consumer group member.

## [XCLAIM](https://redis.io/commands/xclaim/)

Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered a consumer group member.

## [XDEL](https://redis.io/commands/xdel/)

Returns the number of messages after removing them from a stream.

## [XGROUP CREATE](https://redis.io/commands/xgroup-create/)

Creates a consumer group.

## [XGROUP CREATECONSUMER](https://redis.io/commands/xgroup-createconsumer/)

Creates a consumer in a consumer group.

## [XGROUP DELCONSUMER](https://redis.io/commands/xgroup-delconsumer/)

Deletes a consumer from a consumer group.

## [XGROUP DESTROY](https://redis.io/commands/xgroup-destroy/)

Destroys a consumer group.

## [XGROUP SETID](https://redis.io/commands/xgroup-setid/)

Sets the last-delivered ID of a consumer group.

## [XINFO CONSUMERS](https://redis.io/commands/xinfo-consumers/)

Returns a list of the consumers in a consumer group.

## [XINFO GROUPS](https://redis.io/commands/xinfo-groups/)

Returns a list of the consumer groups of a stream.

## [XINFO STREAM](https://redis.io/commands/xinfo-stream/)

Returns information about a stream.

## [XLEN](https://redis.io/commands/xlen/)

Return the number of messages in a stream.

## [XPENDING](https://redis.io/commands/xpending/)

Returns the information and entries from a stream consumer group's pending entries list.

## [XRANGE](https://redis.io/commands/xrange/)

Returns the messages from a stream within a range of IDs.

## [XREAD](https://redis.io/commands/xread/)

Returns messages from multiple streams with IDs greater than the ones requested. Blocks until a message is available otherwise.

## [XREADGROUP](https://redis.io/commands/xreadgroup/)

Returns new or historical messages from a stream for a consumer in a group. Blocks until a message is available otherwise.

## [XREVRANGE](https://redis.io/commands/xrevrange/)

Returns the messages from a stream within a range of IDs in reverse order.

## [XTRIM](https://redis.io/commands/xtrim/)

Deletes messages from the beginning of a stream.



92 changes: 92 additions & 0 deletions docs/supported-commands/Redis/STRING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Redis `string` commands (22/22 implemented)

## [APPEND](https://redis.io/commands/append/)

Appends a string to the value of a key. Creates the key if it doesn't exist.

## [DECR](https://redis.io/commands/decr/)

Decrements the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.

## [DECRBY](https://redis.io/commands/decrby/)

Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist.

## [GET](https://redis.io/commands/get/)

Returns the string value of a key.

## [GETDEL](https://redis.io/commands/getdel/)

Returns the string value of a key after deleting the key.

## [GETEX](https://redis.io/commands/getex/)

Returns the string value of a key after setting its expiration time.

## [GETRANGE](https://redis.io/commands/getrange/)

Returns a substring of the string stored at a key.

## [GETSET](https://redis.io/commands/getset/)

Returns the previous string value of a key after setting it to a new value.

## [INCR](https://redis.io/commands/incr/)

Increments the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.

## [INCRBY](https://redis.io/commands/incrby/)

Increments the integer value of a key by a number. Uses 0 as initial value if the key doesn't exist.

## [INCRBYFLOAT](https://redis.io/commands/incrbyfloat/)

Increment the floating point value of a key by a number. Uses 0 as initial value if the key doesn't exist.

## [LCS](https://redis.io/commands/lcs/)

Finds the longest common substring.

## [MGET](https://redis.io/commands/mget/)

Atomically returns the string values of one or more keys.

## [MSET](https://redis.io/commands/mset/)

Atomically creates or modifies the string values of one or more keys.

## [MSETNX](https://redis.io/commands/msetnx/)

Atomically modifies the string values of one or more keys only when all keys don't exist.

## [PSETEX](https://redis.io/commands/psetex/)

Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn't exist.

## [SET](https://redis.io/commands/set/)

Sets the string value of a key, ignoring its type. The key is created if it doesn't exist.

## [SETEX](https://redis.io/commands/setex/)

Sets the string value and expiration time of a key. Creates the key if it doesn't exist.

## [SETNX](https://redis.io/commands/setnx/)

Set the string value of a key only when the key doesn't exist.

## [SETRANGE](https://redis.io/commands/setrange/)

Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist.

## [STRLEN](https://redis.io/commands/strlen/)

Returns the length of a string value.

## [SUBSTR](https://redis.io/commands/substr/)

Returns a substring from a string value.



24 changes: 24 additions & 0 deletions docs/supported-commands/Redis/TRANSACTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Redis `transactions` commands (5/5 implemented)

## [DISCARD](https://redis.io/commands/discard/)

Discards a transaction.

## [EXEC](https://redis.io/commands/exec/)

Executes all commands in a transaction.

## [MULTI](https://redis.io/commands/multi/)

Starts a transaction.

## [UNWATCH](https://redis.io/commands/unwatch/)

Forgets about watched keys of a transaction.

## [WATCH](https://redis.io/commands/watch/)

Monitors changes to keys to determine the execution of a transaction.



44 changes: 44 additions & 0 deletions docs/supported-commands/RedisBloom/BF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# RedisBloom `bf` commands (10/10 implemented)

## [BF.RESERVE](https://redis.io/commands/bf.reserve/)

Creates a new Bloom Filter

## [BF.ADD](https://redis.io/commands/bf.add/)

Adds an item to a Bloom Filter

## [BF.MADD](https://redis.io/commands/bf.madd/)

Adds one or more items to a Bloom Filter. A filter will be created if it does not exist

## [BF.INSERT](https://redis.io/commands/bf.insert/)

Adds one or more items to a Bloom Filter. A filter will be created if it does not exist

## [BF.EXISTS](https://redis.io/commands/bf.exists/)

Checks whether an item exists in a Bloom Filter

## [BF.MEXISTS](https://redis.io/commands/bf.mexists/)

Checks whether one or more items exist in a Bloom Filter

## [BF.SCANDUMP](https://redis.io/commands/bf.scandump/)

Begins an incremental save of the bloom filter

## [BF.LOADCHUNK](https://redis.io/commands/bf.loadchunk/)

Restores a filter previously saved using SCANDUMP

## [BF.INFO](https://redis.io/commands/bf.info/)

Returns information about a Bloom Filter

## [BF.CARD](https://redis.io/commands/bf.card/)

Returns the cardinality of a Bloom filter



52 changes: 52 additions & 0 deletions docs/supported-commands/RedisBloom/CF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# RedisBloom `cf` commands (12/12 implemented)

## [CF.RESERVE](https://redis.io/commands/cf.reserve/)

Creates a new Cuckoo Filter

## [CF.ADD](https://redis.io/commands/cf.add/)

Adds an item to a Cuckoo Filter

## [CF.ADDNX](https://redis.io/commands/cf.addnx/)

Adds an item to a Cuckoo Filter if the item did not exist previously.

## [CF.INSERT](https://redis.io/commands/cf.insert/)

Adds one or more items to a Cuckoo Filter. A filter will be created if it does not exist

## [CF.INSERTNX](https://redis.io/commands/cf.insertnx/)

Adds one or more items to a Cuckoo Filter if the items did not exist previously. A filter will be created if it does not exist

## [CF.EXISTS](https://redis.io/commands/cf.exists/)

Checks whether one or more items exist in a Cuckoo Filter

## [CF.MEXISTS](https://redis.io/commands/cf.mexists/)

Checks whether one or more items exist in a Cuckoo Filter

## [CF.DEL](https://redis.io/commands/cf.del/)

Deletes an item from a Cuckoo Filter

## [CF.COUNT](https://redis.io/commands/cf.count/)

Return the number of times an item might be in a Cuckoo Filter

## [CF.SCANDUMP](https://redis.io/commands/cf.scandump/)

Begins an incremental save of the bloom filter

## [CF.LOADCHUNK](https://redis.io/commands/cf.loadchunk/)

Restores a filter previously saved using SCANDUMP

## [CF.INFO](https://redis.io/commands/cf.info/)

Returns information about a Cuckoo Filter



28 changes: 28 additions & 0 deletions docs/supported-commands/RedisBloom/CMS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# RedisBloom `cms` commands (6/6 implemented)

## [CMS.INITBYDIM](https://redis.io/commands/cms.initbydim/)

Initializes a Count-Min Sketch to dimensions specified by user

## [CMS.INITBYPROB](https://redis.io/commands/cms.initbyprob/)

Initializes a Count-Min Sketch to accommodate requested tolerances.

## [CMS.INCRBY](https://redis.io/commands/cms.incrby/)

Increases the count of one or more items by increment

## [CMS.QUERY](https://redis.io/commands/cms.query/)

Returns the count for one or more items in a sketch

## [CMS.MERGE](https://redis.io/commands/cms.merge/)

Merges several sketches into one sketch

## [CMS.INFO](https://redis.io/commands/cms.info/)

Returns information about a sketch



60 changes: 60 additions & 0 deletions docs/supported-commands/RedisBloom/TDIGEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# RedisBloom `tdigest` commands (14/14 implemented)

## [TDIGEST.CREATE](https://redis.io/commands/tdigest.create/)

Allocates memory and initializes a new t-digest sketch

## [TDIGEST.RESET](https://redis.io/commands/tdigest.reset/)

Resets a t-digest sketch: empty the sketch and re-initializes it.

## [TDIGEST.ADD](https://redis.io/commands/tdigest.add/)

Adds one or more observations to a t-digest sketch

## [TDIGEST.MERGE](https://redis.io/commands/tdigest.merge/)

Merges multiple t-digest sketches into a single sketch

## [TDIGEST.MIN](https://redis.io/commands/tdigest.min/)

Returns the minimum observation value from a t-digest sketch

## [TDIGEST.MAX](https://redis.io/commands/tdigest.max/)

Returns the maximum observation value from a t-digest sketch

## [TDIGEST.QUANTILE](https://redis.io/commands/tdigest.quantile/)

Returns, for each input fraction, an estimation of the value (floating point) that is smaller than the given fraction of observations

## [TDIGEST.CDF](https://redis.io/commands/tdigest.cdf/)

Returns, for each input value, an estimation of the fraction (floating-point) of (observations smaller than the given value + half the observations equal to the given value)

## [TDIGEST.TRIMMED_MEAN](https://redis.io/commands/tdigest.trimmed_mean/)

Returns an estimation of the mean value from the sketch, excluding observation values outside the low and high cutoff quantiles

## [TDIGEST.RANK](https://redis.io/commands/tdigest.rank/)

Returns, for each input value (floating-point), the estimated rank of the value (the number of observations in the sketch that are smaller than the value + half the number of observations that are equal to the value)

## [TDIGEST.REVRANK](https://redis.io/commands/tdigest.revrank/)

Returns, for each input value (floating-point), the estimated reverse rank of the value (the number of observations in the sketch that are larger than the value + half the number of observations that are equal to the value)

## [TDIGEST.BYRANK](https://redis.io/commands/tdigest.byrank/)

Returns, for each input rank, an estimation of the value (floating-point) with that rank

## [TDIGEST.BYREVRANK](https://redis.io/commands/tdigest.byrevrank/)

Returns, for each input reverse rank, an estimation of the value (floating-point) with that reverse rank

## [TDIGEST.INFO](https://redis.io/commands/tdigest.info/)

Returns information and statistics about a t-digest sketch



32 changes: 32 additions & 0 deletions docs/supported-commands/RedisBloom/TOPK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# RedisBloom `topk` commands (7/7 implemented)

## [TOPK.RESERVE](https://redis.io/commands/topk.reserve/)

Initializes a TopK with specified parameters

## [TOPK.ADD](https://redis.io/commands/topk.add/)

Increases the count of one or more items by increment

## [TOPK.INCRBY](https://redis.io/commands/topk.incrby/)

Increases the count of one or more items by increment

## [TOPK.QUERY](https://redis.io/commands/topk.query/)

Checks whether one or more items are in a sketch

## [TOPK.COUNT](https://redis.io/commands/topk.count/)

Return the count for one or more items are in a sketch

## [TOPK.LIST](https://redis.io/commands/topk.list/)

Return full list of items in Top K list

## [TOPK.INFO](https://redis.io/commands/topk.info/)

Returns information about a sketch



92 changes: 92 additions & 0 deletions docs/supported-commands/RedisJson/JSON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# RedisJson `json` commands (22/22 implemented)

## [JSON.DEL](https://redis.io/commands/json.del/)

Deletes a value

## [JSON.FORGET](https://redis.io/commands/json.forget/)

Deletes a value

## [JSON.GET](https://redis.io/commands/json.get/)

Gets the value at one or more paths in JSON serialized form

## [JSON.TOGGLE](https://redis.io/commands/json.toggle/)

Toggles a boolean value

## [JSON.CLEAR](https://redis.io/commands/json.clear/)

Clears all values from an array or an object and sets numeric values to `0`

## [JSON.SET](https://redis.io/commands/json.set/)

Sets or updates the JSON value at a path

## [JSON.MSET](https://redis.io/commands/json.mset/)

Sets or updates the JSON value of one or more keys

## [JSON.MERGE](https://redis.io/commands/json.merge/)

Merges a given JSON value into matching paths. Consequently, JSON values at matching paths are updated, deleted, or expanded with new children

## [JSON.MGET](https://redis.io/commands/json.mget/)

Returns the values at a path from one or more keys

## [JSON.NUMINCRBY](https://redis.io/commands/json.numincrby/)

Increments the numeric value at path by a value

## [JSON.NUMMULTBY](https://redis.io/commands/json.nummultby/)

Multiplies the numeric value at path by a value

## [JSON.STRAPPEND](https://redis.io/commands/json.strappend/)

Appends a string to a JSON string value at path

## [JSON.STRLEN](https://redis.io/commands/json.strlen/)

Returns the length of the JSON String at path in key

## [JSON.ARRAPPEND](https://redis.io/commands/json.arrappend/)

Append one or more json values into the array at path after the last element in it.

## [JSON.ARRINDEX](https://redis.io/commands/json.arrindex/)

Returns the index of the first occurrence of a JSON scalar value in the array at path

## [JSON.ARRINSERT](https://redis.io/commands/json.arrinsert/)

Inserts the JSON scalar(s) value at the specified index in the array at path

## [JSON.ARRLEN](https://redis.io/commands/json.arrlen/)

Returns the length of the array at path

## [JSON.ARRPOP](https://redis.io/commands/json.arrpop/)

Removes and returns the element at the specified index in the array at path

## [JSON.ARRTRIM](https://redis.io/commands/json.arrtrim/)

Trims the array at path to contain only the specified inclusive range of indices from start to stop

## [JSON.OBJKEYS](https://redis.io/commands/json.objkeys/)

Returns the JSON keys of the object at path

## [JSON.OBJLEN](https://redis.io/commands/json.objlen/)

Returns the number of keys of the object at path

## [JSON.TYPE](https://redis.io/commands/json.type/)

Returns the type of the JSON value at path



Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# Search commands

Module currently not implemented in fakeredis.


### Unsupported search commands
## Unsupported search commands
> To implement support for a command, see [here](/guides/implement-command/)
#### [FT.CREATE](https://redis.io/commands/ft.create/) <small>(not implemented)</small>
@@ -107,24 +103,3 @@ Reads from a cursor
Deletes a cursor



### Unsupported suggestion commands
> To implement support for a command, see [here](/guides/implement-command/)
#### [FT.SUGADD](https://redis.io/commands/ft.sugadd/) <small>(not implemented)</small>

Adds a suggestion string to an auto-complete suggestion dictionary

#### [FT.SUGGET](https://redis.io/commands/ft.sugget/) <small>(not implemented)</small>

Gets completion suggestions for a prefix

#### [FT.SUGDEL](https://redis.io/commands/ft.sugdel/) <small>(not implemented)</small>

Deletes a string from a suggestion index

#### [FT.SUGLEN](https://redis.io/commands/ft.suglen/) <small>(not implemented)</small>

Gets the size of an auto-complete suggestion dictionary


21 changes: 21 additions & 0 deletions docs/supported-commands/RedisSearch/SUGGESTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

## Unsupported suggestion commands
> To implement support for a command, see [here](/guides/implement-command/)
#### [FT.SUGADD](https://redis.io/commands/ft.sugadd/) <small>(not implemented)</small>

Adds a suggestion string to an auto-complete suggestion dictionary

#### [FT.SUGGET](https://redis.io/commands/ft.sugget/) <small>(not implemented)</small>

Gets completion suggestions for a prefix

#### [FT.SUGDEL](https://redis.io/commands/ft.sugdel/) <small>(not implemented)</small>

Deletes a string from a suggestion index

#### [FT.SUGLEN](https://redis.io/commands/ft.suglen/) <small>(not implemented)</small>

Gets the size of an auto-complete suggestion dictionary


72 changes: 72 additions & 0 deletions docs/supported-commands/RedisTimeSeries/TIMESERIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# RedisTimeSeries `timeseries` commands (17/17 implemented)

## [TS.CREATE](https://redis.io/commands/ts.create/)

Create a new time series

## [TS.DEL](https://redis.io/commands/ts.del/)

Delete all samples between two timestamps for a given time series

## [TS.ALTER](https://redis.io/commands/ts.alter/)

Update the retention, chunk size, duplicate policy, and labels of an existing time series

## [TS.ADD](https://redis.io/commands/ts.add/)

Append a sample to a time series

## [TS.MADD](https://redis.io/commands/ts.madd/)

Append new samples to one or more time series

## [TS.INCRBY](https://redis.io/commands/ts.incrby/)

Increase the value of the sample with the maximum existing timestamp, or create a new sample with a value equal to the value of the sample with the maximum existing timestamp with a given increment

## [TS.DECRBY](https://redis.io/commands/ts.decrby/)

Decrease the value of the sample with the maximum existing timestamp, or create a new sample with a value equal to the value of the sample with the maximum existing timestamp with a given decrement

## [TS.CREATERULE](https://redis.io/commands/ts.createrule/)

Create a compaction rule

## [TS.DELETERULE](https://redis.io/commands/ts.deleterule/)

Delete a compaction rule

## [TS.RANGE](https://redis.io/commands/ts.range/)

Query a range in forward direction

## [TS.REVRANGE](https://redis.io/commands/ts.revrange/)

Query a range in reverse direction

## [TS.MRANGE](https://redis.io/commands/ts.mrange/)

Query a range across multiple time series by filters in forward direction

## [TS.MREVRANGE](https://redis.io/commands/ts.mrevrange/)

Query a range across multiple time-series by filters in reverse direction

## [TS.GET](https://redis.io/commands/ts.get/)

Get the sample with the highest timestamp from a given time series

## [TS.MGET](https://redis.io/commands/ts.mget/)

Get the sample with the highest timestamp from each time series matching a specific filter

## [TS.INFO](https://redis.io/commands/ts.info/)

Returns information and statistics for a time series

## [TS.QUERYINDEX](https://redis.io/commands/ts.queryindex/)

Get all time series keys matching a filter list



11 changes: 11 additions & 0 deletions docs/supported-commands/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Supported commands

Commands from [Redis][1], [RedisJSON][2], [RedisTimeSeries][3], and [RedisBloom][4] are supported.

Additionally, [Dragonfly specific commands][dragonfly] are also supported.

[1]: /supported-commands/Redis/BITMAP/
[2]: /supported-commands/RedisJSON/JSON/
[3]: /supported-commands/RedisTimeSeries/TIMESERIES/
[4]: /supported-commands/RedisBloom/BF/
[dragonfly]: /supported-commands/DRAGONFLY/
40 changes: 40 additions & 0 deletions docs/valkey-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Support for valkey

[Valkey][1] is an open source (BSD) high-performance key/value datastore that supports a variety of workloads such as
caching, message queues, and can act as a primary database.
The project was forked from the open source Redis project right before the transition to their new source available
licenses.

FakeRedis can be used as a valkey replacement for testing and development purposes as well.

To make the process more straightforward, the `FakeValkey` class sets all relevant arguments in `FakeRedis` to the
valkey values.

```python
from fakeredis import FakeValkey

valkey = FakeValkey()
valkey.set("key", "value")
print(valkey.get("key"))
```

Alternatively, you can start a thread with a Fake Valkey server.

```python
from threading import Thread
from fakeredis import TcpFakeServer

server_address = ("127.0.0.1", 6379)
server = TcpFakeServer(server_address, server_type="valkey")
t = Thread(target=server.serve_forever, daemon=True)
t.start()

import valkey

r = valkey.Valkey(host=server_address[0], port=server_address[1])
r.set("foo", "bar")
assert r.get("foo") == b"bar"

```

[1]: https://github.com/valkey-io/valkey
45 changes: 41 additions & 4 deletions fakeredis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
from ._server import FakeServer, FakeRedis, FakeStrictRedis, FakeConnection, FakeRedisConnSingleton
import sys

from ._connection import (
FakeRedis,
FakeStrictRedis,
FakeConnection,
)
from ._server import FakeServer
from ._valkey import FakeValkey, FakeAsyncValkey, FakeStrictValkey
from .aioredis import (
FakeRedis as FakeAsyncRedis,
FakeConnection as FakeAsyncConnection,
)

if sys.version_info >= (3, 11):
from ._tcp_server import TcpFakeServer
else:

class TcpFakeServer:
def __init__(self, *args, **kwargs):
raise NotImplementedError("TcpFakeServer is only available in Python 3.11+")


try:
from importlib import metadata
except ImportError: # for Python<3.8
except ImportError: # for Python < 3.8
import importlib_metadata as metadata # type: ignore
__version__ = metadata.version("fakeredis")
__author__ = "Daniel Moran"
__maintainer__ = "Daniel Moran"
__email__ = "daniel@moransoftware.ca"
__license__ = "BSD-3-Clause"
__url__ = "https://github.com/cunla/fakeredis-py"
__bugtrack_url__ = "https://github.com/cunla/fakeredis-py/issues"


__all__ = ["FakeServer", "FakeRedis", "FakeStrictRedis", "FakeConnection", "FakeRedisConnSingleton"]
__all__ = [
"FakeServer",
"FakeRedis",
"FakeStrictRedis",
"FakeConnection",
"FakeAsyncRedis",
"FakeAsyncConnection",
"TcpFakeServer",
"FakeValkey",
"FakeAsyncValkey",
"FakeStrictValkey",
]
364 changes: 224 additions & 140 deletions fakeredis/_basefakesocket.py

Large diffs are not rendered by default.

115 changes: 56 additions & 59 deletions fakeredis/_command_args_parsing.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
from typing import Tuple, List, Dict, Any
from typing import Tuple, List, Dict, Any, Sequence, Optional

from . import _msgs as msgs
from ._commands import Int, Float
from ._helpers import SimpleError, null_terminate


def _count_params(s: str):
def _count_params(s: str) -> int:
res = 0
while s[res] in '.+*~':
while res < len(s) and s[res] in ".+*~":
res += 1
return res


def _encode_arg(s: str):
return s[_count_params(s):].encode()
def _encode_arg(s: str) -> bytes:
return s[_count_params(s) :].encode()


def _default_value(s: str):
if s[0] == '~':
def _default_value(s: str) -> Any:
if s[0] == "~":
return None
ind = _count_params(s)
if ind == 0:
@@ -29,39 +29,35 @@ def _default_value(s: str):


def extract_args(
actual_args: Tuple[bytes, ...],
expected: Tuple[str, ...],
error_on_unexpected: bool = True,
left_from_first_unexpected: bool = True,
) -> Tuple[List, List]:
"""Parse argument values
actual_args: Tuple[bytes, ...],
expected: Tuple[str, ...],
error_on_unexpected: bool = True,
left_from_first_unexpected: bool = True,
exception: Optional[str] = None,
) -> Tuple[List[Any], Sequence[Any]]:
"""Parse argument values.
Extract from actual arguments which arguments exist and their value if relevant.
Parameters:
- actual_args:
The actual arguments to parse
- expected:
Arguments to look for, see below explanation.
- error_on_unexpected:
Should an error be raised when actual_args contain an unexpected argument?
- left_from_first_unexpected:
Once reaching an unexpected argument in actual_args,
Should parsing stop?
Returns:
- List of values for expected arguments.
- List of remaining args.
:param actual_args: The actual arguments to parse
:param expected: Arguments to look for, see below explanation.
:param error_on_unexpected: Should an error be raised when actual_args contain an unexpected argument?
:param left_from_first_unexpected: Once reaching an unexpected argument in actual_args, Should parsing stop?
:param exception: What exception msg to raise
:returns:
- List of values for expected arguments.
- List of remaining args.
An expected argument can have parameters:
- A numerical (Int) parameter is identified with +.
- A float (Float) parameter is identified with .
- A non-numerical parameter is identified with a *.
- A argument with potentially ~ or = between the
argument name and the value is identified with a ~.
- A numerical (Int) parameter is identified with '+'
- A float (Float) parameter is identified with '.'
- A non-numerical parameter is identified with a '*'
- An argument with potentially ~ or = between the
argument name and the value is identified with a '~'
- A numberical argument with potentially ~ or = between the
argument name and the value is identified with a ~+.
argument name and the value marked with a '~+'
e.g.
E.g.
'++limit' will translate as an argument with 2 int parameters.
>>> extract_args((b'nx', b'ex', b'324', b'xx',), ('nx', 'xx', '+ex', 'keepttl'))
@@ -72,46 +68,43 @@ def extract_args(
('~+maxlen', 'nx', 'xx', '+ex', 'keepttl'))
10, [True, True, 324, False], None
"""
args_info: Dict[bytes, int] = {
_encode_arg(k): (i, _count_params(k))
for (i, k) in enumerate(expected)
}

def _parse_params(
key: str,
ind: int,
actual_args: Tuple[bytes, ...]) -> Tuple[Any, int]:
"""
Parse an argument from actual args.
args_info: Dict[bytes, Tuple[int, int]] = {_encode_arg(k): (i, _count_params(k)) for (i, k) in enumerate(expected)}

def _parse_params(key: bytes, ind: int, _actual_args: Tuple[bytes, ...]) -> Tuple[Any, int]:
"""Parse an argument from actual args.
:param key: Argument name to parse
:param ind: index of argument in actual_args
:param _actual_args: actual args
"""
pos, expected_following = args_info[key]
argument_name = expected[pos]

# Deal with parameters with optional ~/= before numerical value.
if argument_name[0] == '~':
if ind + 1 >= len(actual_args):
arg: Any
if argument_name[0] == "~":
if ind + 1 >= len(_actual_args):
raise SimpleError(msgs.SYNTAX_ERROR_MSG)
if actual_args[ind + 1] != b'~' and actual_args[ind + 1] != b'=':
arg, parsed = actual_args[ind + 1], 1
elif ind + 2 >= len(actual_args):
if _actual_args[ind + 1] != b"~" and _actual_args[ind + 1] != b"=":
arg, _parsed = _actual_args[ind + 1], 1
elif ind + 2 >= len(_actual_args):
raise SimpleError(msgs.SYNTAX_ERROR_MSG)
else:
arg, parsed = actual_args[ind + 2], 2
if argument_name[1] == '+':
arg, _parsed = _actual_args[ind + 2], 2
if argument_name[1] == "+":
arg = Int.decode(arg)
return arg, parsed
return arg, _parsed
# Boolean parameters
if expected_following == 0:
return True, 0

if ind + expected_following >= len(actual_args):
if ind + expected_following >= len(_actual_args):
raise SimpleError(msgs.SYNTAX_ERROR_MSG)
temp_res = []
for i in range(expected_following):
curr_arg = actual_args[ind + i + 1]
if argument_name[i] == '+':
curr_arg: Any = _actual_args[ind + i + 1]
if argument_name[i] == "+":
curr_arg = Int.decode(curr_arg)
elif argument_name[i] == '.':
elif argument_name[i] == ".":
curr_arg = Float.decode(curr_arg)
temp_res.append(curr_arg)

@@ -120,13 +113,13 @@ def _parse_params(
else:
return temp_res, expected_following

results: List = [_default_value(key) for key in expected]
results: List[Any] = [_default_value(key) for key in expected]
left_args = []
i = 0
while i < len(actual_args):
found = False
for key in args_info:
if null_terminate(actual_args[i]).lower() == key:
if null_terminate(actual_args[i]) == key:
arg_position, _ = args_info[key]
results[arg_position], parsed = _parse_params(key, i, actual_args)
i += parsed
@@ -135,7 +128,11 @@ def _parse_params(

if not found:
if error_on_unexpected:
raise SimpleError(msgs.SYNTAX_ERROR_MSG)
raise (
SimpleError(msgs.SYNTAX_ERROR_MSG)
if exception is None
else SimpleError(exception.format(actual_args[i]))
)
if left_from_first_unexpected:
return results, actual_args[i:]
left_args.append(actual_args[i])
338 changes: 196 additions & 142 deletions fakeredis/_commands.py

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions fakeredis/_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import inspect
import queue
import sys
import uuid
import warnings
from typing import Tuple, Any, List, Optional, Set

from ._server import FakeBaseConnectionMixin, FakeServer, VersionType

if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self

import redis

from fakeredis._fakesocket import FakeSocket
from fakeredis._helpers import FakeSelector
from . import _msgs as msgs


class FakeConnection(FakeBaseConnectionMixin, redis.Connection):
def connect(self) -> None:
super().connect()
# The selector is set in redis.Connection.connect() after _connect() is called
self._selector: Optional[FakeSelector] = FakeSelector(self._sock)

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,
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:
return True
if not self._sock:
self.connect()
# We use check_can_read rather than can_read, because on redis-py<3.2,
# FakeSelector inherits from a stub BaseSelector which doesn't
# implement can_read. Normally can_read provides retries on EINTR,
# but that's not necessary for the implementation of
# FakeSelector.check_can_read.
return self._selector is not None and self._selector.check_can_read(timeout)

def _decode(self, response: Any) -> Any:
if isinstance(response, list):
return [self._decode(item) for item in response]
elif isinstance(response, bytes):
return self.encoder.decode(response)
else:
return response

def read_response(self, **kwargs: Any) -> Any: # type: ignore
if not self._sock:
raise redis.ConnectionError(msgs.CONNECTION_ERROR_MSG)
if not self._server.connected:
try:
response = self._sock.responses.get_nowait()
except queue.Empty:
if kwargs.get("disconnect_on_error", True):
self.disconnect()
raise redis.ConnectionError(msgs.CONNECTION_ERROR_MSG)
else:
response = self._sock.responses.get()
if isinstance(response, (redis.ResponseError, redis.AuthenticationError)):
raise response
if kwargs.get("disable_decoding", False):
return response
else:
return self._decode(response)

def repr_pieces(self) -> List[Tuple[str, Any]]:
pieces = [("server", self._server), ("db", self.db)]
if self.client_name:
pieces.append(("client_name", self.client_name))
return pieces

def __str__(self) -> str:
return self.server_key


class FakeRedisMixin:
def __init__(
self,
*args: Any,
server: Optional[FakeServer] = None,
version: VersionType = (7,),
server_type: str = "redis",
lua_modules: Optional[Set[str]] = None,
**kwargs: Any,
) -> None:
# Interpret the positional and keyword arguments according to the
# version of redis in use.
parameters = list(inspect.signature(redis.Redis.__init__).parameters.values())[1:]
# Convert args => kwargs
kwargs.update({parameters[i].name: args[i] for i in range(len(args))})
kwargs.setdefault("host", uuid.uuid4().hex)
kwds = {
p.name: kwargs.get(p.name, p.default)
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)
# Adapted from redis-py
if charset is not None:
warnings.warn(DeprecationWarning('"charset" is deprecated. Use "encoding" instead'))
kwds["encoding"] = charset
if errors is not None:
warnings.warn(DeprecationWarning('"errors" is deprecated. Use "encoding_errors" instead'))
kwds["encoding_errors"] = errors
conn_pool_args = {
"host",
"port",
"db",
"username",
"password",
"socket_timeout",
"encoding",
"encoding_errors",
"decode_responses",
"retry_on_timeout",
"max_connections",
"health_check_interval",
"client_name",
"connected",
"server",
}
connection_kwargs = {
"connection_class": FakeConnection,
"version": version,
"server_type": server_type,
"lua_modules": lua_modules,
}
connection_kwargs.update({arg: kwds[arg] for arg in conn_pool_args if arg in kwds})
kwds["connection_pool"] = redis.connection.ConnectionPool(**connection_kwargs) # type: ignore
kwds.pop("server", None)
kwds.pop("connected", None)
kwds.pop("version", None)
kwds.pop("server_type", None)
kwds.pop("lua_modules", None)
super().__init__(**kwds)

@classmethod
def from_url(cls, *args: Any, **kwargs: Any) -> Self:
kwargs.setdefault("version", "7.4")
kwargs.setdefault("server_type", "redis")
pool = redis.ConnectionPool.from_url(*args, **kwargs)
# Now override how it creates connections
pool.connection_class = FakeConnection
return cls(connection_pool=pool, *args, **kwargs)


class FakeStrictRedis(FakeRedisMixin, redis.StrictRedis): # type: ignore
pass


class FakeRedis(FakeRedisMixin, redis.Redis): # type: ignore
pass
63 changes: 46 additions & 17 deletions fakeredis/_fakesocket.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
from fakeredis.stack import JSONCommandsMixin
from typing import Optional, Set, Any

from fakeredis.commands_mixins import (
BitmapCommandsMixin,
ConnectionCommandsMixin,
GenericCommandsMixin,
GeoCommandsMixin,
HashCommandsMixin,
ListCommandsMixin,
PubSubCommandsMixin,
ScriptingCommandsMixin,
ServerCommandsMixin,
StringCommandsMixin,
TransactionsCommandsMixin,
SetCommandsMixin,
StreamsCommandsMixin,
AclCommandsMixin,
)
from fakeredis.stack import (
JSONCommandsMixin,
BFCommandsMixin,
CFCommandsMixin,
CMSCommandsMixin,
TopkCommandsMixin,
TDigestCommandsMixin,
TimeSeriesCommandsMixin,
)
from ._basefakesocket import BaseFakeSocket
from .commands_mixins.bitmap_mixin import BitmapCommandsMixin
from .commands_mixins.connection_mixin import ConnectionCommandsMixin
from .commands_mixins.generic_mixin import GenericCommandsMixin
from .commands_mixins.geo_mixin import GeoCommandsMixin
from .commands_mixins.hash_mixin import HashCommandsMixin
from .commands_mixins.list_mixin import ListCommandsMixin
from .commands_mixins.pubsub_mixin import PubSubCommandsMixin
from .commands_mixins.scripting_mixin import ScriptingCommandsMixin
from .commands_mixins.server_mixin import ServerCommandsMixin
from .commands_mixins.set_mixin import SetCommandsMixin
from ._server import FakeServer
from .commands_mixins.sortedset_mixin import SortedSetCommandsMixin
from .commands_mixins.streams_mixin import StreamsCommandsMixin
from .commands_mixins.string_mixin import StringCommandsMixin
from .commands_mixins.transactions_mixin import TransactionsCommandsMixin
from .server_specific_commands import DragonflyCommandsMixin


class FakeSocket(
@@ -33,7 +48,21 @@ class FakeSocket(
StreamsCommandsMixin,
JSONCommandsMixin,
GeoCommandsMixin,
BFCommandsMixin,
CFCommandsMixin,
CMSCommandsMixin,
TopkCommandsMixin,
TDigestCommandsMixin,
TimeSeriesCommandsMixin,
DragonflyCommandsMixin,
AclCommandsMixin,
):

def __init__(self, server, db):
super(FakeSocket, self).__init__(server, db)
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, *args, lua_modules=lua_modules, **kwargs)
139 changes: 73 additions & 66 deletions fakeredis/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,205 +1,212 @@
from collections import defaultdict

import re
import threading
import time
import weakref
from collections import defaultdict
from collections.abc import MutableMapping
from typing import Any, Set, Callable, Dict, Optional, Iterator


class SimpleString:
def __init__(self, value):
def __init__(self, value: bytes) -> None:
assert isinstance(value, bytes)
self.value = value

@classmethod
def decode(cls, value):
def decode(cls, value: bytes) -> bytes:
return value


class SimpleError(Exception):
"""Exception that will be turned into a frontend-specific exception."""

def __init__(self, value):
def __init__(self, value: str) -> None:
assert isinstance(value, str)
self.value = value


class NoResponse:
"""Returned by pub/sub commands to indicate that no response should be returned"""

pass


OK = SimpleString(b'OK')
QUEUED = SimpleString(b'QUEUED')
BGSAVE_STARTED = SimpleString(b'Background saving started')
OK = SimpleString(b"OK")
QUEUED = SimpleString(b"QUEUED")
BGSAVE_STARTED = SimpleString(b"Background saving started")


def current_time() -> int:
return int(time.time() * 1000)


def null_terminate(s):
def null_terminate(s: bytes) -> bytes:
# Redis uses C functions on some strings, which means they stop at the
# first NULL.
ind = s.find(b'\0')
ind = s.find(b"\0")
if ind > -1:
return s[:ind].lower()
return s.lower()


def casematch(a, b):
def casematch(a: bytes, b: bytes) -> bool:
return null_terminate(a) == null_terminate(b)


def encode_command(s):
return s.decode(encoding='utf-8', errors='replace').lower()
def decode_command_bytes(s: bytes) -> str:
return s.decode(encoding="utf-8", errors="replace").lower()


def compile_pattern(pattern):
"""Compile a glob pattern (e.g. for keys) to a bytes regex.
def compile_pattern(pattern_bytes: bytes) -> re.Pattern: # type: ignore
"""Compile a glob pattern (e.g., for keys) to a `bytes` regex.
fnmatch.fnmatchcase doesn't work for this, because it uses different
`fnmatch.fnmatchcase` doesn't work for this because it uses different
escaping rules to redis, uses ! instead of ^ to negate a character set,
and handles invalid cases (such as a [ without a ]) differently. This
implementation was written by studying the redis implementation.
"""
# It's easier to work with text than bytes, because indexing bytes
# doesn't behave the same in Python 3. Latin-1 will round-trip safely.
pattern = pattern.decode('latin-1', )
parts = ['^']
pattern: str = pattern_bytes.decode(
"latin-1",
)
parts = ["^"]
i = 0
pattern_len = len(pattern)
while i < pattern_len:
c = pattern[i]
i += 1
if c == '?':
parts.append('.')
elif c == '*':
parts.append('.*')
elif c == '\\':
if c == "?":
parts.append(".")
elif c == "*":
parts.append(".*")
elif c == "\\":
if i == pattern_len:
i -= 1
parts.append(re.escape(pattern[i]))
i += 1
elif c == '[':
parts.append('[')
if i < pattern_len and pattern[i] == '^':
elif c == "[":
parts.append("[")
if i < pattern_len and pattern[i] == "^":
i += 1
parts.append('^')
parts.append("^")
parts_len = len(parts) # To detect if anything was added
while i < pattern_len:
if pattern[i] == '\\' and i + 1 < pattern_len:
if pattern[i] == "\\" and i + 1 < pattern_len:
i += 1
parts.append(re.escape(pattern[i]))
elif pattern[i] == ']':
elif pattern[i] == "]":
i += 1
break
elif i + 2 < pattern_len and pattern[i + 1] == '-':
elif i + 2 < pattern_len and pattern[i + 1] == "-":
start = pattern[i]
end = pattern[i + 2]
if start > end:
start, end = end, start
parts.append(re.escape(start) + '-' + re.escape(end))
parts.append(re.escape(start) + "-" + re.escape(end))
i += 2
else:
parts.append(re.escape(pattern[i]))
i += 1
if len(parts) == parts_len:
if parts[-1] == '[':
if parts[-1] == "[":
# Empty group - will never match
parts[-1] = '(?:$.)'
parts[-1] = "(?:$.)"
else:
# Negated empty group - matches any character
assert parts[-1] == '^'
assert parts[-1] == "^"
parts.pop()
parts[-1] = '.'
parts[-1] = "."
else:
parts.append(']')
parts.append("]")
else:
parts.append(re.escape(c))
parts.append('\\Z')
regex = ''.join(parts).encode('latin-1')
return re.compile(regex, re.S)
parts.append("\\Z")
regex: bytes = "".join(parts).encode("latin-1")
return re.compile(regex, flags=re.S)


class Database(MutableMapping):
def __init__(self, lock, *args, **kwargs):
self._dict = dict(*args, **kwargs)
class Database(MutableMapping): # type: ignore
def __init__(self, lock: Optional[threading.Lock], *args: Any, **kwargs: Any) -> None:
self._dict: Dict[bytes, Any] = dict(*args, **kwargs)
self.time = 0.0
self._watches = defaultdict(weakref.WeakSet) # key to set of connections
# key to the set of connections
self._watches: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet)
self.condition = threading.Condition(lock)
self._change_callbacks = set()
self._change_callbacks: Set[Callable[[], None]] = set()

def swap(self, other):
def swap(self, other: "Database") -> None:
self._dict, other._dict = other._dict, self._dict
self.time, other.time = other.time, self.time

def notify_watch(self, key):
def notify_watch(self, key: bytes) -> None:
for sock in self._watches.get(key, set()):
sock.notify_watch()
self.condition.notify_all()
for callback in self._change_callbacks:
callback()

def add_watch(self, key, sock):
def add_watch(self, key: bytes, sock: Any) -> None:
self._watches[key].add(sock)

def remove_watch(self, key, sock):
def remove_watch(self, key: bytes, sock: Any) -> None:
watches = self._watches[key]
watches.discard(sock)
if not watches:
del self._watches[key]

def add_change_callback(self, callback):
def add_change_callback(self, callback: Callable[[], None]) -> None:
self._change_callbacks.add(callback)

def remove_change_callback(self, callback):
def remove_change_callback(self, callback: Callable[[], None]) -> None:
self._change_callbacks.remove(callback)

def clear(self):
def clear(self) -> None:
for key in self:
self.notify_watch(key)
self._dict.clear()

def expired(self, item):
def expired(self, item: Any) -> bool:
return item.expireat is not None and item.expireat < self.time

def _remove_expired(self):
def _remove_expired(self) -> None:
for key in list(self._dict):
item = self._dict[key]
if self.expired(item):
del self._dict[key]

def __getitem__(self, key):
def __getitem__(self, key: bytes) -> Any:
item = self._dict[key]
if self.expired(item):
del self._dict[key]
raise KeyError(key)
return item

def __setitem__(self, key, value):
def __setitem__(self, key: bytes, value: Any) -> None:
self._dict[key] = value

def __delitem__(self, key):
def __delitem__(self, key: bytes) -> None:
del self._dict[key]

def __iter__(self):
def __iter__(self) -> Iterator[bytes]:
self._remove_expired()
return iter(self._dict)

def __len__(self):
def __len__(self) -> int:
self._remove_expired()
return len(self._dict)

def __hash__(self):
def __hash__(self) -> int:
return hash(super(object, self))

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
return super(object, self) == other


def valid_response_type(value, nested=False):
def valid_response_type(value: Any, nested: bool = False) -> bool:
if isinstance(value, NoResponse) and not nested:
return True
if (value is not None
and not isinstance(value, (bytes, SimpleString, SimpleError, float, int, list))):
if value is not None and not isinstance(value, (bytes, SimpleString, SimpleError, float, int, list)):
return False
if isinstance(value, list):
if any(not valid_response_type(item, True) for item in value):
@@ -208,10 +215,10 @@ def valid_response_type(value, nested=False):


class FakeSelector(object):
def __init__(self, sock):
def __init__(self, sock: Any):
self.sock = sock

def check_can_read(self, timeout):
def check_can_read(self, timeout: Optional[float]) -> bool:
if self.sock.responses.qsize():
return True
if timeout is not None and timeout <= 0:
@@ -229,5 +236,5 @@ def check_can_read(self, timeout):
return False

@staticmethod
def check_is_ready_for_command(timeout):
def check_is_ready_for_command(_: Any) -> bool:
return True
94 changes: 81 additions & 13 deletions fakeredis/_msgs.py
Original file line number Diff line number Diff line change
@@ -2,33 +2,38 @@
WRONGTYPE_MSG = "WRONGTYPE Operation against a key holding the wrong kind of value"
SYNTAX_ERROR_MSG = "ERR syntax error"
SYNTAX_ERROR_LIMIT_ONLY_WITH_MSG = (
"ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX")
"ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX"
)
INVALID_HASH_MSG = "ERR hash value is not an integer"
INVALID_INT_MSG = "ERR value is not an integer or out of range"
INVALID_FLOAT_MSG = "ERR value is not a valid float"
INVALID_WEIGHT_MSG = "ERR weight value is not a float"
INVALID_OFFSET_MSG = "ERR offset is out of range"
INVALID_BIT_OFFSET_MSG = "ERR bit offset is not an integer or out of range"
INVALID_BIT_VALUE_MSG = "ERR bit is not an integer or out of range"
BITOP_NOT_ONE_KEY_ONLY = "ERR BITOP NOT must be called with a single source key"
INVALID_DB_MSG = "ERR DB index is out of range"
INVALID_MIN_MAX_FLOAT_MSG = "ERR min or max is not a float"
INVALID_MIN_MAX_STR_MSG = "ERR min or max not a valid string range item"
STRING_OVERFLOW_MSG = "ERR string exceeds maximum allowed size (512MB)"
STRING_OVERFLOW_MSG = "ERR string exceeds maximum allowed size (proto-max-bulk-len)"
OVERFLOW_MSG = "ERR increment or decrement would overflow"
NONFINITE_MSG = "ERR increment would produce NaN or Infinity"
SCORE_NAN_MSG = "ERR resulting score is not a number (NaN)"
INVALID_SORT_FLOAT_MSG = "ERR One or more scores can't be converted into double"
SRC_DST_SAME_MSG = "ERR source and destination objects are the same"
NO_KEY_MSG = "ERR no such key"
INDEX_ERROR_MSG = "ERR index out of range"
ZADD_NX_XX_ERROR_MSG = "ERR ZADD allows either 'nx' or 'xx', not both"
INDEX_NEGATIVE_ERROR_MSG = "ERR value is out of range, must be positive"
# ZADD_NX_XX_ERROR_MSG6 = "ERR ZADD allows either 'nx' or 'xx', not both"
ZADD_NX_XX_ERROR_MSG = "ERR XX and NX options at the same time are not compatible"
ZADD_INCR_LEN_ERROR_MSG = "ERR INCR option supports a single increment-element pair"
ZADD_NX_GT_LT_ERROR_MSG = "ERR GT, LT, and/or NX options at the same time are not compatible"
NX_XX_GT_LT_ERROR_MSG = "ERR NX and XX, GT or LT options at the same time are not compatible"
EXPIRE_UNSUPPORTED_OPTION = "ERR Unsupported option {}"
ZUNIONSTORE_KEYS_MSG = "ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE"
ZUNIONSTORE_KEYS_MSG = "ERR at least 1 input key is needed for {}"
WRONG_ARGS_MSG7 = "ERR Wrong number of args calling Redis command from script"
WRONG_ARGS_MSG6 = "ERR wrong number of arguments for '{}' command"
UNKNOWN_COMMAND_MSG = "ERR unknown command '{}'"
UNKNOWN_COMMAND_MSG = "ERR unknown command `{}`, with args beginning with: "
EXECABORT_MSG = "EXECABORT Transaction discarded because of previous errors."
MULTI_NESTED_MSG = "ERR MULTI calls can not be nested"
WITHOUT_MULTI_MSG = "ERR {0} without MULTI"
@@ -40,27 +45,90 @@
GLOBAL_VARIABLE_MSG = "ERR Script attempted to set global variables: {}"
COMMAND_IN_SCRIPT_MSG = "ERR This Redis command is not allowed from scripts"
BAD_SUBCOMMAND_MSG = "ERR Unknown {} subcommand or wrong # of args."
BAD_COMMAND_IN_PUBSUB_MSG = \
"ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context"
BAD_COMMAND_IN_PUBSUB_MSG = "ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context"
CONNECTION_ERROR_MSG = "FakeRedis is emulating a connection error."
REQUIRES_MORE_ARGS_MSG = "ERR {} requires {} arguments or more."
LOG_INVALID_DEBUG_LEVEL_MSG = "ERR Invalid debug level."
LUA_COMMAND_ARG_MSG6 = "ERR Lua redis() command arguments must be strings or integers"
LUA_COMMAND_ARG_MSG = "ERR Lua redis lib command arguments must be strings or integers"
VALKEY_LUA_COMMAND_ARG_MSG = "Command arguments must be strings or integers script: {}"
LUA_WRONG_NUMBER_ARGS_MSG = "ERR wrong number or type of arguments"
SCRIPT_ERROR_MSG = "ERR Error running script (call to f_{}): @user_script:?: {}"
RESTORE_KEY_EXISTS = "BUSYKEY Target key name already exists."
RESTORE_INVALID_CHECKSUM_MSG = "ERR DUMP payload version or checksum are wrong"

RESTORE_INVALID_TTL_MSG = "ERR Invalid TTL value, must be >= 0"
JSON_WRONG_REDIS_TYPE = "ERR Existing key has wrong Redis type"
JSON_KEY_NOT_FOUND = "ERR could not perform this operation on a key that doesn't exist"
JSON_PATH_NOT_FOUND_OR_NOT_STRING = "ERR Path '{}' does not exist or not a string"
JSON_PATH_DOES_NOT_EXIST = "ERR Path '{}' does not exist"
LCS_CANT_HAVE_BOTH_LEN_AND_IDX = "ERR If you want both the length and indexes, please just use IDX."
BIT_ARG_MUST_BE_ZERO_OR_ONE = "ERR The bit argument must be 1 or 0."
XADD_ID_LOWER_THAN_LAST = "The ID specified in XADD is equal or smaller than the target stream top item"
XADD_INVALID_ID = 'Invalid stream ID specified as stream command argument'
FLAG_NO_SCRIPT = 's' # Command not allowed in scripts
FLAG_LEAVE_EMPTY_VAL = 'v'
FLAG_TRANSACTION = 't'
GEO_UNSUPPORTED_UNIT = 'unsupported unit provided. please use M, KM, FT, MI'
XADD_ID_LOWER_THAN_LAST = "ERR The ID specified in XADD is equal or smaller than the target stream top item"
XADD_INVALID_ID = "ERR Invalid stream ID specified as stream command argument"
XGROUP_BUSYGROUP = "ERR BUSYGROUP Consumer Group name already exists"
XREADGROUP_KEY_OR_GROUP_NOT_FOUND_MSG = (
"NOGROUP No such key '{0}' or consumer group '{1}' in XREADGROUP with GROUP option"
)
XGROUP_GROUP_NOT_FOUND_MSG = "NOGROUP No such consumer group '{0}' for key name '{1}'"
XGROUP_KEY_NOT_FOUND_MSG = (
"ERR The XGROUP subcommand requires the key to exist."
" Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically."
)
GEO_UNSUPPORTED_UNIT = "unsupported unit provided. please use M, KM, FT, MI"
LPOS_RANK_CAN_NOT_BE_ZERO = (
"RANK can't be zero: use 1 to start from the first match, 2 from the second ... "
"or use negative to start from the end of the list"
)
NUMKEYS_GREATER_THAN_ZERO_MSG = "numkeys should be greater than 0"
FILTER_FULL_MSG = ""
NONSCALING_FILTERS_CANNOT_EXPAND_MSG = "Nonscaling filters cannot expand"
ITEM_EXISTS_MSG = "item exists"
NOT_FOUND_MSG = "not found"
INVALID_BITFIELD_TYPE = (
"ERR Invalid bitfield type. Use something like i16 u8. " "Note that u64 is not supported but i64 is."
)
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"
TDIGEST_ERROR_PARSING_VALUE = "T-Digest: error parsing val parameter"
TDIGEST_BAD_QUANTILE = "T-Digest: quantile should be in [0,1]"
TDIGEST_BAD_RANK = "T-Digest: rank needs to be non negative"

# TimeSeries error messages
TIMESERIES_KEY_EXISTS = "TSDB: key already exists"
TIMESERIES_INVALID_DUPLICATE_POLICY = "TSDB: Unknown DUPLICATE_POLICY"
TIMESERIES_KEY_DOES_NOT_EXIST = "TSDB: the key does not exist"
TIMESERIES_RULE_DOES_NOT_EXIST = "TSDB: compaction rule does not exist"
TIMESERIES_RULE_EXISTS = "TSDB: the destination key already has a src rule"
TIMESERIES_BAD_AGGREGATION_TYPE = "TSDB: Unknown aggregation type"
TIMESERIES_INVALID_TIMESTAMP = "TSDB: invalid timestamp"
TIMESERIES_BAD_TIMESTAMP = "TSDB: Couldn't parse alignTimestamp"
TIMESERIES_TIMESTAMP_OLDER_THAN_RETENTION = "TSDB: Timestamp is older than retention"
TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V7 = (
"TSDB: timestamp must be equal to or higher than the maximum existing timestamp"
)
TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V6 = "TSDB: for incrby/decrby, timestamp should be newer than the lastest one"
TIMESERIES_BAD_CHUNK_SIZE = "TSDB: CHUNK_SIZE value must be a multiple of 8 in the range [48 .. 1048576]"
TIMESERIES_DUPLICATE_POLICY_BLOCK = (
"TSDB: Error at upsert, update is not supported when DUPLICATE_POLICY is set to BLOCK mode"
)
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"
FLAG_TRANSACTION = "t"
FLAG_DO_NOT_CREATE = "i"
265 changes: 81 additions & 184 deletions fakeredis/_server.py
Original file line number Diff line number Diff line change
@@ -1,207 +1,104 @@
import inspect
import logging
import queue
import threading
import time
import warnings
import weakref
from collections import defaultdict
from typing import Dict
from typing import Dict, Tuple, Any, List, Optional, Union

import redis
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal

from fakeredis._fakesocket import FakeSocket
from fakeredis._helpers import (Database, FakeSelector)
from . import _msgs as msgs

LOGGER = logging.getLogger('fakeredis')
from fakeredis.model import AccessControlList
from fakeredis._helpers import Database, FakeSelector

LOGGER = logging.getLogger("fakeredis")

class FakeServer:
_servers_map: Dict[str, 'FakeServer'] = dict()
VersionType = Union[Tuple[int, ...], int, str]

ServerType = Literal["redis", "dragonfly", "valkey"]


def _create_version(v: VersionType) -> Tuple[int, ...]:
if isinstance(v, tuple):
return v
if isinstance(v, int):
return (v,)
if isinstance(v, str):
v_split = v.split(".")
return tuple(int(x) for x in v_split)
return v


def _version_to_str(v: VersionType) -> str:
if isinstance(v, tuple):
return ".".join(str(x) for x in v)
return str(v)

def __init__(self, version=7):

class FakeServer:
_servers_map: Dict[str, "FakeServer"] = dict()

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 = defaultdict(lambda: Database(self.lock))
# Maps channel/pattern to weak set of sockets
self.subscribers = defaultdict(weakref.WeakSet)
self.psubscribers = defaultdict(weakref.WeakSet)
self.lastsave = int(time.time())
self.dbs: Dict[int, Database] = defaultdict(lambda: Database(self.lock))
# Maps channel/pattern to a weak set of sockets
self.subscribers: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet)
self.psubscribers: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet)
self.ssubscribers: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet)
self.lastsave: int = int(time.time())
self.connected = True
# List of weakrefs to sockets that are being closed lazily
self.closed_sockets = []
self.version = version
self.closed_sockets: List[Any] = []
self.version: Tuple[int, ...] = _create_version(version)
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, version: int):
return FakeServer._servers_map.setdefault(key, FakeServer(version=version))


class FakeBaseConnectionMixin:
def __init__(self, *args, **kwargs):
self.client_name = None
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: ServerType = "redis", **kwargs: Any
) -> None:
self.client_name: Optional[str] = None
self.server_key: str
self._sock = None
self._selector = None
self._server = kwargs.pop('server', None)
path = kwargs.pop('path', None)
version = kwargs.pop('version', 7)
connected = kwargs.pop('connected', True)
self._selector: Optional[FakeSelector] = None
self._server = kwargs.pop("server", None)
self._lua_modules = kwargs.pop("lua_modules", set())
path = kwargs.pop("path", None)
connected = kwargs.pop("connected", True)
if self._server is None:
if path:
self.server_key = path
else:
host, port = kwargs.get('host'), kwargs.get('port')
self.server_key = 'shared' if host is None or port is None else f'{host}:{port}'
self.server_key += f'v{version}'
self._server = FakeServer.get_server(self.server_key, version=version)
host, port = kwargs.get("host"), kwargs.get("port")
self.server_key = f"{host}:{port}"
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)


class FakeConnection(FakeBaseConnectionMixin, redis.Connection):

def connect(self):
super().connect()
# The selector is set in redis.Connection.connect() after _connect() is called
self._selector = FakeSelector(self._sock)

def _connect(self):
if not self._server.connected:
raise redis.ConnectionError(msgs.CONNECTION_ERROR_MSG)
return FakeSocket(self._server, db=self.db)

def can_read(self, timeout=0):
if not self._server.connected:
return True
if not self._sock:
self.connect()
# We use check_can_read rather than can_read, because on redis-py<3.2,
# FakeSelector inherits from a stub BaseSelector which doesn't
# implement can_read. Normally can_read provides retries on EINTR,
# but that's not necessary for the implementation of
# FakeSelector.check_can_read.
return self._selector.check_can_read(timeout)

def _decode(self, response):
if isinstance(response, list):
return [self._decode(item) for item in response]
elif isinstance(response, bytes):
return self.encoder.decode(response, )
else:
return response

def read_response(self, disable_decoding=False):
if not self._server.connected:
try:
response = self._sock.responses.get_nowait()
except queue.Empty:
raise redis.ConnectionError(msgs.CONNECTION_ERROR_MSG)
else:
response = self._sock.responses.get()
if isinstance(response, redis.ResponseError):
raise response
if disable_decoding:
return response
else:
return self._decode(response)

def repr_pieces(self):
pieces = [
('server', self._server),
('db', self.db)
]
if self.client_name:
pieces.append(('client_name', self.client_name))
return pieces

def __str__(self):
return self.server_key


class FakeRedisMixin:
def __init__(self, *args, server=None, connected=True, version=7, **kwargs):
# Interpret the positional and keyword arguments according to the
# version of redis in use.
parameters = inspect.signature(redis.Redis.__init__).parameters
parameter_names = list(parameters.keys())
default_args = parameters.values()
kwds = {p.name: p.default for p in default_args if p.default != inspect.Parameter.empty}
kwds.update(kwargs)
if not kwds.get('connection_pool', None):
charset = kwds.get('charset', None)
errors = kwds.get('errors', None)
# Adapted from redis-py
if charset is not None:
warnings.warn(DeprecationWarning(
'"charset" is deprecated. Use "encoding" instead'))
kwds['encoding'] = charset
if errors is not None:
warnings.warn(DeprecationWarning(
'"errors" is deprecated. Use "encoding_errors" instead'))
kwds['encoding_errors'] = errors
conn_pool_args = {
'host',
'port',
'db',
# Ignoring because AUTH is not implemented
# 'username',
# 'password',
'socket_timeout',
'encoding',
'encoding_errors',
'decode_responses',
'retry_on_timeout',
'max_connections',
'health_check_interval',
'client_name',
}
connection_kwargs = {
'connection_class': FakeConnection,
'server': server,
'version': version,
}
connection_kwargs.update({arg: kwds[arg] for arg in conn_pool_args if arg in kwds})
kwds['connection_pool'] = redis.connection.ConnectionPool(**connection_kwargs)
kwds.pop('server', None)
kwds.pop('connected', None)
kwds.pop('version', None)
parameter_names_to_cut = parameter_names[1:len(args) + 1]
for param in parameter_names_to_cut:
kwds.pop(param, None)
super().__init__(*args, **kwds)

@classmethod
def from_url(cls, *args, **kwargs):
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)


class FakeStrictRedis(FakeRedisMixin, redis.StrictRedis):
pass


class FakeRedis(FakeRedisMixin, redis.Redis):
pass


# RQ
# Configuration to pretend there is a Redis service available.
# Set up the connection before RQ Django reads the settings.
# The connection must be the same because in fakeredis connections
# do not share the state. Therefore, we define a singleton object to reuse it.
class FakeRedisConnSingleton:
"""Singleton FakeRedis connection."""

def __init__(self):
self.conn = None

def __call__(self, _, strict):
if not self.conn:
self.conn = FakeStrictRedis() if strict else FakeRedis()
return self.conn
127 changes: 0 additions & 127 deletions fakeredis/_stream.py

This file was deleted.

129 changes: 129 additions & 0 deletions fakeredis/_tcp_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import logging
from dataclasses import dataclass
from itertools import count
from socketserver import ThreadingTCPServer, StreamRequestHandler
from typing import BinaryIO, Dict, Tuple

from fakeredis import FakeRedis
from fakeredis import FakeServer
from fakeredis._server import ServerType

LOGGER = logging.getLogger("fakeredis")
LOGGER.setLevel(logging.DEBUG)


def to_bytes(value) -> bytes:
if isinstance(value, bytes):
return value
return str(value).encode()


@dataclass
class Client:
connection: FakeRedis
client_address: int


@dataclass
class Reader:
reader: BinaryIO

def load_array(self, length: int):
array = [None] * length
for i in range(length):
array[i] = self.load()
return array

def load(self):
line = self.reader.readline().strip()
match line[0:1], line[1:]:
case b"*", length:
return self.load_array(int(length))
case b"$", length:
bulk_string = self.reader.read(int(length) + 2).strip()
if len(bulk_string) != int(length):
raise ValueError()
return bulk_string
case b":", value:
return int(value)
case b"+", value:
return value
case b"-", value:
return Exception(value)
case _:
return None


@dataclass
class Writer:
writer: BinaryIO

def dump(self, value, dump_bulk=False):
if isinstance(value, int):
self.writer.write(f":{value}\r\n".encode())
elif isinstance(value, (str, bytes)):
value = to_bytes(value)
if dump_bulk or b"\r" in value or b"\n" in value:
self.writer.write(b"$" + str(len(value)).encode() + b"\r\n" + value + b"\r\n")
else:
self.writer.write(b"+" + value + b"\r\n")
elif isinstance(value, (list, set)):
self.writer.write(f"*{len(value)}\r\n".encode())
for item in value:
self.dump(item, dump_bulk=True)
elif value is None:
self.writer.write("$-1\r\n".encode())
elif isinstance(value, Exception):
self.writer.write(f"-{value.args[0]}\r\n".encode())


class TCPFakeRequestHandler(StreamRequestHandler):

def setup(self) -> None:
super().setup()
if self.client_address in self.server.clients:
self.current_client = self.server.clients[self.client_address]
else:
self.current_client = Client(
connection=FakeRedis(server=self.server.fake_server),
client_address=self.client_address,
)
self.reader = Reader(self.rfile)
self.writer = Writer(self.wfile)
self.server.clients[self.client_address] = self.current_client

def handle(self):
while True:
try:
self.data = self.reader.load()
LOGGER.debug(f">>> {self.client_address[0]}: {self.data}")
res = self.current_client.connection.execute_command(*self.data)
LOGGER.debug(f"<<< {self.client_address[0]}: {res}")
self.writer.dump(res)
except Exception as e:
LOGGER.debug(f"!!! {self.client_address[0]}: {e}")
self.writer.dump(e)
break

def finish(self) -> None:
del self.server.clients[self.current_client.client_address]
super().finish()


class TcpFakeServer(ThreadingTCPServer):
def __init__(
self,
server_address: Tuple[str | bytes | bytearray, int],
bind_and_activate: bool = True,
server_type: ServerType = "redis",
server_version: Tuple[int, ...] = (7, 4),
):
super().__init__(server_address, TCPFakeRequestHandler, bind_and_activate)
self.fake_server = FakeServer(server_type=server_type, version=server_version)
self.client_ids = count(0)
self.clients: Dict[int, FakeRedis] = dict()


if __name__ == "__main__":
server = TcpFakeServer(("localhost", 19000))
server.serve_forever()
50 changes: 50 additions & 0 deletions fakeredis/_valkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import sys
from typing import Any, Dict

from . import FakeStrictRedis
from ._connection import FakeRedis
from .aioredis import FakeRedis as FakeAsyncRedis

if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self


def _validate_server_type(args_dict: Dict[str, Any]) -> None:
if "server_type" in args_dict and args_dict["server_type"] != "valkey":
raise ValueError("server_type must be valkey")
args_dict.setdefault("server_type", "valkey")


class FakeValkey(FakeRedis):
def __init__(self, *args: Any, **kwargs: Any) -> None:
_validate_server_type(kwargs)
super().__init__(*args, **kwargs)

@classmethod
def from_url(cls, *args: Any, **kwargs: Any) -> Self:
_validate_server_type(kwargs)
return super().from_url(*args, **kwargs)


class FakeStrictValkey(FakeStrictRedis):
def __init__(self, *args: Any, **kwargs: Any) -> None:
_validate_server_type(kwargs)
super().__init__(*args, **kwargs)

@classmethod
def from_url(cls, *args: Any, **kwargs: Any) -> Self:
_validate_server_type(kwargs)
return super().from_url(*args, **kwargs)


class FakeAsyncValkey(FakeAsyncRedis):
def __init__(self, *args: Any, **kwargs: Any) -> None:
_validate_server_type(kwargs)
super().__init__(*args, **kwargs)

@classmethod
def from_url(cls, *args: Any, **kwargs: Any) -> Self:
_validate_server_type(kwargs)
return super().from_url(*args, **kwargs)
87 changes: 0 additions & 87 deletions fakeredis/_zset.py

This file was deleted.

238 changes: 127 additions & 111 deletions fakeredis/aioredis.py
Original file line number Diff line number Diff line change
@@ -2,24 +2,21 @@

import asyncio
import sys
from typing import Union, Optional
import uuid
from typing import Union, Optional, Any, Callable, Iterable, Tuple, List, Set

import redis
from redis import ResponseError

from ._server import FakeBaseConnectionMixin

if sys.version_info >= (3, 8):
from typing import Type, TypedDict
else:
from typing_extensions import Type, TypedDict
from ._helpers import SimpleError
from ._server import FakeBaseConnectionMixin, VersionType

if sys.version_info >= (3, 11):
from asyncio import timeout as async_timeout
else:
from async_timeout import timeout as async_timeout

import redis.asyncio as redis_async # aioredis was integrated into redis in version 4.2.0 as redis.asyncio
from redis.asyncio.connection import BaseParser
from redis.asyncio.connection import DefaultParser

from . import _fakesocket
from . import _helpers
@@ -30,18 +27,26 @@
class AsyncFakeSocket(_fakesocket.FakeSocket):
_connection_error_class = redis_async.ConnectionError

def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.responses = asyncio.Queue()
self.responses: asyncio.Queue = asyncio.Queue() # type:ignore

def _decode_error(self, error):
parser = BaseParser(1) if redis.VERSION < (5, 0) else BaseParser()
def _decode_error(self, error: SimpleError) -> ResponseError:
parser = DefaultParser(1)
return parser.parse_error(error.value)

def put_response(self, msg):
def put_response(self, msg: Any) -> None:
if not self.responses:
return
self.responses.put_nowait(msg)

async def _async_blocking(self, timeout, func, event, callback):
async def _async_blocking(
self,
timeout: Optional[Union[float, int]],
func: Callable[[bool], Any],
event: asyncio.Event,
callback: Callable[[], None],
) -> None:
result = None
try:
async with async_timeout(timeout if timeout else None):
@@ -54,7 +59,6 @@ async def _async_blocking(self, timeout, func, event, callback):
ret = func(False)
if ret is not None:
result = self._decode_result(ret)
self.put_response(result)
break
except asyncio.TimeoutError:
pass
@@ -64,14 +68,18 @@ async def _async_blocking(self, timeout, func, event, callback):
self.put_response(result)
self.resume()

def _blocking(self, timeout, func):
def _blocking(
self,
timeout: Optional[Union[float, int]],
func: Callable[[bool], None],
) -> Any:
loop = asyncio.get_event_loop()
ret = func(True)
if ret is not None or self._in_transaction:
return ret
event = asyncio.Event()

def callback():
def callback() -> None:
loop.call_soon_threadsafe(event.set)

self._db.add_change_callback(callback)
@@ -84,168 +92,176 @@ class FakeReader:
def __init__(self, socket: AsyncFakeSocket) -> None:
self._socket = socket

async def read(self, length: int) -> bytes:
return await self._socket.responses.get()
async def read(self, _: int) -> bytes:
return await self._socket.responses.get() # type:ignore

def at_eof(self) -> bool:
return self._socket.responses.empty() and not self._socket._server.connected


class FakeWriter:
def __init__(self, socket: AsyncFakeSocket) -> None:
self._socket = socket
self._socket: Optional[AsyncFakeSocket] = socket

def close(self):
def close(self) -> None:
self._socket = None

async def wait_closed(self):
async def wait_closed(self) -> None:
pass

async def drain(self):
async def drain(self) -> None:
pass

def writelines(self, data):
def writelines(self, data: Iterable[Any]) -> None:
if self._socket is None:
return
for chunk in data:
self._socket.sendall(chunk)
self._socket.sendall(chunk) # type:ignore


class FakeConnection(FakeBaseConnectionMixin, redis_async.Connection):

async def _connect(self):
async def _connect(self) -> None:
if not self._server.connected:
raise redis_async.ConnectionError(msgs.CONNECTION_ERROR_MSG)
self._sock = AsyncFakeSocket(self._server, self.db)
self._reader = FakeReader(self._sock)
self._writer = FakeWriter(self._sock)
self._sock: Optional[AsyncFakeSocket] = AsyncFakeSocket(self._server, self.db, lua_modules=self._lua_modules)
self._reader: Optional[FakeReader] = FakeReader(self._sock)
self._writer: Optional[FakeWriter] = FakeWriter(self._sock)

async def disconnect(self, **kwargs):
async def disconnect(self, nowait: bool = False, **kwargs: Any) -> None:
await super().disconnect(**kwargs)
self._sock = None

async def can_read(self, timeout: float = 0):
async def can_read(self, timeout: Optional[float] = 0) -> bool:
if not self.is_connected:
await self.connect()
if timeout == 0:
return not self._sock.responses.empty()
return self._sock is not None and not self._sock.responses.empty()
# asyncio.Queue doesn't have a way to wait for the queue to be
# non-empty without consuming an item, so kludge it with a sleep/poll
# loop.
loop = asyncio.get_event_loop()
start = loop.time()
while True:
if not self._sock.responses.empty():
if self._sock and not self._sock.responses.empty():
return True
await asyncio.sleep(0.01)
now = loop.time()
if timeout is not None and now > start + timeout:
return False

def _decode(self, response):
def _decode(self, response: Any) -> Any:
if isinstance(response, list):
return [self._decode(item) for item in response]
elif isinstance(response, bytes):
return self.encoder.decode(response, )
return self.encoder.decode(response)
else:
return response

async def read_response(self, **kwargs):
async def read_response(self, **kwargs: Any) -> Any: # type: ignore
if not self._sock:
raise redis_async.ConnectionError(msgs.CONNECTION_ERROR_MSG)
if not self._server.connected:
try:
response = self._sock.responses.get_nowait()
except asyncio.QueueEmpty:
if kwargs.get("disconnect_on_error", True):
await self.disconnect()
raise redis_async.ConnectionError(msgs.CONNECTION_ERROR_MSG)
else:
timeout = kwargs.pop('timeout', None)
timeout: Optional[float] = kwargs.pop("timeout", None)
can_read = await self.can_read(timeout)
response = await self._reader.read(0) if can_read else None
response = await self._reader.read(0) if can_read and self._reader else None
if isinstance(response, redis_async.ResponseError):
raise response
if kwargs.get("disable_decoding", False):
return response
return self._decode(response)

def repr_pieces(self):
pieces = [
('server', self._server),
('db', self.db)
]
def repr_pieces(self) -> List[Tuple[str, Any]]:
pieces = [("server", self._server), ("db", self.db)]
if self.client_name:
pieces.append(('client_name', self.client_name))
pieces.append(("client_name", self.client_name))
return pieces


class ConnectionKwargs(TypedDict, total=False):
db: Union[str, int]
username: Optional[str]
password: Optional[str]
socket_timeout: Optional[float]
encoding: str
encoding_errors: str
decode_responses: bool
retry_on_timeout: bool
health_check_interval: int
client_name: Optional[str]
server: Optional[_server.FakeServer]
connection_class: Type[redis_async.Connection]
max_connections: Optional[int]
def __str__(self) -> str:
return self.server_key


class FakeRedis(redis_async.Redis):
def __init__(
self,
*,
db: Union[str, int] = 0,
password: Optional[str] = None,
socket_timeout: Optional[float] = None,
connection_pool: Optional[redis_async.ConnectionPool] = None,
encoding: str = "utf-8",
encoding_errors: str = "strict",
decode_responses: bool = False,
retry_on_timeout: bool = False,
max_connections: Optional[int] = None,
health_check_interval: int = 0,
client_name: Optional[str] = None,
username: Optional[str] = None,
server: Optional[_server.FakeServer] = None,
connected: bool = True,
**kwargs,
):
self,
*,
host: Optional[str] = None,
port: int = 6379,
db: Union[str, int] = 0,
password: Optional[str] = None,
socket_timeout: Optional[float] = None,
connection_pool: Optional[redis_async.ConnectionPool] = None,
encoding: str = "utf-8",
encoding_errors: str = "strict",
decode_responses: bool = False,
retry_on_timeout: bool = False,
max_connections: Optional[int] = None,
health_check_interval: int = 0,
client_name: Optional[str] = None,
username: Optional[str] = None,
server: Optional[_server.FakeServer] = None,
connected: bool = True,
version: VersionType = (7,),
server_type: str = "redis",
lua_modules: Optional[Set[str]] = None,
**kwargs: Any,
) -> None:
if not connection_pool:
# Adapted from aioredis
connection_kwargs: ConnectionKwargs = {
"db": db,
connection_kwargs = dict(
host=host or uuid.uuid4().hex,
port=port,
db=db,
# Ignoring because AUTH is not implemented
# 'username',
# 'password',
"socket_timeout": socket_timeout,
"encoding": encoding,
"encoding_errors": encoding_errors,
"decode_responses": decode_responses,
"retry_on_timeout": retry_on_timeout,
"health_check_interval": health_check_interval,
"client_name": client_name,
"server": server,
"connected": connected,
"connection_class": FakeConnection,
"max_connections": max_connections,
}
connection_pool = redis_async.ConnectionPool(**connection_kwargs)
super().__init__(
db=db,
password=password,
socket_timeout=socket_timeout,
connection_pool=connection_pool,
encoding=encoding,
encoding_errors=encoding_errors,
decode_responses=decode_responses,
retry_on_timeout=retry_on_timeout,
max_connections=max_connections,
health_check_interval=health_check_interval,
client_name=client_name,
username=username,
**kwargs,
socket_timeout=socket_timeout,
encoding=encoding,
encoding_errors=encoding_errors,
decode_responses=decode_responses,
retry_on_timeout=retry_on_timeout,
health_check_interval=health_check_interval,
client_name=client_name,
server=server,
connected=connected,
connection_class=FakeConnection,
max_connections=max_connections,
version=version,
server_type=server_type,
lua_modules=lua_modules,
)
connection_pool = redis_async.ConnectionPool(**connection_kwargs) # type:ignore
kwargs.update(
dict(
db=db,
password=password,
socket_timeout=socket_timeout,
connection_pool=connection_pool,
encoding=encoding,
encoding_errors=encoding_errors,
decode_responses=decode_responses,
retry_on_timeout=retry_on_timeout,
max_connections=max_connections,
health_check_interval=health_check_interval,
client_name=client_name,
username=username,
)
)
super().__init__(**kwargs)

@classmethod
def from_url(cls, url: str, **kwargs):
def from_url(cls, url: str, **kwargs: Any) -> redis_async.Redis:
self = super().from_url(url, **kwargs)
pool = self.connection_pool # Now override how it creates connections
pool.connection_class = FakeConnection
pool.connection_kwargs.pop('username', None)
pool.connection_kwargs.pop('password', None)
pool.connection_kwargs.setdefault("version", "7.4")
pool.connection_kwargs.setdefault("server_type", "redis")
pool.connection_kwargs.pop("username", None)
pool.connection_kwargs.pop("password", None)
return self
1 change: 1 addition & 0 deletions fakeredis/commands.json

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions fakeredis/commands_mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Any

from .acl_mixin import AclCommandsMixin
from .bitmap_mixin import BitmapCommandsMixin
from .connection_mixin import ConnectionCommandsMixin
from .generic_mixin import GenericCommandsMixin
from .geo_mixin import GeoCommandsMixin
from .hash_mixin import HashCommandsMixin
from .list_mixin import ListCommandsMixin
from .pubsub_mixin import PubSubCommandsMixin
from .server_mixin import ServerCommandsMixin
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
except ImportError:

class ScriptingCommandsMixin: # type: ignore # noqa: E303
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs.pop("lua_modules", None)
super(ScriptingCommandsMixin, self).__init__(*args, **kwargs) # type: ignore


__all__ = [
"BitmapCommandsMixin",
"ConnectionCommandsMixin",
"GenericCommandsMixin",
"GeoCommandsMixin",
"HashCommandsMixin",
"ListCommandsMixin",
"PubSubCommandsMixin",
"ScriptingCommandsMixin",
"TransactionsCommandsMixin",
"ServerCommandsMixin",
"SetCommandsMixin",
"StreamsCommandsMixin",
"StringCommandsMixin",
"AclCommandsMixin",
]
188 changes: 188 additions & 0 deletions fakeredis/commands_mixins/acl_mixin.py
Original file line number Diff line number Diff line change
@@ -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)
241 changes: 187 additions & 54 deletions fakeredis/commands_mixins/bitmap_mixin.py
35 changes: 24 additions & 11 deletions fakeredis/commands_mixins/connection_mixin.py
313 changes: 215 additions & 98 deletions fakeredis/commands_mixins/generic_mixin.py
219 changes: 144 additions & 75 deletions fakeredis/commands_mixins/geo_mixin.py
220 changes: 190 additions & 30 deletions fakeredis/commands_mixins/hash_mixin.py
171 changes: 138 additions & 33 deletions fakeredis/commands_mixins/list_mixin.py
172 changes: 106 additions & 66 deletions fakeredis/commands_mixins/pubsub_mixin.py
221 changes: 130 additions & 91 deletions fakeredis/commands_mixins/scripting_mixin.py
59 changes: 42 additions & 17 deletions fakeredis/commands_mixins/server_mixin.py
124 changes: 64 additions & 60 deletions fakeredis/commands_mixins/set_mixin.py
388 changes: 310 additions & 78 deletions fakeredis/commands_mixins/sortedset_mixin.py
377 changes: 338 additions & 39 deletions fakeredis/commands_mixins/streams_mixin.py
256 changes: 146 additions & 110 deletions fakeredis/commands_mixins/string_mixin.py
40 changes: 25 additions & 15 deletions fakeredis/commands_mixins/transactions_mixin.py
8 changes: 8 additions & 0 deletions fakeredis/geo/__init__.py
42 changes: 26 additions & 16 deletions fakeredis/geo/geohash.py
20 changes: 1 addition & 19 deletions fakeredis/geo/haversine.py
33 changes: 33 additions & 0 deletions fakeredis/model/__init__.py
365 changes: 365 additions & 0 deletions fakeredis/model/_acl.py
60 changes: 60 additions & 0 deletions fakeredis/model/_command_info.py
89 changes: 89 additions & 0 deletions fakeredis/model/_expiring_members_set.py
80 changes: 80 additions & 0 deletions fakeredis/model/_hash.py
529 changes: 529 additions & 0 deletions fakeredis/model/_stream.py
281 changes: 281 additions & 0 deletions fakeredis/model/_timeseries_model.py
103 changes: 103 additions & 0 deletions fakeredis/model/_topk.py
100 changes: 100 additions & 0 deletions fakeredis/model/_zset.py
5 changes: 5 additions & 0 deletions fakeredis/server_specific_commands/__init__.py
24 changes: 24 additions & 0 deletions fakeredis/server_specific_commands/dragonfly_mixin.py
40 changes: 38 additions & 2 deletions fakeredis/stack/__init__.py
184 changes: 184 additions & 0 deletions fakeredis/stack/_bf_mixin.py
206 changes: 206 additions & 0 deletions fakeredis/stack/_cf_mixin.py
142 changes: 142 additions & 0 deletions fakeredis/stack/_cms_mixin.py
451 changes: 296 additions & 155 deletions fakeredis/stack/_json_mixin.py
298 changes: 298 additions & 0 deletions fakeredis/stack/_tdigest_mixin.py
561 changes: 561 additions & 0 deletions fakeredis/stack/_timeseries_mixin.py
100 changes: 100 additions & 0 deletions fakeredis/stack/_topk_mixin.py
46 changes: 36 additions & 10 deletions mkdocs.yml
1,978 changes: 925 additions & 1,053 deletions poetry.lock
108 changes: 80 additions & 28 deletions pyproject.toml
2 changes: 2 additions & 0 deletions redis-conf/redis-stack.conf
1 change: 1 addition & 0 deletions redis-conf/users.acl
102 changes: 57 additions & 45 deletions scripts/create_issues.py
111 changes: 111 additions & 0 deletions scripts/generate_command_info.py
154 changes: 154 additions & 0 deletions scripts/generate_supported_commands_doc.py
104 changes: 0 additions & 104 deletions scripts/supported.py

This file was deleted.

87 changes: 0 additions & 87 deletions scripts/supported2.py

This file was deleted.

169 changes: 118 additions & 51 deletions test/conftest.py
233 changes: 233 additions & 0 deletions test/test_asyncredis.py
103 changes: 0 additions & 103 deletions test/test_extract_args.py

This file was deleted.

25 changes: 0 additions & 25 deletions test/test_general.py

This file was deleted.

608 changes: 290 additions & 318 deletions test/test_hypothesis.py
138 changes: 0 additions & 138 deletions test/test_init_args.py

This file was deleted.

Empty file added test/test_internals/__init__.py
Empty file.
59 changes: 59 additions & 0 deletions test/test_internals/test_acl_save_load.py
139 changes: 139 additions & 0 deletions test/test_internals/test_asyncredis.py
140 changes: 140 additions & 0 deletions test/test_internals/test_extract_args.py
166 changes: 166 additions & 0 deletions test/test_internals/test_init_args.py
73 changes: 73 additions & 0 deletions test/test_internals/test_lua_modules.py
4 changes: 2 additions & 2 deletions test/test_mock.py → test/test_internals/test_mock.py
45 changes: 45 additions & 0 deletions test/test_internals/test_transactions.py
46 changes: 46 additions & 0 deletions test/test_internals/test_xstream.py
359 changes: 250 additions & 109 deletions test/test_json/test_json.py
513 changes: 392 additions & 121 deletions test/test_json/test_json_arr_commands.py
166 changes: 9 additions & 157 deletions test/test_json/test_json_commands.py
403 changes: 403 additions & 0 deletions test/test_mixins/test_acl_commands.py
459 changes: 334 additions & 125 deletions test/test_mixins/test_bitmap_commands.py
243 changes: 124 additions & 119 deletions test/test_connection.py → test/test_mixins/test_connection.py
964 changes: 453 additions & 511 deletions test/test_mixins/test_generic_commands.py
268 changes: 201 additions & 67 deletions test/test_mixins/test_geo_commands.py
347 changes: 185 additions & 162 deletions test/test_mixins/test_hash_commands.py
333 changes: 333 additions & 0 deletions test/test_mixins/test_hash_expire_commands.py
841 changes: 461 additions & 380 deletions test/test_mixins/test_list_commands.py
391 changes: 255 additions & 136 deletions test/test_mixins/test_pubsub_commands.py
202 changes: 202 additions & 0 deletions test/test_mixins/test_scan.py
596 changes: 570 additions & 26 deletions test/test_mixins/test_scripting.py
80 changes: 52 additions & 28 deletions test/test_mixins/test_server_commands.py
538 changes: 286 additions & 252 deletions test/test_mixins/test_set_commands.py
1,584 changes: 829 additions & 755 deletions test/test_mixins/test_sortedset_commands.py
764 changes: 688 additions & 76 deletions test/test_mixins/test_streams_commands.py
626 changes: 323 additions & 303 deletions test/test_mixins/test_string_commands.py
309 changes: 0 additions & 309 deletions test/test_mixins/test_transactions_commands.py

This file was deleted.

174 changes: 174 additions & 0 deletions test/test_mixins/test_zadd.py
429 changes: 0 additions & 429 deletions test/test_redis_asyncio.py

This file was deleted.

484 changes: 0 additions & 484 deletions test/test_scripting_lua_only.py

This file was deleted.

Empty file added test/test_stack/__init__.py
Empty file.
189 changes: 189 additions & 0 deletions test/test_stack/test_bloomfilter.py
131 changes: 131 additions & 0 deletions test/test_stack/test_cms.py
37 changes: 37 additions & 0 deletions test/test_stack/test_cuckoofilter.py
133 changes: 133 additions & 0 deletions test/test_stack/test_tdigest.py
864 changes: 864 additions & 0 deletions test/test_stack/test_timeseries.py
38 changes: 38 additions & 0 deletions test/test_stack/test_topk.py
Empty file.
23 changes: 23 additions & 0 deletions test/test_tcp_server/test_connectivity.py
321 changes: 321 additions & 0 deletions test/test_transactions.py
160 changes: 0 additions & 160 deletions test/test_zadd.py

This file was deleted.

30 changes: 17 additions & 13 deletions test/testtools.py
24 changes: 12 additions & 12 deletions tox.ini