├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── test-coverage │ │ └── action.yml ├── dependabot.yml ├── release-drafter.yml ├── workflows │ ├── publish-documentation.yml │ ├── publish-pypi.yml │ ├── test-dragonfly.yml │ └── test.yml └── zizmor.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── about │ ├── changelog.md │ ├── contributing.md │ └── license.md ├── ads.txt ├── dragonfly-support.md ├── guides │ ├── implement-command.md │ └── test-case.md ├── index.md ├── overrides │ ├── main.html │ └── partials │ │ └── toc-item.html ├── redis-stack.md ├── requirements.txt ├── supported-commands │ ├── DRAGONFLY.md │ ├── Redis │ │ ├── BITMAP.md │ │ ├── CLUSTER.md │ │ ├── CONNECTION.md │ │ ├── GENERIC.md │ │ ├── GEO.md │ │ ├── HASH.md │ │ ├── HYPERLOGLOG.md │ │ ├── LIST.md │ │ ├── PUBSUB.md │ │ ├── SCRIPTING.md │ │ ├── SERVER.md │ │ ├── SET.md │ │ ├── SORTED-SET.md │ │ ├── STREAM.md │ │ ├── STRING.md │ │ └── TRANSACTIONS.md │ ├── RedisBloom │ │ ├── BF.md │ │ ├── CF.md │ │ ├── CMS.md │ │ ├── TDIGEST.md │ │ └── TOPK.md │ ├── RedisJson │ │ └── JSON.md │ ├── RedisSearch │ │ ├── SEARCH.md │ │ └── SUGGESTION.md │ ├── RedisTimeSeries │ │ └── TIMESERIES.md │ └── index.md └── valkey-support.md ├── fakeredis ├── __init__.py ├── _basefakesocket.py ├── _command_args_parsing.py ├── _commands.py ├── _connection.py ├── _fakesocket.py ├── _helpers.py ├── _msgs.py ├── _server.py ├── _tcp_server.py ├── _valkey.py ├── aioredis.py ├── commands.json ├── commands_mixins │ ├── __init__.py │ ├── acl_mixin.py │ ├── bitmap_mixin.py │ ├── connection_mixin.py │ ├── generic_mixin.py │ ├── geo_mixin.py │ ├── hash_mixin.py │ ├── list_mixin.py │ ├── pubsub_mixin.py │ ├── scripting_mixin.py │ ├── server_mixin.py │ ├── set_mixin.py │ ├── sortedset_mixin.py │ ├── streams_mixin.py │ ├── string_mixin.py │ └── transactions_mixin.py ├── geo │ ├── __init__.py │ ├── geohash.py │ └── haversine.py ├── model │ ├── __init__.py │ ├── _acl.py │ ├── _command_info.py │ ├── _expiring_members_set.py │ ├── _hash.py │ ├── _stream.py │ ├── _timeseries_model.py │ ├── _topk.py │ └── _zset.py ├── py.typed ├── server_specific_commands │ ├── __init__.py │ └── dragonfly_mixin.py ├── stack │ ├── __init__.py │ ├── _bf_mixin.py │ ├── _cf_mixin.py │ ├── _cms_mixin.py │ ├── _json_mixin.py │ ├── _tdigest_mixin.py │ ├── _timeseries_mixin.py │ └── _topk_mixin.py └── typing.py ├── mkdocs.yml ├── pyproject.toml ├── redis-conf ├── redis-stack.conf └── users.acl ├── scripts ├── create_issues.py ├── generate_command_info.py └── generate_supported_commands_doc.py ├── test ├── __init__.py ├── conftest.py ├── test_asyncredis.py ├── test_hypothesis │ ├── __init__.py │ ├── _server_info.py │ ├── base.py │ ├── test_connection.py │ ├── test_hash.py │ ├── test_list.py │ ├── test_server.py │ ├── test_set.py │ ├── test_string.py │ ├── test_transaction.py │ └── test_zset.py ├── test_hypothesis_joint.py ├── test_internals │ ├── __init__.py │ ├── test_acl_save_load.py │ ├── test_asyncredis.py │ ├── test_extract_args.py │ ├── test_init_args.py │ ├── test_lua_modules.py │ ├── test_mock.py │ ├── test_transactions.py │ └── test_xstream.py ├── test_json │ ├── __init__.py │ ├── test_json.py │ ├── test_json_arr_commands.py │ └── test_json_commands.py ├── test_mixins │ ├── __init__.py │ ├── test_acl_commands.py │ ├── test_bitmap_commands.py │ ├── test_connection.py │ ├── test_generic_commands.py │ ├── test_geo_commands.py │ ├── test_hash_commands.py │ ├── test_hash_expire_commands.py │ ├── test_hash_expire_redispy6.py │ ├── test_list_commands.py │ ├── test_pubsub_commands.py │ ├── test_redis6.py │ ├── test_scan.py │ ├── test_scripting.py │ ├── test_server_commands.py │ ├── test_set_commands.py │ ├── test_sortedset_commands.py │ ├── test_streams_commands.py │ ├── test_string_commands.py │ └── test_zadd.py ├── test_stack │ ├── __init__.py │ ├── test_bloomfilter.py │ ├── test_cms.py │ ├── test_cuckoofilter.py │ ├── test_tdigest.py │ ├── test_timeseries.py │ └── test_topk.py ├── test_tcp_server │ ├── __init__.py │ └── test_connectivity.py ├── test_transactions.py └── testtools.py ├── tox.ini └── uv.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cunla -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tidelift: "pypi/fakeredis" 3 | github: cunla 4 | polar: cunla 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | Add the code you are trying to run, the stacktrace you are getting and anything else that might help. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - python version 30 | - redis-py version 31 | - full requirements.txt? 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/test-coverage/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run test with coverage' 2 | description: 'Greet someone' 3 | inputs: 4 | github-secret: 5 | description: 'GITHUB_TOKEN' 6 | required: true 7 | gist-secret: 8 | description: 'gist secret' 9 | required: true 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Test with coverage 14 | shell: bash 15 | run: | 16 | uv run ruff check 17 | uv run pytest -v --cov=fakeredis --cov-branch 18 | uv run coverage json 19 | echo "COVERAGE=$(jq '.totals.percent_covered_display|tonumber' coverage.json)" >> $GITHUB_ENV 20 | - name: Create coverage badge 21 | if: ${{ github.event_name == 'push' }} 22 | uses: schneegans/dynamic-badges-action@7142847813c746736c986b42dec98541e49a2cea 23 | with: 24 | auth: ${{ inputs.gist-secret }} 25 | gistID: b756396efb895f0e34558c980f1ca0c7 26 | filename: fakeredis-py.json 27 | label: coverage 28 | message: ${{ env.COVERAGE }}% 29 | color: green 30 | - name: Coverage report 31 | if: ${{ github.event_name == 'pull_request' }} 32 | id: coverage_report 33 | shell: bash 34 | run: | 35 | echo 'REPORT<> $GITHUB_ENV 36 | uv run coverage report >> $GITHUB_ENV 37 | echo 'EOF' >> $GITHUB_ENV 38 | - uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 39 | if: ${{ github.event_name == 'pull_request' }} 40 | with: 41 | message: | 42 | Coverage report: 43 | ``` 44 | ${{ env.REPORT }} 45 | ``` 46 | repo-token: ${{ inputs.github-secret }} 47 | allow-repeats: false 48 | message-id: coverage 49 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "pip" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | 17 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: 'v$RESOLVED_VERSION 🌈' 3 | tag-template: 'v$RESOLVED_VERSION' 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - 'enhancement' 9 | - title: '🐛 Bug Fixes' 10 | labels: 11 | - 'fix' 12 | - 'bugfix' 13 | - 'bug' 14 | - title: '🧰 Maintenance' 15 | label: 'chore' 16 | - title: '⬆️ Dependency Updates' 17 | label: 'dependencies' 18 | change-template: '- $TITLE (#$NUMBER)' 19 | change-title-escapes: '\<*_&' 20 | autolabeler: 21 | - label: 'chore' 22 | files: 23 | - '*.md' 24 | - '.github/*' 25 | - label: 'bug' 26 | title: 27 | - '/fix/i' 28 | - label: 'dependencies' 29 | files: 30 | - 'uv.lock' 31 | version-resolver: 32 | major: 33 | labels: 34 | - 'breaking' 35 | minor: 36 | labels: 37 | - 'feature' 38 | - 'enhancement' 39 | patch: 40 | labels: 41 | - 'chore' 42 | - 'dependencies' 43 | - 'bug' 44 | default: patch 45 | template: | 46 | # Changes 47 | 48 | $CHANGES 49 | 50 | ## Contributors 51 | We'd like to thank all the contributors who worked on this release! 52 | 53 | $CONTRIBUTORS 54 | 55 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 56 | -------------------------------------------------------------------------------- /.github/workflows/publish-documentation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Generate and publish documentation 4 | 5 | on: 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish_documentation: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | environment: 16 | name: pypi 17 | url: https://pypi.org/p/fakeredis 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | persist-credentials: false 22 | - name: Set up Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.13" 26 | - name: Configure Git Credentials 27 | run: | 28 | git config user.name github-actions[bot] 29 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 30 | - name: Publish documentation 31 | env: 32 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }} 34 | run: | 35 | pip install -r docs/requirements.txt 36 | mkdocs gh-deploy --force 37 | mkdocs --version 38 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Upload Python Package to PyPI 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 14 | environment: 15 | name: pypi 16 | url: https://pypi.org/p/fakeredis 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.13" 25 | - name: Install dependencies 26 | env: 27 | PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install build 31 | - name: Build package 32 | run: python -m build 33 | 34 | - name: Publish package to pypi 35 | uses: pypa/gh-action-pypi-publish@v1.12.4 36 | with: 37 | print-hash: true 38 | -------------------------------------------------------------------------------- /.github/workflows/test-dragonfly.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Dragonfly 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | 8 | concurrency: 9 | group: dragon-fly-${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | tests: 19 | - "test_json" 20 | - "test_mixins" 21 | - "test_stack" 22 | - "test_connection.py" 23 | - "test_asyncredis.py" 24 | - "test_general.py" 25 | - "test_scan.py" 26 | - "test_zadd.py" 27 | - "test_translations.py" 28 | - "test_sortedset_commands.py" 29 | permissions: 30 | pull-requests: write 31 | services: 32 | redis: 33 | image: docker.dragonflydb.io/dragonflydb/dragonfly:latest 34 | ports: 35 | - 6390:6379 36 | options: >- 37 | --health-cmd "redis-cli ping" 38 | --health-interval 10s 39 | --health-timeout 5s 40 | --health-retries 5 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | persist-credentials: false 46 | - uses: actions/setup-python@v5 47 | with: 48 | cache-dependency-path: uv.lock 49 | python-version: 3.13 50 | - name: Install dependencies 51 | env: 52 | PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring 53 | run: | 54 | uv sync --extra json --extra bf --extra lua --extra cf 55 | 56 | - name: Test without coverage 57 | run: | 58 | uv run pytest test/${{ matrix.tests }} \ 59 | --html=report-${{ matrix.tests }}.html \ 60 | --self-contained-html \ 61 | -v 62 | - name: Upload Tests Result 63 | if: always() 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: tests-result-${{ matrix.tests }} 67 | path: report-${{ matrix.tests }}.html 68 | 69 | upload-results: 70 | needs: test 71 | if: always() 72 | runs-on: ubuntu-latest 73 | permissions: 74 | contents: read 75 | steps: 76 | - name: Collect Tests Result 77 | uses: actions/upload-artifact/merge@v4 78 | with: 79 | delete-merged: true 80 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unit tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | lint: 18 | name: "Code linting (ruff)" 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | persist-credentials: false 26 | - name: Install uv 27 | uses: astral-sh/setup-uv@v6 28 | - uses: actions/setup-python@v5 29 | with: 30 | cache-dependency-path: uv.lock 31 | python-version: "3.13" 32 | - name: Run ruff 33 | shell: bash 34 | run: | 35 | uv run ruff check 36 | - name: Test import 37 | run: | 38 | uv build 39 | pip install dist/fakeredis-*.tar.gz 40 | python -c "import fakeredis" 41 | test: 42 | name: > 43 | tests 44 | py:${{ matrix.python-version }},${{ matrix.redis-image }}, 45 | redis-py:${{ matrix.redis-py }},cov:${{ matrix.coverage }}, 46 | extra:${{matrix.install-extras}} 47 | needs: 48 | - "lint" 49 | runs-on: ubuntu-latest 50 | strategy: 51 | max-parallel: 8 52 | fail-fast: false 53 | matrix: 54 | redis-image: [ "redis:6.2.17", "redis:7.4.2", "redis:8.0.0", "valkey/valkey:8.1" ] 55 | python-version: [ "3.9", "3.12", "3.13" ] 56 | redis-py: [ "4.6.0", "5.2.1", "5.3.0", "6.2.0" ] 57 | install-extras: [ false ] 58 | coverage: [ false ] 59 | exclude: 60 | - python-version: "3.13" 61 | redis-image: "redis:8.0.0" 62 | redis-py: "6.2.0" 63 | install-extras: false 64 | coverage: false 65 | include: 66 | - python-version: "3.13" 67 | redis-image: "redis/redis-stack-server:6.2.6-v20" 68 | redis-py: "6.2.0" 69 | install-extras: true 70 | - python-version: "3.13" 71 | redis-image: "redis/redis-stack-server:7.4.0-v3" 72 | redis-py: "6.2.0" 73 | install-extras: true 74 | - python-version: "3.13" 75 | redis-image: "redis:8.0.0" 76 | redis-py: "6.2.0" 77 | install-extras: true 78 | coverage: true 79 | permissions: 80 | pull-requests: write 81 | services: 82 | keydb: 83 | image: ${{ matrix.redis-image }} 84 | ports: 85 | - 6390:6379 86 | options: >- 87 | --health-cmd "redis-cli ping" 88 | --health-interval 10s 89 | --health-timeout 5s 90 | --health-retries 5 91 | outputs: 92 | version: ${{ steps.getVersion.outputs.VERSION }} 93 | steps: 94 | - uses: actions/checkout@v4 95 | with: 96 | persist-credentials: false 97 | - name: Install uv 98 | uses: astral-sh/setup-uv@v6 99 | - uses: actions/setup-python@v5 100 | with: 101 | cache-dependency-path: uv.lock 102 | python-version: ${{ matrix.python-version }} 103 | - name: Install dependencies 104 | env: 105 | PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring 106 | run: | 107 | uv sync --extra lua 108 | if [ ${{ matrix.install-extras }} == true ]; then 109 | uv sync --all-extras 110 | fi 111 | uv pip install redis==${{ matrix.redis-py }} 112 | - name: Get version 113 | id: getVersion 114 | shell: bash 115 | run: | 116 | VERSION=$(uv version --short) 117 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 118 | - name: Test without coverage 119 | if: ${{ !matrix.coverage }} 120 | run: | 121 | uv run pytest -v -m "not slow" 122 | - name: Test with coverage 123 | if: ${{ matrix.coverage }} 124 | uses: ./.github/actions/test-coverage 125 | with: 126 | github-secret: ${{ secrets.GITHUB_TOKEN }} 127 | gist-secret: ${{ secrets.GIST_SECRET }} 128 | # Prepare a draft release for GitHub Releases page for the manual verification 129 | # If accepted and published, release workflow would be triggered 130 | update_release_draft: 131 | name: "Create or Update release draft" 132 | permissions: 133 | # write permission is required to create a GitHub release 134 | contents: write 135 | # write permission is required for auto-labeler 136 | # otherwise, read permission is required at least 137 | pull-requests: write 138 | needs: 139 | - "test" 140 | runs-on: ubuntu-latest 141 | steps: 142 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 143 | env: 144 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 145 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | unpinned-images: 3 | ignore: 4 | - 'test.yml' 5 | - 'test-dragonfly.yml' 6 | unpinned-uses: 7 | config: 8 | policies: 9 | actions/*: any 10 | astral-sh/*: any 11 | pypa/gh-action-pypi-publish: any 12 | github-env: 13 | ignore: 14 | - 'action.yml:34:7' 15 | - 'action.yml:15:7' 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | scripts/*.json 2 | fakeredis.egg-info 3 | dump.rdb 4 | extras/* 5 | .tox 6 | *.pyc 7 | .idea 8 | .hypothesis 9 | .coverage 10 | cover/ 11 | venv/ 12 | dist/ 13 | build/* 14 | docker-compose.yml 15 | .DS_Store 16 | *.iml 17 | .venv/ 18 | .fleet 19 | .mypy_cache 20 | .pytest_cache 21 | **/__pycache__ 22 | scratch* 23 | .python-version 24 | .env 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-json 7 | - id: check-xml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/astral-sh/ruff-pre-commit 11 | rev: v0.11.5 12 | hooks: 13 | - id: ruff 14 | - id: ruff-format 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: "ubuntu-20.04" 4 | tools: 5 | python: "3.12" 6 | 7 | mkdocs: 8 | configuration: mkdocs.yml 9 | fail_on_warning: false 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022-, Daniel Moran, 2017-2018, Bruce Merry, 2011 James Saryerwinnie, 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fakeredis: A fake version of a redis-py 2 | ======================================= 3 | 4 | [![badge](https://img.shields.io/pypi/v/fakeredis)](https://pypi.org/project/fakeredis/) 5 | [![CI](https://github.com/cunla/fakeredis-py/actions/workflows/test.yml/badge.svg)](https://github.com/cunla/fakeredis-py/actions/workflows/test.yml) 6 | [![badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/cunla/b756396efb895f0e34558c980f1ca0c7/raw/fakeredis-py.json)](https://github.com/cunla/fakeredis-py/actions/workflows/test.yml) 7 | [![badge](https://img.shields.io/pypi/dm/fakeredis)](https://pypi.org/project/fakeredis/) 8 | [![badge](https://img.shields.io/pypi/l/fakeredis)](./LICENSE) 9 | [![Open Source Helpers](https://www.codetriage.com/cunla/fakeredis-py/badges/users.svg)](https://www.codetriage.com/cunla/fakeredis-py) 10 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 11 | -------------------- 12 | 13 | 14 | Documentation is hosted in https://fakeredis.readthedocs.io/ 15 | 16 | # Intro 17 | 18 | FakeRedis is a pure-Python implementation of the Redis protocol API. It provides enhanced versions of 19 | the [redis-py][redis-py] Python bindings for Redis. 20 | 21 | It enables running tests requiring [Redis][redis]/[ValKey][valkey]/[DragonflyDB][dragonflydb]/[KeyDB][keydb] server 22 | without an actual server. 23 | 24 | It also enables testing compatibility of different key-value datastores. 25 | 26 | That provides the following added functionality: A built-in Redis server that is automatically installed, configured and 27 | managed when the Redis bindings are used. A single server shared by multiple programs or multiple independent servers. 28 | All the servers provided by FakeRedis support all Redis functionality including advanced features such as RedisJson, 29 | RedisBloom, GeoCommands. 30 | 31 | See [official documentation][readthedocs] for list of supported commands. 32 | 33 | # Sponsor 34 | 35 | fakeredis-py is developed for free. 36 | 37 | You can support this project by becoming a sponsor using [this link](https://github.com/sponsors/cunla). 38 | 39 | [readthedocs]: https://fakeredis.readthedocs.io/ 40 | 41 | [redis-py]: https://github.com/redis/redis-py 42 | 43 | [valkey]: https://github.com/valkey-io/valkey 44 | 45 | [redis]: https://redis.io/ 46 | 47 | [dragonflydb]: https://dragonflydb.io/ 48 | 49 | [keydb]: https://docs.keydb.dev/ -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | |---------|--------------------| 10 | | 2.18.x | :white_check_mark: | 11 | | 1.10.x | :white_check_mark: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | To report a security vulnerability, please use the 16 | [Tidelift security contact](https://tidelift.com/security). 17 | Tidelift will coordinate the fix and disclosure. 18 | -------------------------------------------------------------------------------- /docs/about/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | The legal stuff. 4 | 5 | --- 6 | 7 | BSD 3-Clause License 8 | 9 | Copyright (c) 2022-, Daniel Moran, 2017-2018, Bruce Merry, 2011 James Saryerwinnie, 10 | All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | 1. Redistributions of source code must retain the above copyright notice, this 16 | list of conditions and the following disclaimer. 17 | 18 | 2. Redistributions in binary form must reproduce the above copyright notice, 19 | this list of conditions and the following disclaimer in the documentation 20 | and/or other materials provided with the distribution. 21 | 22 | 3. Neither the name of the copyright holder nor the names of its 23 | contributors may be used to endorse or promote products derived from 24 | this software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 28 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 30 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 31 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /docs/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-2802331499006697, DIRECT, f08c47fec0942fa0 2 | -------------------------------------------------------------------------------- /docs/dragonfly-support.md: -------------------------------------------------------------------------------- 1 | # Support for Dragonfly 2 | 3 | [Dragonfly DB][1] is a drop-in Redis replacement that cuts costs and boosts performance. Designed to fully utilize the 4 | power of modern cloud hardware and deliver on the data demands of modern applications, Dragonfly frees developers from 5 | the limits of traditional in-memory data stores. 6 | 7 | FakeRedis can be used as a Dragonfly replacement for testing and development purposes as well. 8 | 9 | Since Dragonfly does not have its own unique clients, you can use the `Fakeredis` client to connect to a Dragonfly. 10 | 11 | ```python 12 | from fakeredis import FakeRedis 13 | 14 | client = FakeRedis(server_type="dragonfly") 15 | client.set("key", "value") 16 | print(client.get("key")) 17 | ``` 18 | 19 | Alternatively, you can start a thread with a Fake Valkey server. 20 | 21 | ```python 22 | from threading import Thread 23 | from fakeredis import TcpFakeServer 24 | 25 | server_address = ("127.0.0.1", 6379) 26 | server = TcpFakeServer(server_address, server_type="dragonfly") 27 | t = Thread(target=server.serve_forever, daemon=True) 28 | t.start() 29 | 30 | import redis 31 | 32 | r = redis.Redis(host=server_address[0], port=server_address[1]) 33 | r.set("foo", "bar") 34 | assert r.get("foo") == b"bar" 35 | 36 | ``` 37 | 38 | To call Dragonfly specific commands, which are not implemented in the redis-py client, you can use the 39 | `execute_command`, like in this example calling the [`SADDEX`][2] command: 40 | 41 | ```python 42 | from fakeredis import FakeRedis 43 | 44 | client = FakeRedis(server_type="dragonfly") 45 | client.sadd("key", "value") 46 | # The SADDEX command is not implemented in redis-py 47 | client.execute_command("SADDEX", 10, "key", "value") 48 | 49 | ``` 50 | 51 | [1]: https://www.dragonflydb.io/ 52 | 53 | [2]: https://www.dragonflydb.io/docs/command-reference/sets/saddex -------------------------------------------------------------------------------- /docs/guides/implement-command.md: -------------------------------------------------------------------------------- 1 | # Implementing support for a command 2 | 3 | Creating a new command support should be done in the `FakeSocket` class (in `_fakesocket.py`) by creating the method 4 | and using `@command` decorator (which should be the command syntax, you can use existing samples on the file). 5 | 6 | For example: 7 | 8 | ```python 9 | class FakeSocket(BaseFakeSocket, FakeLuaSocket): 10 | # ... 11 | @command(name='zscore', fixed=(Key(ZSet), bytes), repeat=(), flags=[]) 12 | def zscore(self, key, member): 13 | try: 14 | return self._encodefloat(key.value[member], False) 15 | except KeyError: 16 | return None 17 | ``` 18 | 19 | ## Parsing command arguments 20 | 21 | The `extract_args` method should help to extract arguments from `*args`. 22 | It extracts from actual arguments which arguments exist and their value if relevant. 23 | 24 | Parameters `extract_args` expect: 25 | 26 | - `actual_args` 27 | The actual arguments to parse 28 | - `expected` 29 | Arguments to look for, see below explanation. 30 | - `error_on_unexpected` (default: True) 31 | Should an error be raised when actual_args contain an unexpected argument? 32 | - `left_from_first_unexpected` (default: True) 33 | Once reaching an unexpected argument in actual_args, 34 | Should parsing stop? 35 | 36 | It returns two lists: 37 | 38 | - List of values for expected arguments. 39 | - List of remaining args. 40 | 41 | ### Expected argument structure: 42 | 43 | - If expected argument has only a name, it will be parsed as boolean 44 | (Whether it exists in actual `*args` or not). 45 | - In order to parse a numerical value following the expected argument, 46 | a `+` prefix is needed, e.g., `+px` will parse `args=('px', '1')` as `px=1` 47 | - In order to parse a string value following the expected argument, 48 | a `*` prefix is needed, e.g., `*type` will parse `args=('type', 'number')` as `type='number'` 49 | - You can have more than one `+`/`*`, e.g., `++limit` will parse `args=('limit','1','10')` 50 | as `limit=(1,10)` 51 | 52 | ## How to use `@command` decorator 53 | 54 | The `@command` decorator register the method as a redis command and define the accepted format for it. 55 | It will create a `Signature` instance for the command. Whenever the command is triggered, the `Signature.apply(..)` 56 | method will be triggered to check the validity of syntax and analyze the command arguments. 57 | 58 | By default, it takes the name of the method as the command name. 59 | 60 | If the method implements a subcommand (e.g., `SCRIPT LOAD`), a Redis module command (e.g., `JSON.GET`), 61 | or a python reserve word where you can not use it as the method name (e.g., `EXEC`), then you can explicitly supply 62 | the name parameter. 63 | 64 | If the command implemented requires certain arguments, they can be supplied in the first parameter as a tuple. 65 | When receiving the command through the socket, the bytes will be converted to the argument types 66 | supplied or remain as `bytes`. 67 | 68 | Argument types (All in `_commands.py`): 69 | 70 | - `Key(KeyType)` - Will get from the DB the key and validate its value is of `KeyType` (if `KeyType` is supplied). 71 | It will generate a `CommandItem` from it which provides access to the database value. 72 | - `Int` - Decode the `bytes` to `int` and vice versa. 73 | - `DbIndex`/`BitOffset`/`BitValue`/`Timeout` - Basically the same behavior as `Int`, but with different messages when 74 | encode/decode fail. 75 | - `Hash` - dictionary, usually describe the type of value stored in Key `Key(Hash)` 76 | - `Float` - Encode/Decode `bytes` <-> `float` 77 | - `SortFloat` - Similar to `Float` with different error messages. 78 | - `ScoreTest` - Argument converter for sorted set score endpoints. 79 | - `StringTest` - Argument converter for sorted set endpoints (lex). 80 | - `ZSet` - Sorted Set. 81 | 82 | ## Implement a test for it 83 | 84 | There are multiple scenarios for test, with different versions of redis server, redis-py, etc. 85 | The tests not only assert the validity of output but runs the same test on a real redis-server and compares the output 86 | to the real server output. 87 | 88 | - Create tests in the relevant test file. 89 | - If support for the command was introduced in a certain version of redis-py 90 | (see [redis-py release notes](https://github.com/redis/redis-py/releases/tag/v4.3.4)) you can use the 91 | decorator `@testtools.run_test_if_redispy_ver` on your tests. example: 92 | 93 | ```python 94 | @testtools.run_test_if_redispy_ver('gte', '4.2.0') # This will run for redis-py 4.2.0 or above. 95 | def test_expire_should_not_expire__when_no_expire_is_set(r): 96 | r.set('foo', 'bar') 97 | assert r.get('foo') == b'bar' 98 | assert r.expire('foo', 1, xx=True) == 0 99 | ``` 100 | 101 | ## Updating documentation 102 | 103 | Lastly, run from the root of the project the script to regenerate documentation for 104 | supported and unsupported commands: 105 | 106 | ```bash 107 | python scripts/generate_supported_commands_doc.py 108 | ``` 109 | 110 | Include the changes in the `docs/` directory in your pull request. 111 | 112 | -------------------------------------------------------------------------------- /docs/guides/test-case.md: -------------------------------------------------------------------------------- 1 | 2 | # Write a new test case 3 | 4 | There are multiple scenarios for test, with different versions of python, redis-py and redis server, etc. 5 | The tests not only assert the validity of the expected output with FakeRedis but also with a real redis server. 6 | That way parity of real Redis and FakeRedis is ensured. 7 | 8 | To write a new test case for a command: 9 | 10 | - Determine which mixin the command belongs to and the test file for 11 | the mixin (e.g., `string_mixin.py` => `test_string_commands.py`). 12 | - Tests should support python 3.7 and above. 13 | - Determine when support for the command was introduced 14 | - To limit the redis-server versions, it will run on use: 15 | `@pytest.mark.max_server(version)` and `@pytest.mark.min_server(version)` 16 | - To limit the redis-py version use `@run_test_if_redispy_ver('gte', version)` 17 | (you can use `ge`/`gte`/`lte`/`lt`/`eq`/`ne`). 18 | - pytest will inject a redis connection to the argument `r` of the test. 19 | 20 | Sample of running a test for redis-py v4.2.0 and above, redis-server 7.0 and above. 21 | 22 | ```python 23 | @pytest.mark.min_server('7') 24 | @testtools.run_test_if_redispy_ver('gte', '4.2.0') 25 | def test_expire_should_not_expire__when_no_expire_is_set(r): 26 | r.set('foo', 'bar') 27 | assert r.get('foo') == b'bar' 28 | assert r.expire('foo', 1, xx=True) == 0 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block extrahead %} 3 | 4 | 6 | {% endblock %} -------------------------------------------------------------------------------- /docs/overrides/partials/toc-item.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 | {{ toc_item.title }} 5 | 6 | 7 | 8 | 9 | {% if toc_item.children %} 10 | 19 | {% endif %} 20 |
  • -------------------------------------------------------------------------------- /docs/redis-stack.md: -------------------------------------------------------------------------------- 1 | # Support for redis-stack 2 | 3 | To install all supported modules, you can install fakeredis with `pip install fakeredis[lua,json,bf]`. 4 | 5 | ## RedisJson support 6 | 7 | The JSON capability of Redis Stack provides JavaScript Object Notation (JSON) support for Redis. It lets you store, 8 | update, and retrieve JSON values in a Redis database, similar to any other Redis data type. Redis JSON also works 9 | seamlessly with Search and Query to let you index and query JSON documents. 10 | 11 | JSONPath's syntax: The following JSONPath syntax table was adapted from Goessner's [path syntax comparison][4]. 12 | 13 | Currently, Redis Json module is fully implemented (see [supported commands][1]). 14 | Support for JSON commands (e.g., [`JSON.GET`][2]) is implemented using 15 | [jsonpath-ng,][3] you can install it using `pip install 'fakeredis[json]'`. 16 | 17 | ```pycon 18 | >>> import fakeredis 19 | >>> from redis.commands.json.path import Path 20 | >>> r = fakeredis.FakeStrictRedis() 21 | >>> assert r.json().set("foo", Path.root_path(), {"x": "bar"}, ) == 1 22 | >>> r.json().get("foo") 23 | {'x': 'bar'} 24 | >>> r.json().get("foo", Path("x")) 25 | 'bar' 26 | ``` 27 | 28 | ## Bloom filter support 29 | 30 | Bloom filters are a probabilistic data structure that checks for the presence of an element in a set. 31 | 32 | Instead of storing all the elements in the set, Bloom Filters store only the elements' hashed representation, thus 33 | sacrificing some precision. The trade-off is that Bloom Filters are very space-efficient and fast. 34 | 35 | You can get a false positive result, but never a false negative, i.e., if the bloom filter says that an element is not 36 | 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 37 | most likely in the set, but it is not guaranteed. 38 | 39 | Currently, RedisBloom module bloom filter commands are fully implemented using [pybloom-live][5]( 40 | see [supported commands][6]). 41 | 42 | You can install it using `pip install 'fakeredis[probabilistic]'`. 43 | 44 | ```pycon 45 | >>> import fakeredis 46 | >>> r = fakeredis.FakeStrictRedis() 47 | >>> r.bf().madd('key', 'v1', 'v2', 'v3') == [1, 1, 1] 48 | >>> r.bf().exists('key', 'v1') 49 | 1 50 | >>> r.bf().exists('key', 'v5') 51 | 0 52 | ``` 53 | 54 | ## [Count-Min Sketch][8] support 55 | 56 | Count-min sketch is a probabilistic data structure that estimates the frequency of an element in a data stream. 57 | 58 | You can install it using `pip install 'fakeredis[probabilistic]'`. 59 | 60 | ```pycon 61 | >>> import fakeredis 62 | >>> r = fakeredis.FakeStrictRedis() 63 | >>> r.cms().initbydim("cmsDim", 100, 5) 64 | OK 65 | >>> r.cms().incrby("cmsDim", ["foo"], [3]) 66 | [3] 67 | ``` 68 | 69 | ## [Cuckoo filter][9] support 70 | 71 | Cuckoo filters are a probabilistic data structure that checks for the presence of an element in a set 72 | 73 | You can install it using `pip install 'fakeredis[probabilistic]'`. 74 | 75 | ## [Redis programmability][7] 76 | 77 | Redis provides a programming interface that lets you execute custom scripts on the server itself. In Redis 7 and beyond, 78 | you can use Redis Functions to manage and run your scripts. In Redis 6.2 and below, you use Lua scripting with the EVAL 79 | command to program the server. 80 | 81 | If you wish to have Lua scripting support (this includes features like ``redis.lock.Lock``, which are implemented in 82 | Lua), you will need [lupa][10], you can install it using `pip install 'fakeredis[lua]'` 83 | 84 | By default, FakeRedis works with LUA version 5.1, to use a different version supported by lupa, 85 | set the `FAKEREDIS_LUA_VERSION` environment variable to the desired version (e.g., `5.4`). 86 | 87 | ### LUA binary modules 88 | 89 | fakeredis supports using LUA binary modules as well. In order to have your FakeRedis instance load a LUA binary module, 90 | you can use the `lua_modules` parameter. 91 | 92 | ```pycon 93 | >>> import fakeredis 94 | >>> r = fakeredis.FakeStrictRedis(lua_modules={"my_module.so"}) 95 | ``` 96 | 97 | The module `.so`/`.dll` file should be in the working directory. 98 | 99 | To install LUA modules, you can use [luarocks][11] to install the module and then copy the `.so`/`.dll` file to the 100 | working directory. 101 | 102 | For example, to install `lua-cjson`: 103 | 104 | ```sh 105 | luarocks install lua-cjson 106 | cp /opt/homebrew/lib/lua/5.4/cjson.so `pwd` 107 | ``` 108 | 109 | [1]:./supported-commands/RedisJson/ 110 | 111 | [2]:https://redis.io/commands/json.get/ 112 | 113 | [3]:https://github.com/h2non/jsonpath-ng 114 | 115 | [4]:https://goessner.net/articles/JsonPath/index.html#e2 116 | 117 | [5]:https://github.com/joseph-fox/python-bloomfilter 118 | 119 | [6]:./supported-commands/BloomFilter/ 120 | 121 | [7]:https://redis.io/docs/interact/programmability/ 122 | 123 | [8]:https://redis.io/docs/data-types/probabilistic/count-min-sketch/ 124 | 125 | [9]:https://redis.io/docs/data-types/probabilistic/cuckoo-filter/ 126 | 127 | [10]:https://pypi.org/project/lupa/ 128 | 129 | [11]:https://luarocks.org/ 130 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.6.1 2 | mkdocs-material==9.6.14 3 | -------------------------------------------------------------------------------- /docs/supported-commands/DRAGONFLY.md: -------------------------------------------------------------------------------- 1 | # Dragonfly specific commands 2 | 3 | > To implement support for a command, see [here](/guides/implement-command/) 4 | 5 | These are commands that are not implemented in Redis but supported in Dragonfly and FakeRedis. To use these commands, 6 | you can call `execute_command` with the command name and arguments as follows: 7 | 8 | ```python 9 | client = FakeRedis(server_type="dragonfly") 10 | client.execute_command("SADDEX", 10, "key", "value") 11 | ``` 12 | 13 | ## [SADDEX](https://www.dragonflydb.io/docs/command-reference/sets/saddex) 14 | 15 | Similar to SADD but adds one or more members that expire after specified number of seconds. An error is returned when 16 | the value stored at key is not a set. 17 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/BITMAP.md: -------------------------------------------------------------------------------- 1 | # Redis `bitmap` commands (6/6 implemented) 2 | 3 | ## [BITCOUNT](https://redis.io/commands/bitcount/) 4 | 5 | Counts the number of set bits (population counting) in a string. 6 | 7 | ## [BITFIELD](https://redis.io/commands/bitfield/) 8 | 9 | Performs arbitrary bitfield integer operations on strings. 10 | 11 | ## [BITOP](https://redis.io/commands/bitop/) 12 | 13 | Performs bitwise operations on multiple strings, and stores the result. 14 | 15 | ## [BITPOS](https://redis.io/commands/bitpos/) 16 | 17 | Finds the first set (1) or clear (0) bit in a string. 18 | 19 | ## [GETBIT](https://redis.io/commands/getbit/) 20 | 21 | Returns a bit value by offset. 22 | 23 | ## [SETBIT](https://redis.io/commands/setbit/) 24 | 25 | Sets or clears the bit at offset of the string value. Creates the key if it doesn't exist. 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/CONNECTION.md: -------------------------------------------------------------------------------- 1 | # Redis `connection` commands (11/24 implemented) 2 | 3 | ## [AUTH](https://redis.io/commands/auth/) 4 | 5 | Authenticates the connection. 6 | 7 | ## [CLIENT GETNAME](https://redis.io/commands/client-getname/) 8 | 9 | Returns the name of the connection. 10 | 11 | ## [CLIENT ID](https://redis.io/commands/client-id/) 12 | 13 | Returns the unique client ID of the connection. 14 | 15 | ## [CLIENT INFO](https://redis.io/commands/client-info/) 16 | 17 | Returns information about the connection. 18 | 19 | ## [CLIENT LIST](https://redis.io/commands/client-list/) 20 | 21 | Lists open connections. 22 | 23 | ## [CLIENT SETINFO](https://redis.io/commands/client-setinfo/) 24 | 25 | Sets information specific to the client or connection. 26 | 27 | ## [CLIENT SETNAME](https://redis.io/commands/client-setname/) 28 | 29 | Sets the connection name. 30 | 31 | ## [ECHO](https://redis.io/commands/echo/) 32 | 33 | Returns the given string. 34 | 35 | ## [HELLO](https://redis.io/commands/hello/) 36 | 37 | Handshakes with the Redis server. 38 | 39 | ## [PING](https://redis.io/commands/ping/) 40 | 41 | Returns the server's liveliness response. 42 | 43 | ## [SELECT](https://redis.io/commands/select/) 44 | 45 | Changes the selected database. 46 | 47 | 48 | ## Unsupported connection commands 49 | > To implement support for a command, see [here](/guides/implement-command/) 50 | 51 | #### [CLIENT](https://redis.io/commands/client/) (not implemented) 52 | 53 | A container for client connection commands. 54 | 55 | #### [CLIENT CACHING](https://redis.io/commands/client-caching/) (not implemented) 56 | 57 | Instructs the server whether to track the keys in the next request. 58 | 59 | #### [CLIENT GETREDIR](https://redis.io/commands/client-getredir/) (not implemented) 60 | 61 | Returns the client ID to which the connection's tracking notifications are redirected. 62 | 63 | #### [CLIENT KILL](https://redis.io/commands/client-kill/) (not implemented) 64 | 65 | Terminates open connections. 66 | 67 | #### [CLIENT NO-EVICT](https://redis.io/commands/client-no-evict/) (not implemented) 68 | 69 | Sets the client eviction mode of the connection. 70 | 71 | #### [CLIENT NO-TOUCH](https://redis.io/commands/client-no-touch/) (not implemented) 72 | 73 | Controls whether commands sent by the client affect the LRU/LFU of accessed keys. 74 | 75 | #### [CLIENT PAUSE](https://redis.io/commands/client-pause/) (not implemented) 76 | 77 | Suspends commands processing. 78 | 79 | #### [CLIENT REPLY](https://redis.io/commands/client-reply/) (not implemented) 80 | 81 | Instructs the server whether to reply to commands. 82 | 83 | #### [CLIENT TRACKING](https://redis.io/commands/client-tracking/) (not implemented) 84 | 85 | Controls server-assisted client-side caching for the connection. 86 | 87 | #### [CLIENT TRACKINGINFO](https://redis.io/commands/client-trackinginfo/) (not implemented) 88 | 89 | Returns information about server-assisted client-side caching for the connection. 90 | 91 | #### [CLIENT UNBLOCK](https://redis.io/commands/client-unblock/) (not implemented) 92 | 93 | Unblocks a client blocked by a blocking command from a different connection. 94 | 95 | #### [CLIENT UNPAUSE](https://redis.io/commands/client-unpause/) (not implemented) 96 | 97 | Resumes processing commands from paused clients. 98 | 99 | #### [RESET](https://redis.io/commands/reset/) (not implemented) 100 | 101 | Resets the connection. 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/GENERIC.md: -------------------------------------------------------------------------------- 1 | # Redis `generic` commands (24/26 implemented) 2 | 3 | ## [COPY](https://redis.io/commands/copy/) 4 | 5 | Copies the value of a key to a new key. 6 | 7 | ## [DEL](https://redis.io/commands/del/) 8 | 9 | Deletes one or more keys. 10 | 11 | ## [DUMP](https://redis.io/commands/dump/) 12 | 13 | Returns a serialized representation of the value stored at a key. 14 | 15 | ## [EXISTS](https://redis.io/commands/exists/) 16 | 17 | Determines whether one or more keys exist. 18 | 19 | ## [EXPIRE](https://redis.io/commands/expire/) 20 | 21 | Sets the expiration time of a key in seconds. 22 | 23 | ## [EXPIREAT](https://redis.io/commands/expireat/) 24 | 25 | Sets the expiration time of a key to a Unix timestamp. 26 | 27 | ## [EXPIRETIME](https://redis.io/commands/expiretime/) 28 | 29 | Returns the expiration time of a key as a Unix timestamp. 30 | 31 | ## [KEYS](https://redis.io/commands/keys/) 32 | 33 | Returns all key names that match a pattern. 34 | 35 | ## [MOVE](https://redis.io/commands/move/) 36 | 37 | Moves a key to another database. 38 | 39 | ## [PERSIST](https://redis.io/commands/persist/) 40 | 41 | Removes the expiration time of a key. 42 | 43 | ## [PEXPIRE](https://redis.io/commands/pexpire/) 44 | 45 | Sets the expiration time of a key in milliseconds. 46 | 47 | ## [PEXPIREAT](https://redis.io/commands/pexpireat/) 48 | 49 | Sets the expiration time of a key to a Unix milliseconds timestamp. 50 | 51 | ## [PEXPIRETIME](https://redis.io/commands/pexpiretime/) 52 | 53 | Returns the expiration time of a key as a Unix milliseconds timestamp. 54 | 55 | ## [PTTL](https://redis.io/commands/pttl/) 56 | 57 | Returns the expiration time in milliseconds of a key. 58 | 59 | ## [RANDOMKEY](https://redis.io/commands/randomkey/) 60 | 61 | Returns a random key name from the database. 62 | 63 | ## [RENAME](https://redis.io/commands/rename/) 64 | 65 | Renames a key and overwrites the destination. 66 | 67 | ## [RENAMENX](https://redis.io/commands/renamenx/) 68 | 69 | Renames a key only when the target key name doesn't exist. 70 | 71 | ## [RESTORE](https://redis.io/commands/restore/) 72 | 73 | Creates a key from the serialized representation of a value. 74 | 75 | ## [SCAN](https://redis.io/commands/scan/) 76 | 77 | Iterates over the key names in the database. 78 | 79 | ## [SORT](https://redis.io/commands/sort/) 80 | 81 | Sorts the elements in a list, a set, or a sorted set, optionally storing the result. 82 | 83 | ## [SORT_RO](https://redis.io/commands/sort_ro/) 84 | 85 | Returns the sorted elements of a list, a set, or a sorted set. 86 | 87 | ## [TTL](https://redis.io/commands/ttl/) 88 | 89 | Returns the expiration time in seconds of a key. 90 | 91 | ## [TYPE](https://redis.io/commands/type/) 92 | 93 | Determines the type of value stored at a key. 94 | 95 | ## [UNLINK](https://redis.io/commands/unlink/) 96 | 97 | Asynchronously deletes one or more keys. 98 | 99 | 100 | ## Unsupported generic commands 101 | > To implement support for a command, see [here](/guides/implement-command/) 102 | 103 | #### [WAIT](https://redis.io/commands/wait/) (not implemented) 104 | 105 | Blocks until the asynchronous replication of all preceding write commands sent by the connection is completed. 106 | 107 | #### [WAITAOF](https://redis.io/commands/waitaof/) (not implemented) 108 | 109 | 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. 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/GEO.md: -------------------------------------------------------------------------------- 1 | # Redis `geo` commands (10/10 implemented) 2 | 3 | ## [GEOADD](https://redis.io/commands/geoadd/) 4 | 5 | Adds one or more members to a geospatial index. The key is created if it doesn't exist. 6 | 7 | ## [GEODIST](https://redis.io/commands/geodist/) 8 | 9 | Returns the distance between two members of a geospatial index. 10 | 11 | ## [GEOHASH](https://redis.io/commands/geohash/) 12 | 13 | Returns members from a geospatial index as geohash strings. 14 | 15 | ## [GEOPOS](https://redis.io/commands/geopos/) 16 | 17 | Returns the longitude and latitude of members from a geospatial index. 18 | 19 | ## [GEORADIUS](https://redis.io/commands/georadius/) 20 | 21 | Queries a geospatial index for members within a distance from a coordinate, optionally stores the result. 22 | 23 | ## [GEORADIUS_RO](https://redis.io/commands/georadius_ro/) 24 | 25 | Returns members from a geospatial index that are within a distance from a coordinate. 26 | 27 | ## [GEORADIUSBYMEMBER](https://redis.io/commands/georadiusbymember/) 28 | 29 | Queries a geospatial index for members within a distance from a member, optionally stores the result. 30 | 31 | ## [GEORADIUSBYMEMBER_RO](https://redis.io/commands/georadiusbymember_ro/) 32 | 33 | Returns members from a geospatial index that are within a distance from a member. 34 | 35 | ## [GEOSEARCH](https://redis.io/commands/geosearch/) 36 | 37 | Queries a geospatial index for members inside an area of a box or a circle. 38 | 39 | ## [GEOSEARCHSTORE](https://redis.io/commands/geosearchstore/) 40 | 41 | Queries a geospatial index for members inside an area of a box or a circle, optionally stores the result. 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/HASH.md: -------------------------------------------------------------------------------- 1 | # Redis `hash` commands (25/27 implemented) 2 | 3 | ## [HDEL](https://redis.io/commands/hdel/) 4 | 5 | Deletes one or more fields and their values from a hash. Deletes the hash if no fields remain. 6 | 7 | ## [HEXISTS](https://redis.io/commands/hexists/) 8 | 9 | Determines whether a field exists in a hash. 10 | 11 | ## [HEXPIRE](https://redis.io/commands/hexpire/) 12 | 13 | Set expiry for hash field using relative time to expire (seconds) 14 | 15 | ## [HEXPIREAT](https://redis.io/commands/hexpireat/) 16 | 17 | Set expiry for hash field using an absolute Unix timestamp (seconds) 18 | 19 | ## [HEXPIRETIME](https://redis.io/commands/hexpiretime/) 20 | 21 | Returns the expiration time of a hash field as a Unix timestamp, in seconds. 22 | 23 | ## [HGET](https://redis.io/commands/hget/) 24 | 25 | Returns the value of a field in a hash. 26 | 27 | ## [HGETALL](https://redis.io/commands/hgetall/) 28 | 29 | Returns all fields and values in a hash. 30 | 31 | ## [HINCRBY](https://redis.io/commands/hincrby/) 32 | 33 | Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist. 34 | 35 | ## [HINCRBYFLOAT](https://redis.io/commands/hincrbyfloat/) 36 | 37 | Increments the floating point value of a field by a number. Uses 0 as initial value if the field doesn't exist. 38 | 39 | ## [HKEYS](https://redis.io/commands/hkeys/) 40 | 41 | Returns all fields in a hash. 42 | 43 | ## [HLEN](https://redis.io/commands/hlen/) 44 | 45 | Returns the number of fields in a hash. 46 | 47 | ## [HMGET](https://redis.io/commands/hmget/) 48 | 49 | Returns the values of all fields in a hash. 50 | 51 | ## [HMSET](https://redis.io/commands/hmset/) 52 | 53 | Sets the values of multiple fields. 54 | 55 | ## [HPERSIST](https://redis.io/commands/hpersist/) 56 | 57 | Removes the expiration time for each specified field 58 | 59 | ## [HPEXPIRE](https://redis.io/commands/hpexpire/) 60 | 61 | Set expiry for hash field using relative time to expire (milliseconds) 62 | 63 | ## [HPEXPIREAT](https://redis.io/commands/hpexpireat/) 64 | 65 | Set expiry for hash field using an absolute Unix timestamp (milliseconds) 66 | 67 | ## [HPEXPIRETIME](https://redis.io/commands/hpexpiretime/) 68 | 69 | Returns the expiration time of a hash field as a Unix timestamp, in msec. 70 | 71 | ## [HPTTL](https://redis.io/commands/hpttl/) 72 | 73 | Returns the TTL in milliseconds of a hash field. 74 | 75 | ## [HRANDFIELD](https://redis.io/commands/hrandfield/) 76 | 77 | Returns one or more random fields from a hash. 78 | 79 | ## [HSCAN](https://redis.io/commands/hscan/) 80 | 81 | Iterates over fields and values of a hash. 82 | 83 | ## [HSET](https://redis.io/commands/hset/) 84 | 85 | Creates or modifies the value of a field in a hash. 86 | 87 | ## [HSETNX](https://redis.io/commands/hsetnx/) 88 | 89 | Sets the value of a field in a hash only when the field doesn't exist. 90 | 91 | ## [HSTRLEN](https://redis.io/commands/hstrlen/) 92 | 93 | Returns the length of the value of a field. 94 | 95 | ## [HTTL](https://redis.io/commands/httl/) 96 | 97 | Returns the TTL in seconds of a hash field. 98 | 99 | ## [HVALS](https://redis.io/commands/hvals/) 100 | 101 | Returns all values in a hash. 102 | 103 | 104 | ## Unsupported hash commands 105 | > To implement support for a command, see [here](/guides/implement-command/) 106 | 107 | #### [HGETF](https://redis.io/commands/hgetf/) (not implemented) 108 | 109 | For each specified field, returns its value and optionally set the field's remaining expiration time in seconds / milliseconds 110 | 111 | #### [HSETF](https://redis.io/commands/hsetf/) (not implemented) 112 | 113 | For each specified field, returns its value and optionally set the field's remaining expiration time in seconds / milliseconds 114 | 115 | 116 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/HYPERLOGLOG.md: -------------------------------------------------------------------------------- 1 | # Redis `hyperloglog` commands (3/3 implemented) 2 | 3 | ## [PFADD](https://redis.io/commands/pfadd/) 4 | 5 | Adds elements to a HyperLogLog key. Creates the key if it doesn't exist. 6 | 7 | ## [PFCOUNT](https://redis.io/commands/pfcount/) 8 | 9 | Returns the approximated cardinality of the set(s) observed by the HyperLogLog key(s). 10 | 11 | ## [PFMERGE](https://redis.io/commands/pfmerge/) 12 | 13 | Merges one or more HyperLogLog values into a single key. 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/LIST.md: -------------------------------------------------------------------------------- 1 | # Redis `list` commands (22/22 implemented) 2 | 3 | ## [BLMOVE](https://redis.io/commands/blmove/) 4 | 5 | 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. 6 | 7 | ## [BLMPOP](https://redis.io/commands/blmpop/) 8 | 9 | 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. 10 | 11 | ## [BLPOP](https://redis.io/commands/blpop/) 12 | 13 | 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. 14 | 15 | ## [BRPOP](https://redis.io/commands/brpop/) 16 | 17 | 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. 18 | 19 | ## [BRPOPLPUSH](https://redis.io/commands/brpoplpush/) 20 | 21 | 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. 22 | 23 | ## [LINDEX](https://redis.io/commands/lindex/) 24 | 25 | Returns an element from a list by its index. 26 | 27 | ## [LINSERT](https://redis.io/commands/linsert/) 28 | 29 | Inserts an element before or after another element in a list. 30 | 31 | ## [LLEN](https://redis.io/commands/llen/) 32 | 33 | Returns the length of a list. 34 | 35 | ## [LMOVE](https://redis.io/commands/lmove/) 36 | 37 | Returns an element after popping it from one list and pushing it to another. Deletes the list if the last element was moved. 38 | 39 | ## [LMPOP](https://redis.io/commands/lmpop/) 40 | 41 | Returns multiple elements from a list after removing them. Deletes the list if the last element was popped. 42 | 43 | ## [LPOP](https://redis.io/commands/lpop/) 44 | 45 | Returns the first elements in a list after removing it. Deletes the list if the last element was popped. 46 | 47 | ## [LPOS](https://redis.io/commands/lpos/) 48 | 49 | Returns the index of matching elements in a list. 50 | 51 | ## [LPUSH](https://redis.io/commands/lpush/) 52 | 53 | Prepends one or more elements to a list. Creates the key if it doesn't exist. 54 | 55 | ## [LPUSHX](https://redis.io/commands/lpushx/) 56 | 57 | Prepends one or more elements to a list only when the list exists. 58 | 59 | ## [LRANGE](https://redis.io/commands/lrange/) 60 | 61 | Returns a range of elements from a list. 62 | 63 | ## [LREM](https://redis.io/commands/lrem/) 64 | 65 | Removes elements from a list. Deletes the list if the last element was removed. 66 | 67 | ## [LSET](https://redis.io/commands/lset/) 68 | 69 | Sets the value of an element in a list by its index. 70 | 71 | ## [LTRIM](https://redis.io/commands/ltrim/) 72 | 73 | Removes elements from both ends a list. Deletes the list if all elements were trimmed. 74 | 75 | ## [RPOP](https://redis.io/commands/rpop/) 76 | 77 | Returns and removes the last elements of a list. Deletes the list if the last element was popped. 78 | 79 | ## [RPOPLPUSH](https://redis.io/commands/rpoplpush/) 80 | 81 | Returns the last element of a list after removing and pushing it to another list. Deletes the list if the last element was popped. 82 | 83 | ## [RPUSH](https://redis.io/commands/rpush/) 84 | 85 | Appends one or more elements to a list. Creates the key if it doesn't exist. 86 | 87 | ## [RPUSHX](https://redis.io/commands/rpushx/) 88 | 89 | Appends an element to a list only when the list exists. 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/PUBSUB.md: -------------------------------------------------------------------------------- 1 | # Redis `pubsub` commands (15/15 implemented) 2 | 3 | ## [PSUBSCRIBE](https://redis.io/commands/psubscribe/) 4 | 5 | Listens for messages published to channels that match one or more patterns. 6 | 7 | ## [PUBLISH](https://redis.io/commands/publish/) 8 | 9 | Posts a message to a channel. 10 | 11 | ## [PUBSUB](https://redis.io/commands/pubsub/) 12 | 13 | A container for Pub/Sub commands. 14 | 15 | ## [PUBSUB CHANNELS](https://redis.io/commands/pubsub-channels/) 16 | 17 | Returns the active channels. 18 | 19 | ## [PUBSUB HELP](https://redis.io/commands/pubsub-help/) 20 | 21 | Returns helpful text about the different subcommands. 22 | 23 | ## [PUBSUB NUMPAT](https://redis.io/commands/pubsub-numpat/) 24 | 25 | Returns a count of unique pattern subscriptions. 26 | 27 | ## [PUBSUB NUMSUB](https://redis.io/commands/pubsub-numsub/) 28 | 29 | Returns a count of subscribers to channels. 30 | 31 | ## [PUBSUB SHARDCHANNELS](https://redis.io/commands/pubsub-shardchannels/) 32 | 33 | Returns the active shard channels. 34 | 35 | ## [PUBSUB SHARDNUMSUB](https://redis.io/commands/pubsub-shardnumsub/) 36 | 37 | Returns the count of subscribers of shard channels. 38 | 39 | ## [PUNSUBSCRIBE](https://redis.io/commands/punsubscribe/) 40 | 41 | Stops listening to messages published to channels that match one or more patterns. 42 | 43 | ## [SPUBLISH](https://redis.io/commands/spublish/) 44 | 45 | Post a message to a shard channel 46 | 47 | ## [SSUBSCRIBE](https://redis.io/commands/ssubscribe/) 48 | 49 | Listens for messages published to shard channels. 50 | 51 | ## [SUBSCRIBE](https://redis.io/commands/subscribe/) 52 | 53 | Listens for messages published to channels. 54 | 55 | ## [SUNSUBSCRIBE](https://redis.io/commands/sunsubscribe/) 56 | 57 | Stops listening to messages posted to shard channels. 58 | 59 | ## [UNSUBSCRIBE](https://redis.io/commands/unsubscribe/) 60 | 61 | Stops listening to messages posted to channels. 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/SCRIPTING.md: -------------------------------------------------------------------------------- 1 | # Redis `scripting` commands (7/22 implemented) 2 | 3 | ## [EVAL](https://redis.io/commands/eval/) 4 | 5 | Executes a server-side Lua script. 6 | 7 | ## [EVALSHA](https://redis.io/commands/evalsha/) 8 | 9 | Executes a server-side Lua script by SHA1 digest. 10 | 11 | ## [SCRIPT](https://redis.io/commands/script/) 12 | 13 | A container for Lua scripts management commands. 14 | 15 | ## [SCRIPT EXISTS](https://redis.io/commands/script-exists/) 16 | 17 | Determines whether server-side Lua scripts exist in the script cache. 18 | 19 | ## [SCRIPT FLUSH](https://redis.io/commands/script-flush/) 20 | 21 | Removes all server-side Lua scripts from the script cache. 22 | 23 | ## [SCRIPT HELP](https://redis.io/commands/script-help/) 24 | 25 | Returns helpful text about the different subcommands. 26 | 27 | ## [SCRIPT LOAD](https://redis.io/commands/script-load/) 28 | 29 | Loads a server-side Lua script to the script cache. 30 | 31 | 32 | ## Unsupported scripting commands 33 | > To implement support for a command, see [here](/guides/implement-command/) 34 | 35 | #### [EVAL_RO](https://redis.io/commands/eval_ro/) (not implemented) 36 | 37 | Executes a read-only server-side Lua script. 38 | 39 | #### [EVALSHA_RO](https://redis.io/commands/evalsha_ro/) (not implemented) 40 | 41 | Executes a read-only server-side Lua script by SHA1 digest. 42 | 43 | #### [FCALL](https://redis.io/commands/fcall/) (not implemented) 44 | 45 | Invokes a function. 46 | 47 | #### [FCALL_RO](https://redis.io/commands/fcall_ro/) (not implemented) 48 | 49 | Invokes a read-only function. 50 | 51 | #### [FUNCTION](https://redis.io/commands/function/) (not implemented) 52 | 53 | A container for function commands. 54 | 55 | #### [FUNCTION DELETE](https://redis.io/commands/function-delete/) (not implemented) 56 | 57 | Deletes a library and its functions. 58 | 59 | #### [FUNCTION DUMP](https://redis.io/commands/function-dump/) (not implemented) 60 | 61 | Dumps all libraries into a serialized binary payload. 62 | 63 | #### [FUNCTION FLUSH](https://redis.io/commands/function-flush/) (not implemented) 64 | 65 | Deletes all libraries and functions. 66 | 67 | #### [FUNCTION KILL](https://redis.io/commands/function-kill/) (not implemented) 68 | 69 | Terminates a function during execution. 70 | 71 | #### [FUNCTION LIST](https://redis.io/commands/function-list/) (not implemented) 72 | 73 | Returns information about all libraries. 74 | 75 | #### [FUNCTION LOAD](https://redis.io/commands/function-load/) (not implemented) 76 | 77 | Creates a library. 78 | 79 | #### [FUNCTION RESTORE](https://redis.io/commands/function-restore/) (not implemented) 80 | 81 | Restores all libraries from a payload. 82 | 83 | #### [FUNCTION STATS](https://redis.io/commands/function-stats/) (not implemented) 84 | 85 | Returns information about a function during execution. 86 | 87 | #### [SCRIPT DEBUG](https://redis.io/commands/script-debug/) (not implemented) 88 | 89 | Sets the debug mode of server-side Lua scripts. 90 | 91 | #### [SCRIPT KILL](https://redis.io/commands/script-kill/) (not implemented) 92 | 93 | Terminates a server-side Lua script during execution. 94 | 95 | 96 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/SET.md: -------------------------------------------------------------------------------- 1 | # Redis `set` commands (17/17 implemented) 2 | 3 | ## [SADD](https://redis.io/commands/sadd/) 4 | 5 | Adds one or more members to a set. Creates the key if it doesn't exist. 6 | 7 | ## [SCARD](https://redis.io/commands/scard/) 8 | 9 | Returns the number of members in a set. 10 | 11 | ## [SDIFF](https://redis.io/commands/sdiff/) 12 | 13 | Returns the difference of multiple sets. 14 | 15 | ## [SDIFFSTORE](https://redis.io/commands/sdiffstore/) 16 | 17 | Stores the difference of multiple sets in a key. 18 | 19 | ## [SINTER](https://redis.io/commands/sinter/) 20 | 21 | Returns the intersect of multiple sets. 22 | 23 | ## [SINTERCARD](https://redis.io/commands/sintercard/) 24 | 25 | Returns the number of members of the intersect of multiple sets. 26 | 27 | ## [SINTERSTORE](https://redis.io/commands/sinterstore/) 28 | 29 | Stores the intersect of multiple sets in a key. 30 | 31 | ## [SISMEMBER](https://redis.io/commands/sismember/) 32 | 33 | Determines whether a member belongs to a set. 34 | 35 | ## [SMEMBERS](https://redis.io/commands/smembers/) 36 | 37 | Returns all members of a set. 38 | 39 | ## [SMISMEMBER](https://redis.io/commands/smismember/) 40 | 41 | Determines whether multiple members belong to a set. 42 | 43 | ## [SMOVE](https://redis.io/commands/smove/) 44 | 45 | Moves a member from one set to another. 46 | 47 | ## [SPOP](https://redis.io/commands/spop/) 48 | 49 | Returns one or more random members from a set after removing them. Deletes the set if the last member was popped. 50 | 51 | ## [SRANDMEMBER](https://redis.io/commands/srandmember/) 52 | 53 | Get one or multiple random members from a set 54 | 55 | ## [SREM](https://redis.io/commands/srem/) 56 | 57 | Removes one or more members from a set. Deletes the set if the last member was removed. 58 | 59 | ## [SSCAN](https://redis.io/commands/sscan/) 60 | 61 | Iterates over members of a set. 62 | 63 | ## [SUNION](https://redis.io/commands/sunion/) 64 | 65 | Returns the union of multiple sets. 66 | 67 | ## [SUNIONSTORE](https://redis.io/commands/sunionstore/) 68 | 69 | Stores the union of multiple sets in a key. 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/STREAM.md: -------------------------------------------------------------------------------- 1 | # Redis `stream` commands (20/20 implemented) 2 | 3 | ## [XACK](https://redis.io/commands/xack/) 4 | 5 | Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. 6 | 7 | ## [XADD](https://redis.io/commands/xadd/) 8 | 9 | Appends a new message to a stream. Creates the key if it doesn't exist. 10 | 11 | ## [XAUTOCLAIM](https://redis.io/commands/xautoclaim/) 12 | 13 | Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to as consumer group member. 14 | 15 | ## [XCLAIM](https://redis.io/commands/xclaim/) 16 | 17 | Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered a consumer group member. 18 | 19 | ## [XDEL](https://redis.io/commands/xdel/) 20 | 21 | Returns the number of messages after removing them from a stream. 22 | 23 | ## [XGROUP CREATE](https://redis.io/commands/xgroup-create/) 24 | 25 | Creates a consumer group. 26 | 27 | ## [XGROUP CREATECONSUMER](https://redis.io/commands/xgroup-createconsumer/) 28 | 29 | Creates a consumer in a consumer group. 30 | 31 | ## [XGROUP DELCONSUMER](https://redis.io/commands/xgroup-delconsumer/) 32 | 33 | Deletes a consumer from a consumer group. 34 | 35 | ## [XGROUP DESTROY](https://redis.io/commands/xgroup-destroy/) 36 | 37 | Destroys a consumer group. 38 | 39 | ## [XGROUP SETID](https://redis.io/commands/xgroup-setid/) 40 | 41 | Sets the last-delivered ID of a consumer group. 42 | 43 | ## [XINFO CONSUMERS](https://redis.io/commands/xinfo-consumers/) 44 | 45 | Returns a list of the consumers in a consumer group. 46 | 47 | ## [XINFO GROUPS](https://redis.io/commands/xinfo-groups/) 48 | 49 | Returns a list of the consumer groups of a stream. 50 | 51 | ## [XINFO STREAM](https://redis.io/commands/xinfo-stream/) 52 | 53 | Returns information about a stream. 54 | 55 | ## [XLEN](https://redis.io/commands/xlen/) 56 | 57 | Return the number of messages in a stream. 58 | 59 | ## [XPENDING](https://redis.io/commands/xpending/) 60 | 61 | Returns the information and entries from a stream consumer group's pending entries list. 62 | 63 | ## [XRANGE](https://redis.io/commands/xrange/) 64 | 65 | Returns the messages from a stream within a range of IDs. 66 | 67 | ## [XREAD](https://redis.io/commands/xread/) 68 | 69 | Returns messages from multiple streams with IDs greater than the ones requested. Blocks until a message is available otherwise. 70 | 71 | ## [XREADGROUP](https://redis.io/commands/xreadgroup/) 72 | 73 | Returns new or historical messages from a stream for a consumer in a group. Blocks until a message is available otherwise. 74 | 75 | ## [XREVRANGE](https://redis.io/commands/xrevrange/) 76 | 77 | Returns the messages from a stream within a range of IDs in reverse order. 78 | 79 | ## [XTRIM](https://redis.io/commands/xtrim/) 80 | 81 | Deletes messages from the beginning of a stream. 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/STRING.md: -------------------------------------------------------------------------------- 1 | # Redis `string` commands (22/22 implemented) 2 | 3 | ## [APPEND](https://redis.io/commands/append/) 4 | 5 | Appends a string to the value of a key. Creates the key if it doesn't exist. 6 | 7 | ## [DECR](https://redis.io/commands/decr/) 8 | 9 | Decrements the integer value of a key by one. Uses 0 as initial value if the key doesn't exist. 10 | 11 | ## [DECRBY](https://redis.io/commands/decrby/) 12 | 13 | Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist. 14 | 15 | ## [GET](https://redis.io/commands/get/) 16 | 17 | Returns the string value of a key. 18 | 19 | ## [GETDEL](https://redis.io/commands/getdel/) 20 | 21 | Returns the string value of a key after deleting the key. 22 | 23 | ## [GETEX](https://redis.io/commands/getex/) 24 | 25 | Returns the string value of a key after setting its expiration time. 26 | 27 | ## [GETRANGE](https://redis.io/commands/getrange/) 28 | 29 | Returns a substring of the string stored at a key. 30 | 31 | ## [GETSET](https://redis.io/commands/getset/) 32 | 33 | Returns the previous string value of a key after setting it to a new value. 34 | 35 | ## [INCR](https://redis.io/commands/incr/) 36 | 37 | Increments the integer value of a key by one. Uses 0 as initial value if the key doesn't exist. 38 | 39 | ## [INCRBY](https://redis.io/commands/incrby/) 40 | 41 | Increments the integer value of a key by a number. Uses 0 as initial value if the key doesn't exist. 42 | 43 | ## [INCRBYFLOAT](https://redis.io/commands/incrbyfloat/) 44 | 45 | Increment the floating point value of a key by a number. Uses 0 as initial value if the key doesn't exist. 46 | 47 | ## [LCS](https://redis.io/commands/lcs/) 48 | 49 | Finds the longest common substring. 50 | 51 | ## [MGET](https://redis.io/commands/mget/) 52 | 53 | Atomically returns the string values of one or more keys. 54 | 55 | ## [MSET](https://redis.io/commands/mset/) 56 | 57 | Atomically creates or modifies the string values of one or more keys. 58 | 59 | ## [MSETNX](https://redis.io/commands/msetnx/) 60 | 61 | Atomically modifies the string values of one or more keys only when all keys don't exist. 62 | 63 | ## [PSETEX](https://redis.io/commands/psetex/) 64 | 65 | Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn't exist. 66 | 67 | ## [SET](https://redis.io/commands/set/) 68 | 69 | Sets the string value of a key, ignoring its type. The key is created if it doesn't exist. 70 | 71 | ## [SETEX](https://redis.io/commands/setex/) 72 | 73 | Sets the string value and expiration time of a key. Creates the key if it doesn't exist. 74 | 75 | ## [SETNX](https://redis.io/commands/setnx/) 76 | 77 | Set the string value of a key only when the key doesn't exist. 78 | 79 | ## [SETRANGE](https://redis.io/commands/setrange/) 80 | 81 | Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist. 82 | 83 | ## [STRLEN](https://redis.io/commands/strlen/) 84 | 85 | Returns the length of a string value. 86 | 87 | ## [SUBSTR](https://redis.io/commands/substr/) 88 | 89 | Returns a substring from a string value. 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/supported-commands/Redis/TRANSACTIONS.md: -------------------------------------------------------------------------------- 1 | # Redis `transactions` commands (5/5 implemented) 2 | 3 | ## [DISCARD](https://redis.io/commands/discard/) 4 | 5 | Discards a transaction. 6 | 7 | ## [EXEC](https://redis.io/commands/exec/) 8 | 9 | Executes all commands in a transaction. 10 | 11 | ## [MULTI](https://redis.io/commands/multi/) 12 | 13 | Starts a transaction. 14 | 15 | ## [UNWATCH](https://redis.io/commands/unwatch/) 16 | 17 | Forgets about watched keys of a transaction. 18 | 19 | ## [WATCH](https://redis.io/commands/watch/) 20 | 21 | Monitors changes to keys to determine the execution of a transaction. 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisBloom/BF.md: -------------------------------------------------------------------------------- 1 | # RedisBloom `bf` commands (10/10 implemented) 2 | 3 | ## [BF.ADD](https://redis.io/commands/bf.add/) 4 | 5 | Adds an item to a Bloom Filter 6 | 7 | ## [BF.CARD](https://redis.io/commands/bf.card/) 8 | 9 | Returns the cardinality of a Bloom filter 10 | 11 | ## [BF.EXISTS](https://redis.io/commands/bf.exists/) 12 | 13 | Checks whether an item exists in a Bloom Filter 14 | 15 | ## [BF.INFO](https://redis.io/commands/bf.info/) 16 | 17 | Returns information about a Bloom Filter 18 | 19 | ## [BF.INSERT](https://redis.io/commands/bf.insert/) 20 | 21 | Adds one or more items to a Bloom Filter. A filter will be created if it does not exist 22 | 23 | ## [BF.LOADCHUNK](https://redis.io/commands/bf.loadchunk/) 24 | 25 | Restores a filter previously saved using SCANDUMP 26 | 27 | ## [BF.MADD](https://redis.io/commands/bf.madd/) 28 | 29 | Adds one or more items to a Bloom Filter. A filter will be created if it does not exist 30 | 31 | ## [BF.MEXISTS](https://redis.io/commands/bf.mexists/) 32 | 33 | Checks whether one or more items exist in a Bloom Filter 34 | 35 | ## [BF.RESERVE](https://redis.io/commands/bf.reserve/) 36 | 37 | Creates a new Bloom Filter 38 | 39 | ## [BF.SCANDUMP](https://redis.io/commands/bf.scandump/) 40 | 41 | Begins an incremental save of the bloom filter 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisBloom/CF.md: -------------------------------------------------------------------------------- 1 | # RedisBloom `cf` commands (12/12 implemented) 2 | 3 | ## [CF.ADD](https://redis.io/commands/cf.add/) 4 | 5 | Adds an item to a Cuckoo Filter 6 | 7 | ## [CF.ADDNX](https://redis.io/commands/cf.addnx/) 8 | 9 | Adds an item to a Cuckoo Filter if the item did not exist previously. 10 | 11 | ## [CF.COUNT](https://redis.io/commands/cf.count/) 12 | 13 | Return the number of times an item might be in a Cuckoo Filter 14 | 15 | ## [CF.DEL](https://redis.io/commands/cf.del/) 16 | 17 | Deletes an item from a Cuckoo Filter 18 | 19 | ## [CF.EXISTS](https://redis.io/commands/cf.exists/) 20 | 21 | Checks whether one or more items exist in a Cuckoo Filter 22 | 23 | ## [CF.INFO](https://redis.io/commands/cf.info/) 24 | 25 | Returns information about a Cuckoo Filter 26 | 27 | ## [CF.INSERT](https://redis.io/commands/cf.insert/) 28 | 29 | Adds one or more items to a Cuckoo Filter. A filter will be created if it does not exist 30 | 31 | ## [CF.INSERTNX](https://redis.io/commands/cf.insertnx/) 32 | 33 | 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 34 | 35 | ## [CF.LOADCHUNK](https://redis.io/commands/cf.loadchunk/) 36 | 37 | Restores a filter previously saved using SCANDUMP 38 | 39 | ## [CF.MEXISTS](https://redis.io/commands/cf.mexists/) 40 | 41 | Checks whether one or more items exist in a Cuckoo Filter 42 | 43 | ## [CF.RESERVE](https://redis.io/commands/cf.reserve/) 44 | 45 | Creates a new Cuckoo Filter 46 | 47 | ## [CF.SCANDUMP](https://redis.io/commands/cf.scandump/) 48 | 49 | Begins an incremental save of the bloom filter 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisBloom/CMS.md: -------------------------------------------------------------------------------- 1 | # RedisBloom `cms` commands (6/6 implemented) 2 | 3 | ## [CMS.INCRBY](https://redis.io/commands/cms.incrby/) 4 | 5 | Increases the count of one or more items by increment 6 | 7 | ## [CMS.INFO](https://redis.io/commands/cms.info/) 8 | 9 | Returns information about a sketch 10 | 11 | ## [CMS.INITBYDIM](https://redis.io/commands/cms.initbydim/) 12 | 13 | Initializes a Count-Min Sketch to dimensions specified by user 14 | 15 | ## [CMS.INITBYPROB](https://redis.io/commands/cms.initbyprob/) 16 | 17 | Initializes a Count-Min Sketch to accommodate requested tolerances. 18 | 19 | ## [CMS.MERGE](https://redis.io/commands/cms.merge/) 20 | 21 | Merges several sketches into one sketch 22 | 23 | ## [CMS.QUERY](https://redis.io/commands/cms.query/) 24 | 25 | Returns the count for one or more items in a sketch 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisBloom/TDIGEST.md: -------------------------------------------------------------------------------- 1 | # RedisBloom `tdigest` commands (14/14 implemented) 2 | 3 | ## [TDIGEST.ADD](https://redis.io/commands/tdigest.add/) 4 | 5 | Adds one or more observations to a t-digest sketch 6 | 7 | ## [TDIGEST.BYRANK](https://redis.io/commands/tdigest.byrank/) 8 | 9 | Returns, for each input rank, an estimation of the value (floating-point) with that rank 10 | 11 | ## [TDIGEST.BYREVRANK](https://redis.io/commands/tdigest.byrevrank/) 12 | 13 | Returns, for each input reverse rank, an estimation of the value (floating-point) with that reverse rank 14 | 15 | ## [TDIGEST.CDF](https://redis.io/commands/tdigest.cdf/) 16 | 17 | 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) 18 | 19 | ## [TDIGEST.CREATE](https://redis.io/commands/tdigest.create/) 20 | 21 | Allocates memory and initializes a new t-digest sketch 22 | 23 | ## [TDIGEST.INFO](https://redis.io/commands/tdigest.info/) 24 | 25 | Returns information and statistics about a t-digest sketch 26 | 27 | ## [TDIGEST.MAX](https://redis.io/commands/tdigest.max/) 28 | 29 | Returns the maximum observation value from a t-digest sketch 30 | 31 | ## [TDIGEST.MERGE](https://redis.io/commands/tdigest.merge/) 32 | 33 | Merges multiple t-digest sketches into a single sketch 34 | 35 | ## [TDIGEST.MIN](https://redis.io/commands/tdigest.min/) 36 | 37 | Returns the minimum observation value from a t-digest sketch 38 | 39 | ## [TDIGEST.QUANTILE](https://redis.io/commands/tdigest.quantile/) 40 | 41 | Returns, for each input fraction, an estimation of the value (floating point) that is smaller than the given fraction of observations 42 | 43 | ## [TDIGEST.RANK](https://redis.io/commands/tdigest.rank/) 44 | 45 | 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) 46 | 47 | ## [TDIGEST.RESET](https://redis.io/commands/tdigest.reset/) 48 | 49 | Resets a t-digest sketch: empty the sketch and re-initializes it. 50 | 51 | ## [TDIGEST.REVRANK](https://redis.io/commands/tdigest.revrank/) 52 | 53 | 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) 54 | 55 | ## [TDIGEST.TRIMMED_MEAN](https://redis.io/commands/tdigest.trimmed_mean/) 56 | 57 | Returns an estimation of the mean value from the sketch, excluding observation values outside the low and high cutoff quantiles 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisBloom/TOPK.md: -------------------------------------------------------------------------------- 1 | # RedisBloom `topk` commands (7/7 implemented) 2 | 3 | ## [TOPK.ADD](https://redis.io/commands/topk.add/) 4 | 5 | Increases the count of one or more items by increment 6 | 7 | ## [TOPK.COUNT](https://redis.io/commands/topk.count/) 8 | 9 | Return the count for one or more items are in a sketch 10 | 11 | ## [TOPK.INCRBY](https://redis.io/commands/topk.incrby/) 12 | 13 | Increases the count of one or more items by increment 14 | 15 | ## [TOPK.INFO](https://redis.io/commands/topk.info/) 16 | 17 | Returns information about a sketch 18 | 19 | ## [TOPK.LIST](https://redis.io/commands/topk.list/) 20 | 21 | Return full list of items in Top K list 22 | 23 | ## [TOPK.QUERY](https://redis.io/commands/topk.query/) 24 | 25 | Checks whether one or more items are in a sketch 26 | 27 | ## [TOPK.RESERVE](https://redis.io/commands/topk.reserve/) 28 | 29 | Initializes a TopK with specified parameters 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisJson/JSON.md: -------------------------------------------------------------------------------- 1 | # RedisJson `json` commands (22/22 implemented) 2 | 3 | ## [JSON.ARRAPPEND](https://redis.io/commands/json.arrappend/) 4 | 5 | Append one or more json values into the array at path after the last element in it. 6 | 7 | ## [JSON.ARRINDEX](https://redis.io/commands/json.arrindex/) 8 | 9 | Returns the index of the first occurrence of a JSON scalar value in the array at path 10 | 11 | ## [JSON.ARRINSERT](https://redis.io/commands/json.arrinsert/) 12 | 13 | Inserts the JSON scalar(s) value at the specified index in the array at path 14 | 15 | ## [JSON.ARRLEN](https://redis.io/commands/json.arrlen/) 16 | 17 | Returns the length of the array at path 18 | 19 | ## [JSON.ARRPOP](https://redis.io/commands/json.arrpop/) 20 | 21 | Removes and returns the element at the specified index in the array at path 22 | 23 | ## [JSON.ARRTRIM](https://redis.io/commands/json.arrtrim/) 24 | 25 | Trims the array at path to contain only the specified inclusive range of indices from start to stop 26 | 27 | ## [JSON.CLEAR](https://redis.io/commands/json.clear/) 28 | 29 | Clears all values from an array or an object and sets numeric values to `0` 30 | 31 | ## [JSON.DEL](https://redis.io/commands/json.del/) 32 | 33 | Deletes a value 34 | 35 | ## [JSON.FORGET](https://redis.io/commands/json.forget/) 36 | 37 | Deletes a value 38 | 39 | ## [JSON.GET](https://redis.io/commands/json.get/) 40 | 41 | Gets the value at one or more paths in JSON serialized form 42 | 43 | ## [JSON.MERGE](https://redis.io/commands/json.merge/) 44 | 45 | Merges a given JSON value into matching paths. Consequently, JSON values at matching paths are updated, deleted, or expanded with new children 46 | 47 | ## [JSON.MGET](https://redis.io/commands/json.mget/) 48 | 49 | Returns the values at a path from one or more keys 50 | 51 | ## [JSON.MSET](https://redis.io/commands/json.mset/) 52 | 53 | Sets or updates the JSON value of one or more keys 54 | 55 | ## [JSON.NUMINCRBY](https://redis.io/commands/json.numincrby/) 56 | 57 | Increments the numeric value at path by a value 58 | 59 | ## [JSON.NUMMULTBY](https://redis.io/commands/json.nummultby/) 60 | 61 | Multiplies the numeric value at path by a value 62 | 63 | ## [JSON.OBJKEYS](https://redis.io/commands/json.objkeys/) 64 | 65 | Returns the JSON keys of the object at path 66 | 67 | ## [JSON.OBJLEN](https://redis.io/commands/json.objlen/) 68 | 69 | Returns the number of keys of the object at path 70 | 71 | ## [JSON.SET](https://redis.io/commands/json.set/) 72 | 73 | Sets or updates the JSON value at a path 74 | 75 | ## [JSON.STRAPPEND](https://redis.io/commands/json.strappend/) 76 | 77 | Appends a string to a JSON string value at path 78 | 79 | ## [JSON.STRLEN](https://redis.io/commands/json.strlen/) 80 | 81 | Returns the length of the JSON String at path in key 82 | 83 | ## [JSON.TOGGLE](https://redis.io/commands/json.toggle/) 84 | 85 | Toggles a boolean value 86 | 87 | ## [JSON.TYPE](https://redis.io/commands/json.type/) 88 | 89 | Returns the type of the JSON value at path 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisSearch/SEARCH.md: -------------------------------------------------------------------------------- 1 | 2 | ## Unsupported search commands 3 | > To implement support for a command, see [here](/guides/implement-command/) 4 | 5 | #### [FT._LIST](https://redis.io/commands/ft._list/) (not implemented) 6 | 7 | Returns a list of all existing indexes 8 | 9 | #### [FT.AGGREGATE](https://redis.io/commands/ft.aggregate/) (not implemented) 10 | 11 | Run a search query on an index and perform aggregate transformations on the results 12 | 13 | #### [FT.ALIASADD](https://redis.io/commands/ft.aliasadd/) (not implemented) 14 | 15 | Adds an alias to the index 16 | 17 | #### [FT.ALIASDEL](https://redis.io/commands/ft.aliasdel/) (not implemented) 18 | 19 | Deletes an alias from the index 20 | 21 | #### [FT.ALIASUPDATE](https://redis.io/commands/ft.aliasupdate/) (not implemented) 22 | 23 | Adds or updates an alias to the index 24 | 25 | #### [FT.ALTER](https://redis.io/commands/ft.alter/) (not implemented) 26 | 27 | Adds a new field to the index 28 | 29 | #### [FT.CONFIG GET](https://redis.io/commands/ft.config-get/) (not implemented) 30 | 31 | Retrieves runtime configuration options 32 | 33 | #### [FT.CONFIG HELP](https://redis.io/commands/ft.config-help/) (not implemented) 34 | 35 | Help description of runtime configuration options 36 | 37 | #### [FT.CONFIG SET](https://redis.io/commands/ft.config-set/) (not implemented) 38 | 39 | Sets runtime configuration options 40 | 41 | #### [FT.CREATE](https://redis.io/commands/ft.create/) (not implemented) 42 | 43 | Creates an index with the given spec 44 | 45 | #### [FT.CURSOR DEL](https://redis.io/commands/ft.cursor-del/) (not implemented) 46 | 47 | Deletes a cursor 48 | 49 | #### [FT.CURSOR READ](https://redis.io/commands/ft.cursor-read/) (not implemented) 50 | 51 | Reads from a cursor 52 | 53 | #### [FT.DICTADD](https://redis.io/commands/ft.dictadd/) (not implemented) 54 | 55 | Adds terms to a dictionary 56 | 57 | #### [FT.DICTDEL](https://redis.io/commands/ft.dictdel/) (not implemented) 58 | 59 | Deletes terms from a dictionary 60 | 61 | #### [FT.DICTDUMP](https://redis.io/commands/ft.dictdump/) (not implemented) 62 | 63 | Dumps all terms in the given dictionary 64 | 65 | #### [FT.DROPINDEX](https://redis.io/commands/ft.dropindex/) (not implemented) 66 | 67 | Deletes the index 68 | 69 | #### [FT.EXPLAIN](https://redis.io/commands/ft.explain/) (not implemented) 70 | 71 | Returns the execution plan for a complex query 72 | 73 | #### [FT.EXPLAINCLI](https://redis.io/commands/ft.explaincli/) (not implemented) 74 | 75 | Returns the execution plan for a complex query 76 | 77 | #### [FT.INFO](https://redis.io/commands/ft.info/) (not implemented) 78 | 79 | Returns information and statistics on the index 80 | 81 | #### [FT.PROFILE](https://redis.io/commands/ft.profile/) (not implemented) 82 | 83 | Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information 84 | 85 | #### [FT.SEARCH](https://redis.io/commands/ft.search/) (not implemented) 86 | 87 | Searches the index with a textual query, returning either documents or just ids 88 | 89 | #### [FT.SPELLCHECK](https://redis.io/commands/ft.spellcheck/) (not implemented) 90 | 91 | Performs spelling correction on a query, returning suggestions for misspelled terms 92 | 93 | #### [FT.SYNDUMP](https://redis.io/commands/ft.syndump/) (not implemented) 94 | 95 | Dumps the contents of a synonym group 96 | 97 | #### [FT.SYNUPDATE](https://redis.io/commands/ft.synupdate/) (not implemented) 98 | 99 | Creates or updates a synonym group with additional terms 100 | 101 | #### [FT.TAGVALS](https://redis.io/commands/ft.tagvals/) (not implemented) 102 | 103 | Returns the distinct tags indexed in a Tag field 104 | 105 | 106 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisSearch/SUGGESTION.md: -------------------------------------------------------------------------------- 1 | 2 | ## Unsupported suggestion commands 3 | > To implement support for a command, see [here](/guides/implement-command/) 4 | 5 | #### [FT.SUGADD](https://redis.io/commands/ft.sugadd/) (not implemented) 6 | 7 | Adds a suggestion string to an auto-complete suggestion dictionary 8 | 9 | #### [FT.SUGDEL](https://redis.io/commands/ft.sugdel/) (not implemented) 10 | 11 | Deletes a string from a suggestion index 12 | 13 | #### [FT.SUGGET](https://redis.io/commands/ft.sugget/) (not implemented) 14 | 15 | Gets completion suggestions for a prefix 16 | 17 | #### [FT.SUGLEN](https://redis.io/commands/ft.suglen/) (not implemented) 18 | 19 | Gets the size of an auto-complete suggestion dictionary 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/supported-commands/RedisTimeSeries/TIMESERIES.md: -------------------------------------------------------------------------------- 1 | # RedisTimeSeries `timeseries` commands (17/17 implemented) 2 | 3 | ## [TS.ADD](https://redis.io/commands/ts.add/) 4 | 5 | Append a sample to a time series 6 | 7 | ## [TS.ALTER](https://redis.io/commands/ts.alter/) 8 | 9 | Update the retention, chunk size, duplicate policy, and labels of an existing time series 10 | 11 | ## [TS.CREATE](https://redis.io/commands/ts.create/) 12 | 13 | Create a new time series 14 | 15 | ## [TS.CREATERULE](https://redis.io/commands/ts.createrule/) 16 | 17 | Create a compaction rule 18 | 19 | ## [TS.DECRBY](https://redis.io/commands/ts.decrby/) 20 | 21 | 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 22 | 23 | ## [TS.DEL](https://redis.io/commands/ts.del/) 24 | 25 | Delete all samples between two timestamps for a given time series 26 | 27 | ## [TS.DELETERULE](https://redis.io/commands/ts.deleterule/) 28 | 29 | Delete a compaction rule 30 | 31 | ## [TS.GET](https://redis.io/commands/ts.get/) 32 | 33 | Get the sample with the highest timestamp from a given time series 34 | 35 | ## [TS.INCRBY](https://redis.io/commands/ts.incrby/) 36 | 37 | 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 38 | 39 | ## [TS.INFO](https://redis.io/commands/ts.info/) 40 | 41 | Returns information and statistics for a time series 42 | 43 | ## [TS.MADD](https://redis.io/commands/ts.madd/) 44 | 45 | Append new samples to one or more time series 46 | 47 | ## [TS.MGET](https://redis.io/commands/ts.mget/) 48 | 49 | Get the sample with the highest timestamp from each time series matching a specific filter 50 | 51 | ## [TS.MRANGE](https://redis.io/commands/ts.mrange/) 52 | 53 | Query a range across multiple time series by filters in forward direction 54 | 55 | ## [TS.MREVRANGE](https://redis.io/commands/ts.mrevrange/) 56 | 57 | Query a range across multiple time-series by filters in reverse direction 58 | 59 | ## [TS.QUERYINDEX](https://redis.io/commands/ts.queryindex/) 60 | 61 | Get all time series keys matching a filter list 62 | 63 | ## [TS.RANGE](https://redis.io/commands/ts.range/) 64 | 65 | Query a range in forward direction 66 | 67 | ## [TS.REVRANGE](https://redis.io/commands/ts.revrange/) 68 | 69 | Query a range in reverse direction 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/supported-commands/index.md: -------------------------------------------------------------------------------- 1 | # Supported commands 2 | 3 | Commands from [Redis][1], [RedisJSON][2], [RedisTimeSeries][3], and [RedisBloom][4] are supported. 4 | 5 | Additionally, [Dragonfly specific commands][dragonfly] are also supported. 6 | 7 | [1]: /supported-commands/Redis/BITMAP/ 8 | [2]: /supported-commands/RedisJSON/JSON/ 9 | [3]: /supported-commands/RedisTimeSeries/TIMESERIES/ 10 | [4]: /supported-commands/RedisBloom/BF/ 11 | [dragonfly]: /supported-commands/DRAGONFLY/ 12 | -------------------------------------------------------------------------------- /docs/valkey-support.md: -------------------------------------------------------------------------------- 1 | # Support for valkey 2 | 3 | [Valkey][1] is an open source (BSD) high-performance key/value datastore that supports a variety of workloads such as 4 | caching, message queues, and can act as a primary database. 5 | The project was forked from the open source Redis project right before the transition to their new source available 6 | licenses. 7 | 8 | FakeRedis can be used as a valkey replacement for testing and development purposes as well. 9 | 10 | To make the process more straightforward, the `FakeValkey` class sets all relevant arguments in `FakeRedis` to the 11 | valkey values. 12 | 13 | ```python 14 | from fakeredis import FakeValkey 15 | 16 | valkey = FakeValkey() 17 | valkey.set("key", "value") 18 | print(valkey.get("key")) 19 | ``` 20 | 21 | Alternatively, you can start a thread with a Fake Valkey server. 22 | 23 | ```python 24 | from threading import Thread 25 | from fakeredis import TcpFakeServer 26 | 27 | server_address = ("127.0.0.1", 6379) 28 | server = TcpFakeServer(server_address, server_type="valkey") 29 | t = Thread(target=server.serve_forever, daemon=True) 30 | t.start() 31 | 32 | import valkey 33 | 34 | r = valkey.Valkey(host=server_address[0], port=server_address[1]) 35 | r.set("foo", "bar") 36 | assert r.get("foo") == b"bar" 37 | 38 | ``` 39 | 40 | [1]: https://github.com/valkey-io/valkey 41 | -------------------------------------------------------------------------------- /fakeredis/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from ._connection import ( 4 | FakeRedis, 5 | FakeStrictRedis, 6 | FakeConnection, 7 | ) 8 | from ._server import FakeServer 9 | from ._valkey import FakeValkey, FakeAsyncValkey, FakeStrictValkey 10 | from .aioredis import ( 11 | FakeRedis as FakeAsyncRedis, 12 | FakeConnection as FakeAsyncConnection, 13 | ) 14 | 15 | if sys.version_info >= (3, 11): 16 | from ._tcp_server import TcpFakeServer 17 | else: 18 | 19 | class TcpFakeServer: 20 | def __init__(self, *args, **kwargs): 21 | raise NotImplementedError("TcpFakeServer is only available in Python 3.11+") 22 | 23 | 24 | try: 25 | from importlib import metadata 26 | except ImportError: # for Python < 3.8 27 | import importlib_metadata as metadata # type: ignore 28 | __version__ = metadata.version("fakeredis") 29 | __author__ = "Daniel Moran" 30 | __maintainer__ = "Daniel Moran" 31 | __email__ = "daniel@moransoftware.ca" 32 | __license__ = "BSD-3-Clause" 33 | __url__ = "https://github.com/cunla/fakeredis-py" 34 | __bugtrack_url__ = "https://github.com/cunla/fakeredis-py/issues" 35 | 36 | __all__ = [ 37 | "FakeServer", 38 | "FakeRedis", 39 | "FakeStrictRedis", 40 | "FakeConnection", 41 | "FakeAsyncRedis", 42 | "FakeAsyncConnection", 43 | "TcpFakeServer", 44 | "FakeValkey", 45 | "FakeAsyncValkey", 46 | "FakeStrictValkey", 47 | ] 48 | -------------------------------------------------------------------------------- /fakeredis/_fakesocket.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Set, Any 2 | 3 | from fakeredis.commands_mixins import ( 4 | BitmapCommandsMixin, 5 | ConnectionCommandsMixin, 6 | GenericCommandsMixin, 7 | GeoCommandsMixin, 8 | HashCommandsMixin, 9 | ListCommandsMixin, 10 | PubSubCommandsMixin, 11 | ScriptingCommandsMixin, 12 | ServerCommandsMixin, 13 | StringCommandsMixin, 14 | TransactionsCommandsMixin, 15 | SetCommandsMixin, 16 | StreamsCommandsMixin, 17 | AclCommandsMixin, 18 | ) 19 | from fakeredis.stack import ( 20 | JSONCommandsMixin, 21 | BFCommandsMixin, 22 | CFCommandsMixin, 23 | CMSCommandsMixin, 24 | TopkCommandsMixin, 25 | TDigestCommandsMixin, 26 | TimeSeriesCommandsMixin, 27 | ) 28 | from ._basefakesocket import BaseFakeSocket 29 | from ._server import FakeServer 30 | from .commands_mixins.sortedset_mixin import SortedSetCommandsMixin 31 | from .server_specific_commands import DragonflyCommandsMixin 32 | 33 | 34 | class FakeSocket( 35 | BaseFakeSocket, 36 | GenericCommandsMixin, 37 | ScriptingCommandsMixin, 38 | HashCommandsMixin, 39 | ConnectionCommandsMixin, 40 | ListCommandsMixin, 41 | ServerCommandsMixin, 42 | StringCommandsMixin, 43 | TransactionsCommandsMixin, 44 | PubSubCommandsMixin, 45 | SetCommandsMixin, 46 | BitmapCommandsMixin, 47 | SortedSetCommandsMixin, 48 | StreamsCommandsMixin, 49 | JSONCommandsMixin, 50 | GeoCommandsMixin, 51 | BFCommandsMixin, 52 | CFCommandsMixin, 53 | CMSCommandsMixin, 54 | TopkCommandsMixin, 55 | TDigestCommandsMixin, 56 | TimeSeriesCommandsMixin, 57 | DragonflyCommandsMixin, 58 | AclCommandsMixin, 59 | ): 60 | def __init__( 61 | self, 62 | server: "FakeServer", 63 | db: int, 64 | lua_modules: Optional[Set[str]] = None, # noqa: F821 65 | *args: Any, 66 | **kwargs: Any, 67 | ) -> None: 68 | super(FakeSocket, self).__init__(server, db, *args, lua_modules=lua_modules, **kwargs) 69 | -------------------------------------------------------------------------------- /fakeredis/_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import time 4 | import weakref 5 | from collections import defaultdict 6 | from typing import Dict, Tuple, Any, List, Optional, Union 7 | 8 | try: 9 | from typing import Literal 10 | except ImportError: 11 | from typing_extensions import Literal 12 | 13 | from fakeredis.model import AccessControlList 14 | from fakeredis._helpers import Database, FakeSelector 15 | 16 | LOGGER = logging.getLogger("fakeredis") 17 | 18 | VersionType = Union[Tuple[int, ...], int, str] 19 | 20 | ServerType = Literal["redis", "dragonfly", "valkey"] 21 | 22 | 23 | def _create_version(v: VersionType) -> Tuple[int, ...]: 24 | if isinstance(v, tuple): 25 | return v 26 | if isinstance(v, int): 27 | return (v,) 28 | if isinstance(v, str): 29 | v_split = v.split(".") 30 | return tuple(int(x) for x in v_split) 31 | return v 32 | 33 | 34 | def _version_to_str(v: VersionType) -> str: 35 | if isinstance(v, tuple): 36 | return ".".join(str(x) for x in v) 37 | return str(v) 38 | 39 | 40 | class FakeServer: 41 | _servers_map: Dict[str, "FakeServer"] = dict() 42 | 43 | def __init__( 44 | self, 45 | version: VersionType = (7,), 46 | server_type: ServerType = "redis", 47 | config: Optional[Dict[bytes, bytes]] = None, 48 | ) -> None: 49 | """Initialize a new FakeServer instance. 50 | :param version: The version of the server (e.g. 6, 7.4, "7.4.1", can also be a tuple) 51 | :param server_type: The type of server (redis, dragonfly, valkey) 52 | :param config: A dictionary of configuration options. 53 | 54 | Configuration options: 55 | - `requirepass`: The password required to authenticate to the server. 56 | - `aclfile`: The path to the ACL file. 57 | """ 58 | self.lock = threading.Lock() 59 | self.dbs: Dict[int, Database] = defaultdict(lambda: Database(self.lock)) 60 | # Maps channel/pattern to a weak set of sockets 61 | self.subscribers: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet) 62 | self.psubscribers: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet) 63 | self.ssubscribers: Dict[bytes, weakref.WeakSet[Any]] = defaultdict(weakref.WeakSet) 64 | self.lastsave: int = int(time.time()) 65 | self.connected = True 66 | # List of weakrefs to sockets that are being closed lazily 67 | self.sockets: List[Any] = [] 68 | self.closed_sockets: List[Any] = [] 69 | self.version: Tuple[int, ...] = _create_version(version) 70 | if server_type not in ("redis", "dragonfly", "valkey"): 71 | raise ValueError(f"Unsupported server type: {server_type}") 72 | self.server_type: str = server_type 73 | self.config: Dict[bytes, bytes] = config or dict() 74 | self.acl: AccessControlList = AccessControlList() 75 | 76 | @staticmethod 77 | def get_server(key: str, version: VersionType, server_type: ServerType) -> "FakeServer": 78 | if key not in FakeServer._servers_map: 79 | FakeServer._servers_map[key] = FakeServer(version=version, server_type=server_type) 80 | return FakeServer._servers_map[key] 81 | 82 | 83 | class FakeBaseConnectionMixin(object): 84 | def __init__( 85 | self, *args: Any, version: VersionType = (7, 0), server_type: ServerType = "redis", **kwargs: Any 86 | ) -> None: 87 | self.client_name: Optional[str] = None 88 | self.server_key: str 89 | self._sock = None 90 | self._selector: Optional[FakeSelector] = None 91 | self._server = kwargs.pop("server", None) 92 | self._lua_modules = kwargs.pop("lua_modules", set()) 93 | path = kwargs.pop("path", None) 94 | connected = kwargs.pop("connected", True) 95 | if self._server is None: 96 | if path: 97 | self.server_key = path 98 | else: 99 | host, port = kwargs.get("host"), kwargs.get("port") 100 | self.server_key = f"{host}:{port}" 101 | self.server_key += f":{server_type}:v{_version_to_str(version)[0]}" 102 | self._server = FakeServer.get_server(self.server_key, server_type=server_type, version=version) 103 | self._server.connected = connected 104 | super().__init__(*args, **kwargs) 105 | -------------------------------------------------------------------------------- /fakeredis/_tcp_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dataclasses import dataclass 3 | from itertools import count 4 | from socketserver import ThreadingTCPServer, StreamRequestHandler 5 | from typing import BinaryIO, Dict, Tuple, Any 6 | 7 | from fakeredis import FakeRedis 8 | from fakeredis import FakeServer 9 | from fakeredis._server import ServerType 10 | 11 | LOGGER = logging.getLogger("fakeredis") 12 | LOGGER.setLevel(logging.DEBUG) 13 | 14 | 15 | def to_bytes(value) -> bytes: 16 | if isinstance(value, bytes): 17 | return value 18 | return str(value).encode() 19 | 20 | 21 | @dataclass 22 | class Client: 23 | connection: FakeRedis 24 | client_address: int 25 | 26 | 27 | @dataclass 28 | class Reader: 29 | reader: BinaryIO 30 | 31 | def load(self) -> Any: 32 | line = self.reader.readline().strip() 33 | match line[0:1], line[1:]: 34 | case b"*", length: 35 | length = int(length) 36 | array = [None] * length 37 | for i in range(length): 38 | array[i] = self.load() 39 | return array 40 | case b"$", length: 41 | bulk_string = self.reader.read(int(length) + 2).strip() 42 | if len(bulk_string) != int(length): 43 | raise ValueError() 44 | return bulk_string 45 | case b":", value: 46 | return int(value) 47 | case b"+", value: 48 | return value 49 | case b"-", value: 50 | return Exception(value) 51 | case _: 52 | return None 53 | 54 | 55 | @dataclass 56 | class Writer: 57 | writer: BinaryIO 58 | 59 | def dump(self, value: Any, dump_bulk=False) -> None: 60 | if isinstance(value, int): 61 | self.writer.write(f":{value}\r\n".encode()) 62 | elif isinstance(value, (str, bytes)): 63 | value = to_bytes(value) 64 | if dump_bulk or b"\r" in value or b"\n" in value: 65 | self.writer.write(b"$" + str(len(value)).encode() + b"\r\n" + value + b"\r\n") 66 | else: 67 | self.writer.write(b"+" + value + b"\r\n") 68 | elif isinstance(value, (list, set)): 69 | self.writer.write(f"*{len(value)}\r\n".encode()) 70 | for item in value: 71 | self.dump(item, dump_bulk=True) 72 | elif value is None: 73 | self.writer.write("$-1\r\n".encode()) 74 | elif isinstance(value, Exception): 75 | self.writer.write(f"-{value.args[0]}\r\n".encode()) 76 | 77 | 78 | class TCPFakeRequestHandler(StreamRequestHandler): 79 | def setup(self) -> None: 80 | super().setup() 81 | if self.client_address in self.server.clients: 82 | self.current_client = self.server.clients[self.client_address] 83 | else: 84 | self.current_client = Client( 85 | connection=FakeRedis(server=self.server.fake_server), 86 | client_address=self.client_address, 87 | ) 88 | self.reader = Reader(self.rfile) 89 | self.writer = Writer(self.wfile) 90 | self.server.clients[self.client_address] = self.current_client 91 | 92 | def handle(self): 93 | while True: 94 | try: 95 | self.data = self.reader.load() 96 | LOGGER.debug(f">>> {self.client_address[0]}: {self.data}") 97 | res = self.current_client.connection.execute_command(*self.data) 98 | LOGGER.debug(f"<<< {self.client_address[0]}: {res}") 99 | self.writer.dump(res) 100 | except Exception as e: 101 | LOGGER.debug(f"!!! {self.client_address[0]}: {e}") 102 | self.writer.dump(e) 103 | break 104 | 105 | def finish(self) -> None: 106 | del self.server.clients[self.current_client.client_address] 107 | super().finish() 108 | 109 | 110 | class TcpFakeServer(ThreadingTCPServer): 111 | def __init__( 112 | self, 113 | server_address: Tuple[str | bytes | bytearray, int], 114 | bind_and_activate: bool = True, 115 | server_type: ServerType = "redis", 116 | server_version: Tuple[int, ...] = (7, 4), 117 | ): 118 | super().__init__(server_address, TCPFakeRequestHandler, bind_and_activate) 119 | self.fake_server = FakeServer(server_type=server_type, version=server_version) 120 | self.client_ids = count(0) 121 | self.clients: Dict[int, FakeRedis] = dict() 122 | 123 | 124 | if __name__ == "__main__": 125 | server = TcpFakeServer(("localhost", 19000)) 126 | server.serve_forever() 127 | -------------------------------------------------------------------------------- /fakeredis/_valkey.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from . import FakeStrictRedis 4 | from ._connection import FakeRedis 5 | from .aioredis import FakeRedis as FakeAsyncRedis 6 | from .typing import Self 7 | 8 | 9 | def _validate_server_type(args_dict: Dict[str, Any]) -> None: 10 | if "server_type" in args_dict and args_dict["server_type"] != "valkey": 11 | raise ValueError("server_type must be valkey") 12 | args_dict.setdefault("server_type", "valkey") 13 | 14 | 15 | class FakeValkey(FakeRedis): 16 | def __init__(self, *args: Any, **kwargs: Any) -> None: 17 | _validate_server_type(kwargs) 18 | super().__init__(*args, **kwargs) 19 | 20 | @classmethod 21 | def from_url(cls, *args: Any, **kwargs: Any) -> Self: 22 | _validate_server_type(kwargs) 23 | return super().from_url(*args, **kwargs) 24 | 25 | 26 | class FakeStrictValkey(FakeStrictRedis): 27 | def __init__(self, *args: Any, **kwargs: Any) -> None: 28 | _validate_server_type(kwargs) 29 | super(FakeStrictValkey, self).__init__(*args, **kwargs) 30 | 31 | @classmethod 32 | def from_url(cls, *args: Any, **kwargs: Any) -> Self: 33 | _validate_server_type(kwargs) 34 | return super().from_url(*args, **kwargs) 35 | 36 | 37 | class FakeAsyncValkey(FakeAsyncRedis): 38 | def __init__(self, *args: Any, **kwargs: Any) -> None: 39 | _validate_server_type(kwargs) 40 | super(FakeAsyncValkey, self).__init__(*args, **kwargs) 41 | 42 | @classmethod 43 | def from_url(cls, *args: Any, **kwargs: Any) -> Self: 44 | _validate_server_type(kwargs) 45 | return super().from_url(*args, **kwargs) 46 | -------------------------------------------------------------------------------- /fakeredis/commands_mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from .acl_mixin import AclCommandsMixin 4 | from .bitmap_mixin import BitmapCommandsMixin 5 | from .connection_mixin import ConnectionCommandsMixin 6 | from .generic_mixin import GenericCommandsMixin 7 | from .geo_mixin import GeoCommandsMixin 8 | from .hash_mixin import HashCommandsMixin 9 | from .list_mixin import ListCommandsMixin 10 | from .pubsub_mixin import PubSubCommandsMixin 11 | from .server_mixin import ServerCommandsMixin 12 | from .set_mixin import SetCommandsMixin 13 | from .streams_mixin import StreamsCommandsMixin 14 | from .string_mixin import StringCommandsMixin 15 | from .transactions_mixin import TransactionsCommandsMixin 16 | 17 | try: 18 | from .scripting_mixin import ScriptingCommandsMixin 19 | except ImportError: 20 | 21 | class ScriptingCommandsMixin: # type: ignore # noqa: E303 22 | def __init__(self, *args: Any, **kwargs: Any) -> None: 23 | kwargs.pop("lua_modules", None) 24 | super(ScriptingCommandsMixin, self).__init__(*args, **kwargs) # type: ignore 25 | 26 | 27 | __all__ = [ 28 | "BitmapCommandsMixin", 29 | "ConnectionCommandsMixin", 30 | "GenericCommandsMixin", 31 | "GeoCommandsMixin", 32 | "HashCommandsMixin", 33 | "ListCommandsMixin", 34 | "PubSubCommandsMixin", 35 | "ScriptingCommandsMixin", 36 | "TransactionsCommandsMixin", 37 | "ServerCommandsMixin", 38 | "SetCommandsMixin", 39 | "StreamsCommandsMixin", 40 | "StringCommandsMixin", 41 | "AclCommandsMixin", 42 | ] 43 | -------------------------------------------------------------------------------- /fakeredis/commands_mixins/connection_mixin.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Union, Dict 2 | 3 | import fakeredis 4 | from fakeredis import _msgs as msgs 5 | from fakeredis._commands import command, DbIndex, Int 6 | from fakeredis._helpers import SimpleError, OK, SimpleString, Database, casematch 7 | 8 | PONG = SimpleString(b"PONG") 9 | 10 | 11 | class ConnectionCommandsMixin: 12 | def __init__(self, *args: Any, **kwargs: Any) -> None: 13 | super(ConnectionCommandsMixin, self).__init__(*args, **kwargs) 14 | self._db: Database 15 | self._db_num: int 16 | self._pubsub: int 17 | self._client_info: Dict[str, Union[str, int]] 18 | self._server: Any 19 | 20 | @command((bytes,)) 21 | def echo(self, message: bytes) -> bytes: 22 | return message 23 | 24 | @command((), (bytes,)) 25 | def ping(self, *args: bytes) -> Union[List[bytes], bytes, SimpleString]: 26 | if len(args) > 1: 27 | msg = msgs.WRONG_ARGS_MSG6.format("ping") 28 | raise SimpleError(msg) 29 | if self._pubsub and self.protocol_version == 2: 30 | return [b"pong", args[0] if args else b""] 31 | else: 32 | return args[0] if args else PONG 33 | 34 | @command(name="SELECT", fixed=(DbIndex,)) 35 | def select(self, index: DbIndex) -> SimpleString: 36 | self._db = self._server.dbs[index] 37 | self._db_num = index # type: ignore 38 | return OK 39 | 40 | @command(name="CLIENT SETINFO", fixed=(bytes, bytes), repeat=()) 41 | def client_setinfo(self, lib_data: bytes, value: bytes) -> SimpleString: 42 | if casematch(lib_data, b"LIB-NAME"): 43 | self._client_info["lib-name"] = value.decode("utf-8") 44 | return OK 45 | if casematch(lib_data, b"LIB-VER"): 46 | self._client_info["lib-ver"] = value.decode("utf-8") 47 | return OK 48 | raise SimpleError(msgs.SYNTAX_ERROR_MSG) 49 | 50 | @command(name="CLIENT SETNAME", fixed=(bytes,), repeat=()) 51 | def client_setname(self, value: bytes) -> SimpleString: 52 | self._client_info["name"] = value.decode("utf-8") 53 | return OK 54 | 55 | @command(name="CLIENT GETNAME", fixed=(), repeat=()) 56 | def client_getname(self) -> bytes: 57 | return self._client_info.get("name", "").encode("utf-8") 58 | 59 | @command(name="CLIENT ID", fixed=(), repeat=()) 60 | def client_getid(self) -> int: 61 | return self._client_info.get("id", 1) 62 | 63 | @command(name="CLIENT INFO", fixed=(), repeat=()) 64 | def client_info_cmd(self) -> bytes: 65 | return self.client_info_as_bytes 66 | 67 | @command(name="CLIENT LIST", fixed=(), repeat=(bytes,)) 68 | def client_list_cmd(self, *args: bytes) -> bytes: 69 | sockets = self._server.sockets.copy() 70 | i = 0 71 | filter_ids = set() 72 | while i < len(args): 73 | if casematch(args[i], b"TYPE") and i + 1 < len(args): 74 | i += 2 75 | if casematch(args[i], b"ID") and i + 1 < len(args): 76 | i += 1 77 | while i < len(args): 78 | filter_ids.add(Int.decode(args[i])) 79 | i += 1 80 | else: 81 | raise SimpleError(msgs.SYNTAX_ERROR_MSG) 82 | if len(filter_ids) > 0: 83 | sockets = [sock for sock in sockets if sock._client_info["id"] in filter_ids] 84 | res = [item.client_info_as_bytes for item in sockets] 85 | return b"\n".join(res) 86 | 87 | @command(name="HELLO", fixed=(), repeat=(bytes,)) 88 | def hello(self, *args: bytes) -> List[bytes]: 89 | self._client_info["resp"] = 2 if len(args) == 0 else Int.decode(args[0]) 90 | i = 1 91 | while i < len(args): 92 | if args[i] == b"SETNAME" and i + 1 < len(args): 93 | self._client_info["name"] = args[i + 1].decode("utf-8") 94 | i += 2 95 | elif args[i] == b"AUTH" and i + 2 < len(args): 96 | user = args[i + 1] 97 | password = args[i + 2] 98 | self._server._acl.get_user_acl(user).check_password(password) 99 | i += 3 100 | else: 101 | raise SimpleError(msgs.SYNTAX_ERROR_MSG) 102 | data = dict( 103 | server="fakeredis", 104 | version=fakeredis.__version__, 105 | proto=self._client_info["resp"], 106 | id=self._client_info.get("id", 1), 107 | mode="standalone", 108 | role="master", 109 | modules=[], 110 | ) 111 | return data 112 | -------------------------------------------------------------------------------- /fakeredis/commands_mixins/server_mixin.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Any, List 3 | 4 | from fakeredis import _msgs as msgs 5 | from fakeredis._commands import command, DbIndex 6 | from fakeredis._helpers import OK, SimpleError, casematch, BGSAVE_STARTED, Database, SimpleString 7 | from fakeredis.model import get_command_info, get_all_commands_info 8 | 9 | 10 | class ServerCommandsMixin: 11 | def __init__(self, *args: Any, **kwargs: Any) -> None: 12 | super().__init__(*args, **kwargs) 13 | from fakeredis._server import FakeServer 14 | 15 | self._server: "FakeServer" 16 | self._db: Database 17 | 18 | @command((), (bytes,), flags=msgs.FLAG_NO_SCRIPT) 19 | def bgsave(self, *args: bytes) -> SimpleString: 20 | if len(args) > 1 or (len(args) == 1 and not casematch(args[0], b"schedule")): 21 | raise SimpleError(msgs.SYNTAX_ERROR_MSG) 22 | self._server.lastsave = int(time.time()) 23 | return BGSAVE_STARTED 24 | 25 | @command(()) 26 | def dbsize(self) -> int: 27 | return len(self._db) 28 | 29 | @command((), (bytes,)) 30 | def flushdb(self, *args: bytes) -> SimpleString: 31 | if len(args) > 0 and (len(args) != 1 or not casematch(args[0], b"async")): 32 | raise SimpleError(msgs.SYNTAX_ERROR_MSG) 33 | self._db.clear() 34 | return OK 35 | 36 | @command((), (bytes,)) 37 | def flushall(self, *args: bytes) -> SimpleString: 38 | if len(args) > 0 and (len(args) != 1 or not casematch(args[0], b"async")): 39 | raise SimpleError(msgs.SYNTAX_ERROR_MSG) 40 | for db in self._server.dbs.values(): 41 | db.clear() 42 | # TODO: clear watches and/or pubsub as well? 43 | return OK 44 | 45 | @command(()) 46 | def lastsave(self) -> int: 47 | return self._server.lastsave 48 | 49 | @command((), flags=msgs.FLAG_NO_SCRIPT) 50 | def save(self) -> SimpleString: 51 | self._server.lastsave = int(time.time()) 52 | return OK 53 | 54 | @command(()) 55 | def time(self) -> List[bytes]: 56 | now_us = round(time.time() * 1_000_000) 57 | now_s = now_us // 1_000_000 58 | now_us %= 1_000_000 59 | return [str(now_s).encode(), str(now_us).encode()] 60 | 61 | @command((DbIndex, DbIndex)) 62 | def swapdb(self, index1: int, index2: int) -> SimpleString: 63 | if index1 != index2: 64 | db1 = self._server.dbs[index1] 65 | db2 = self._server.dbs[index2] 66 | db1.swap(db2) 67 | return OK 68 | 69 | @command(name="COMMAND INFO", fixed=(), repeat=(bytes,)) 70 | def command_info(self, *commands: bytes) -> List[Any]: 71 | res = [get_command_info(cmd) for cmd in commands] 72 | return res 73 | 74 | @command(name="COMMAND COUNT", fixed=(), repeat=()) 75 | def command_count(self) -> int: 76 | return len(get_all_commands_info()) 77 | 78 | @command(name="COMMAND", fixed=(), repeat=()) 79 | def command_(self) -> List[Any]: 80 | res = [get_command_info(cmd) for cmd in get_all_commands_info()] 81 | return res 82 | -------------------------------------------------------------------------------- /fakeredis/commands_mixins/transactions_mixin.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Set, Any, List, Optional 2 | 3 | from fakeredis import _msgs as msgs 4 | from fakeredis._commands import command, Key, CommandItem 5 | from fakeredis._helpers import OK, SimpleError, Database, SimpleString 6 | 7 | 8 | class TransactionsCommandsMixin: 9 | _run_command: Callable # type: ignore 10 | 11 | def __init__(self, *args, **kwargs) -> None: # type: ignore 12 | super(TransactionsCommandsMixin, self).__init__(*args, **kwargs) 13 | self._watches: Set[Any] = set() 14 | # When in a MULTI, set to a list of function calls 15 | self._transaction: Optional[List[Any]] = None 16 | self._transaction_failed = False 17 | # Set when executing the commands from EXEC 18 | self._in_transaction = False 19 | self._watch_notified = False 20 | self._db: Database 21 | 22 | def _clear_watches(self) -> None: 23 | self._watch_notified = False 24 | while self._watches: 25 | (key, db) = self._watches.pop() 26 | db.remove_watch(key, self) 27 | 28 | # Transaction commands 29 | @command((), flags=[msgs.FLAG_NO_SCRIPT, msgs.FLAG_TRANSACTION]) 30 | def discard(self) -> SimpleString: 31 | if self._transaction is None: 32 | raise SimpleError(msgs.WITHOUT_MULTI_MSG.format("DISCARD")) 33 | self._transaction = None 34 | self._transaction_failed = False 35 | self._clear_watches() 36 | return OK 37 | 38 | @command(name="exec", fixed=(), repeat=(), flags=[msgs.FLAG_NO_SCRIPT, msgs.FLAG_TRANSACTION]) 39 | def exec_(self) -> Any: 40 | if self._transaction is None: 41 | raise SimpleError(msgs.WITHOUT_MULTI_MSG.format("EXEC")) 42 | if self._transaction_failed: 43 | self._transaction = None 44 | self._clear_watches() 45 | raise SimpleError(msgs.EXECABORT_MSG) 46 | transaction = self._transaction 47 | self._transaction = None 48 | self._transaction_failed = False 49 | watch_notified = self._watch_notified 50 | self._clear_watches() 51 | if watch_notified: 52 | return None 53 | result = [] 54 | for func, sig, args in transaction: 55 | try: 56 | self._in_transaction = True 57 | ans = self._run_command(func, sig, args, False) 58 | except SimpleError as exc: 59 | ans = exc 60 | finally: 61 | self._in_transaction = False 62 | result.append(ans) 63 | return result 64 | 65 | @command((), flags=[msgs.FLAG_NO_SCRIPT, msgs.FLAG_TRANSACTION]) 66 | def multi(self) -> SimpleString: 67 | if self._transaction is not None: 68 | raise SimpleError(msgs.MULTI_NESTED_MSG) 69 | self._transaction = [] 70 | self._transaction_failed = False 71 | return OK 72 | 73 | @command((), flags=msgs.FLAG_NO_SCRIPT) 74 | def unwatch(self) -> SimpleString: 75 | self._clear_watches() 76 | return OK 77 | 78 | @command((Key(),), (Key(),), flags=[msgs.FLAG_NO_SCRIPT, msgs.FLAG_TRANSACTION]) 79 | def watch(self, *keys: CommandItem) -> SimpleString: 80 | if self._transaction is not None: 81 | raise SimpleError(msgs.WATCH_INSIDE_MULTI_MSG) 82 | for key in keys: 83 | if key not in self._watches: 84 | self._watches.add((key.key, self._db)) 85 | self._db.add_watch(key.key, self) 86 | return OK 87 | 88 | def notify_watch(self) -> None: 89 | self._watch_notified = True 90 | -------------------------------------------------------------------------------- /fakeredis/geo/__init__.py: -------------------------------------------------------------------------------- 1 | from .geohash import geo_encode, geo_decode 2 | from .haversine import distance 3 | 4 | __all__ = [ 5 | "geo_encode", 6 | "geo_decode", 7 | "distance", 8 | ] 9 | -------------------------------------------------------------------------------- /fakeredis/geo/geohash.py: -------------------------------------------------------------------------------- 1 | # Note: the alphabet in geohash differs from the common base32 2 | # alphabet described in IETF's RFC 4648 3 | # (http://tools.ietf.org/html/rfc4648) 4 | from typing import Tuple 5 | 6 | base32 = "0123456789bcdefghjkmnpqrstuvwxyz" 7 | decodemap = {base32[i]: i for i in range(len(base32))} 8 | 9 | 10 | def geo_decode(geohash: str) -> Tuple[float, float, float, float]: 11 | """ 12 | Decode the geohash to its exact values, including the error margins of the result. Returns four float values: 13 | latitude, longitude, the plus/minus error for latitude (as a positive number) and the plus/minus error for longitude 14 | (as a positive number). 15 | """ 16 | lat_interval, lon_interval = (-90.0, 90.0), (-180.0, 180.0) 17 | lat_err, lon_err = 90.0, 180.0 18 | is_longitude = True 19 | for c in geohash: 20 | cd = decodemap[c] 21 | for mask in [16, 8, 4, 2, 1]: 22 | if is_longitude: # adds longitude info 23 | lon_err /= 2 24 | if cd & mask: 25 | lon_interval = ( 26 | (lon_interval[0] + lon_interval[1]) / 2, 27 | lon_interval[1], 28 | ) 29 | else: 30 | lon_interval = ( 31 | lon_interval[0], 32 | (lon_interval[0] + lon_interval[1]) / 2, 33 | ) 34 | else: # adds latitude info 35 | lat_err /= 2 36 | if cd & mask: 37 | lat_interval = ( 38 | (lat_interval[0] + lat_interval[1]) / 2, 39 | lat_interval[1], 40 | ) 41 | else: 42 | lat_interval = ( 43 | lat_interval[0], 44 | (lat_interval[0] + lat_interval[1]) / 2, 45 | ) 46 | is_longitude = not is_longitude 47 | lat = (lat_interval[0] + lat_interval[1]) / 2 48 | lon = (lon_interval[0] + lon_interval[1]) / 2 49 | return lat, lon, lat_err, lon_err 50 | 51 | 52 | def geo_encode(latitude: float, longitude: float, precision: int = 12) -> str: 53 | """ 54 | Encode a position given in float arguments latitude, longitude to a geohash which will have the character count 55 | precision. 56 | """ 57 | lat_interval, lon_interval = (-90.0, 90.0), (-180.0, 180.0) 58 | geohash, bits = [], [16, 8, 4, 2, 1] # type: ignore 59 | bit, ch = 0, 0 60 | is_longitude = True 61 | 62 | def next_interval(curr: float, interval: Tuple[float, float], ch: int) -> Tuple[Tuple[float, float], int]: 63 | mid = (interval[0] + interval[1]) / 2 64 | if curr > mid: 65 | ch |= bits[bit] 66 | return (mid, interval[1]), ch 67 | else: 68 | return (interval[0], mid), ch 69 | 70 | while len(geohash) < precision: 71 | if is_longitude: 72 | lon_interval, ch = next_interval(longitude, lon_interval, ch) 73 | else: 74 | lat_interval, ch = next_interval(latitude, lat_interval, ch) 75 | is_longitude = not is_longitude 76 | if bit < 4: 77 | bit += 1 78 | else: 79 | geohash += base32[ch] 80 | bit = 0 81 | ch = 0 82 | return "".join(geohash) 83 | -------------------------------------------------------------------------------- /fakeredis/geo/haversine.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import Tuple 3 | 4 | 5 | def distance(origin: Tuple[float, float], destination: Tuple[float, float]) -> float: 6 | """Calculate the Haversine distance in meters.""" 7 | radius = 6372797.560856 # Earth's quatratic mean radius for WGS-84 8 | 9 | lat1, lon1, lat2, lon2 = map(math.radians, [origin[0], origin[1], destination[0], destination[1]]) 10 | 11 | dlon = lon2 - lon1 12 | dlat = lat2 - lat1 13 | a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 14 | c = 2 * math.asin(math.sqrt(a)) 15 | 16 | return c * radius 17 | -------------------------------------------------------------------------------- /fakeredis/model/__init__.py: -------------------------------------------------------------------------------- 1 | from ._expiring_members_set import ExpiringMembersSet 2 | from ._hash import Hash 3 | from ._stream import XStream, StreamEntryKey, StreamGroup, StreamRangeTest 4 | from ._timeseries_model import TimeSeries, TimeSeriesRule, AGGREGATORS 5 | from ._topk import HeavyKeeper 6 | from ._zset import ZSet 7 | 8 | from ._acl import AccessControlList 9 | from ._command_info import ( 10 | get_all_commands_info, 11 | get_command_info, 12 | get_categories, 13 | get_commands_by_category, 14 | ) 15 | 16 | __all__ = [ 17 | "XStream", 18 | "StreamRangeTest", 19 | "StreamGroup", 20 | "StreamEntryKey", 21 | "ZSet", 22 | "TimeSeries", 23 | "TimeSeriesRule", 24 | "AGGREGATORS", 25 | "HeavyKeeper", 26 | "Hash", 27 | "ExpiringMembersSet", 28 | "get_all_commands_info", 29 | "get_command_info", 30 | "get_categories", 31 | "get_commands_by_category", 32 | "AccessControlList", 33 | ] 34 | -------------------------------------------------------------------------------- /fakeredis/model/_command_info.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Optional, Dict, List, Any, AnyStr 4 | from fakeredis._helpers import asbytes 5 | 6 | _COMMAND_INFO: Optional[Dict[bytes, List[Any]]] = None 7 | 8 | 9 | def _encode_obj(obj: Any) -> Any: 10 | if isinstance(obj, str): 11 | return obj.encode() 12 | if isinstance(obj, list): 13 | return [_encode_obj(x) for x in obj] 14 | if isinstance(obj, dict): 15 | return {_encode_obj(k): _encode_obj(obj[k]) for k in obj} 16 | return obj 17 | 18 | 19 | def _load_command_info() -> None: 20 | global _COMMAND_INFO 21 | if _COMMAND_INFO is None: 22 | with open(os.path.join(os.path.dirname(__file__), "..", "commands.json"), encoding="utf8") as f: 23 | _COMMAND_INFO = _encode_obj(json.load(f)) 24 | 25 | 26 | def get_all_commands_info() -> Dict[bytes, List[Any]]: 27 | _load_command_info() 28 | return _COMMAND_INFO # type: ignore[return-value] 29 | 30 | 31 | def get_command_info(cmd: bytes) -> Optional[List[Any]]: 32 | _load_command_info() 33 | if _COMMAND_INFO is None or cmd not in _COMMAND_INFO: 34 | return None 35 | return _COMMAND_INFO.get(cmd, None) 36 | 37 | 38 | def get_categories() -> List[bytes]: 39 | _load_command_info() 40 | if _COMMAND_INFO is None: 41 | return [] 42 | categories = set() 43 | for info in _COMMAND_INFO.values(): 44 | categories.update(info[6]) 45 | categories = {asbytes(x[1:]) for x in categories} 46 | return list(categories) 47 | 48 | 49 | def get_commands_by_category(category: AnyStr) -> List[bytes]: 50 | _load_command_info() 51 | if _COMMAND_INFO is None: 52 | return [] 53 | category = asbytes(category) 54 | if category[0] != ord(b"@"): 55 | category = b"@" + category 56 | commands = [] 57 | for cmd, info in _COMMAND_INFO.items(): 58 | if category in info[6]: 59 | commands.append(cmd) 60 | return commands 61 | -------------------------------------------------------------------------------- /fakeredis/model/_expiring_members_set.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional, Any, Dict, Union, Set 2 | 3 | from fakeredis import _msgs as msgs 4 | from fakeredis._helpers import current_time 5 | from fakeredis.typing import Self 6 | 7 | 8 | class ExpiringMembersSet: 9 | DECODE_ERROR = msgs.INVALID_HASH_MSG 10 | redis_type = b"set" 11 | 12 | def __init__(self, values: Optional[Dict[bytes, Optional[int]]] = None, *args: Any, **kwargs: Any) -> None: 13 | super().__init__(*args, **kwargs) 14 | self._values: Dict[bytes, Optional[int]] = values or dict() 15 | 16 | def _expire_members(self) -> None: 17 | removed = [] 18 | now = current_time() 19 | for k in self._values: 20 | if (self._values[k] or (now + 1)) < now: 21 | removed.append(k) 22 | for k in removed: 23 | self._values.pop(k) 24 | 25 | def set_member_expireat(self, key: bytes, when_ms: int) -> int: 26 | now = current_time() 27 | if when_ms <= now: 28 | self._values.pop(key, None) 29 | return 2 30 | self._values[key] = when_ms 31 | return 1 32 | 33 | def clear_key_expireat(self, key: bytes) -> bool: 34 | return self._values.pop(key, None) is not None 35 | 36 | def get_key_expireat(self, key: bytes) -> Optional[int]: 37 | self._expire_members() 38 | return self._values.get(key, None) 39 | 40 | def __contains__(self, key: bytes) -> bool: 41 | self._expire_members() 42 | return self._values.__contains__(key) 43 | 44 | def __delitem__(self, key: bytes) -> None: 45 | self._values.pop(key, None) 46 | 47 | def __len__(self) -> int: 48 | self._expire_members() 49 | return len(self._values) 50 | 51 | def __iter__(self) -> Iterable[bytes]: 52 | self._expire_members() 53 | now = current_time() 54 | return iter({k for k in self._values if (self._values[k] or (now + 1)) >= now}) 55 | 56 | def __get__(self, instance: object, owner: None = None) -> Set[bytes]: 57 | self._expire_members() 58 | return set(self._values.keys()) 59 | 60 | def __sub__(self, other: Self) -> "ExpiringMembersSet": 61 | self._expire_members() 62 | other._expire_members() 63 | return ExpiringMembersSet({k: v for k, v in self._values.items() if k not in other._values}) 64 | 65 | def __and__(self, other: Self) -> "ExpiringMembersSet": 66 | self._expire_members() 67 | other._expire_members() 68 | return ExpiringMembersSet({k: v for k, v in self._values.items() if k in other._values}) 69 | 70 | def __or__(self, other: Self) -> "ExpiringMembersSet": 71 | self._expire_members() 72 | other._expire_members() 73 | return ExpiringMembersSet({k: v for k, v in self._values.items()}).update(other) 74 | 75 | def update(self, other: Union[Self, Iterable[bytes]]) -> Self: 76 | self._expire_members() 77 | if isinstance(other, ExpiringMembersSet): 78 | self._values.update(other._values) 79 | return self 80 | for value in other: 81 | self._values[value] = None 82 | return self 83 | 84 | def discard(self, key: bytes) -> None: 85 | self._values.pop(key, None) 86 | 87 | def remove(self, key: bytes) -> None: 88 | self._values.pop(key) 89 | 90 | def add(self, key: bytes) -> None: 91 | self._values[key] = None 92 | 93 | def copy(self) -> "ExpiringMembersSet": 94 | return ExpiringMembersSet(self._values.copy()) 95 | -------------------------------------------------------------------------------- /fakeredis/model/_hash.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Tuple, Optional, Any, Dict, AnyStr 2 | 3 | from fakeredis import _msgs as msgs 4 | from fakeredis._helpers import current_time, asbytes 5 | 6 | 7 | class Hash: 8 | DECODE_ERROR = msgs.INVALID_HASH_MSG 9 | redis_type = b"hash" 10 | 11 | def __init__(self, *args: Any, **kwargs: Any) -> None: 12 | super().__init__(*args, **kwargs) 13 | self._expirations: Dict[bytes, int] = dict() 14 | self._values: Dict[bytes, bytes] = dict() 15 | 16 | def _expire_keys(self) -> None: 17 | removed = [] 18 | now = current_time() 19 | for k in self._expirations: 20 | if self._expirations[k] < now: 21 | self._values.pop(k, None) 22 | removed.append(k) 23 | for k in removed: 24 | self._expirations.pop(k, None) 25 | 26 | def set_key_expireat(self, key: AnyStr, when_ms: int) -> int: 27 | now = current_time() 28 | key_bytes = asbytes(key) 29 | if when_ms <= now: 30 | self._values.pop(key_bytes, None) 31 | self._expirations.pop(key_bytes, None) 32 | return 2 33 | self._expirations[key_bytes] = when_ms 34 | return 1 35 | 36 | def clear_key_expireat(self, key: AnyStr) -> bool: 37 | return self._expirations.pop(asbytes(key), None) is not None 38 | 39 | def get_key_expireat(self, key: AnyStr) -> Optional[int]: 40 | self._expire_keys() 41 | return self._expirations.get(asbytes(key), None) 42 | 43 | def __getitem__(self, key: AnyStr) -> Any: 44 | self._expire_keys() 45 | return self._values.get(asbytes(key)) 46 | 47 | def __contains__(self, key: AnyStr) -> bool: 48 | self._expire_keys() 49 | return self._values.__contains__(asbytes(key)) 50 | 51 | def __setitem__(self, key: AnyStr, value: Any) -> None: 52 | key_bytes = asbytes(key) 53 | self._expirations.pop(key_bytes, None) 54 | self._values[key_bytes] = value 55 | 56 | def __delitem__(self, key: AnyStr) -> None: 57 | key_bytes = asbytes(key) 58 | self._values.pop(key_bytes, None) 59 | self._expirations.pop(key_bytes, None) 60 | 61 | def __len__(self) -> int: 62 | self._expire_keys() 63 | return len(self._values) 64 | 65 | def __iter__(self) -> Iterable[str]: 66 | self._expire_keys() 67 | for k in self._values.keys(): 68 | if isinstance(k, bytes): 69 | yield k.decode("utf-8") 70 | else: 71 | yield k 72 | 73 | def get(self, key: AnyStr, default: Any = None) -> Any: 74 | self._expire_keys() 75 | return self._values.get(asbytes(key), default) 76 | 77 | def keys(self) -> Iterable[bytes]: 78 | self._expire_keys() 79 | return [asbytes(k) for k in self._values.keys()] 80 | 81 | def values(self) -> Iterable[Any]: 82 | return [v for k, v in self.items()] 83 | 84 | def items(self) -> Iterable[Tuple[bytes, Any]]: 85 | self._expire_keys() 86 | return [(asbytes(k), asbytes(v)) for k, v in self._values.items()] 87 | 88 | def update(self, values: Dict[bytes, Any], clear_expiration: bool) -> None: 89 | self._expire_keys() 90 | if clear_expiration: 91 | for k, v in values.items(): 92 | self.clear_key_expireat(k) 93 | for k, v in values.items(): 94 | self._values[asbytes(k)] = v 95 | 96 | def getall(self) -> Dict[bytes, bytes]: 97 | self._expire_keys() 98 | res = self._values.copy() 99 | return {asbytes(k): asbytes(v) for k, v in res.items()} 100 | 101 | def pop(self, key: AnyStr, d: Any = None) -> Any: 102 | self._expire_keys() 103 | return self._values.pop(asbytes(key), d) 104 | -------------------------------------------------------------------------------- /fakeredis/model/_topk.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | import random 3 | import time 4 | from typing import List, Optional, Tuple 5 | 6 | 7 | class Bucket(object): 8 | def __init__(self, counter: int, fingerprint: int): 9 | self.counter = counter 10 | self.fingerprint = fingerprint 11 | 12 | def add(self, fingerprint: int, incr: int, decay: float) -> int: 13 | if self.fingerprint == fingerprint: 14 | self.counter += incr 15 | return self.counter 16 | elif self._decay(decay): 17 | self.counter += incr 18 | self.fingerprint = fingerprint 19 | return self.counter 20 | return 0 21 | 22 | def count(self, fingerprint: int) -> int: 23 | if self.fingerprint == fingerprint: 24 | return self.counter 25 | return 0 26 | 27 | def _decay(self, decay: float) -> bool: 28 | if self.counter > 0: 29 | probability = decay**self.counter 30 | if probability >= 1 or random.random() < probability: 31 | self.counter -= 1 32 | return self.counter == 0 33 | 34 | 35 | class HashArray(object): 36 | def __init__(self, width: int, decay: float): 37 | self.width = width 38 | self.decay = decay 39 | self.array = [Bucket(0, 0) for _ in range(width)] 40 | self._seed = random.getrandbits(32) 41 | 42 | def count(self, item: bytes) -> int: 43 | return self.get_bucket(item).count(self._hash(item)) 44 | 45 | def add(self, item: bytes, incr: int) -> int: 46 | bucket = self.get_bucket(item) 47 | return bucket.add(self._hash(item), incr, self.decay) 48 | 49 | def get_bucket(self, item: bytes) -> Bucket: 50 | return self.array[self._hash(item) % self.width] 51 | 52 | def _hash(self, item: bytes) -> int: 53 | return hash(item) ^ self._seed 54 | 55 | 56 | class HeavyKeeper(object): 57 | is_topk_initialized = False 58 | 59 | def __init__(self, k: int, width: int = 1024, depth: int = 5, decay: float = 0.9) -> None: 60 | if not HeavyKeeper.is_topk_initialized: 61 | random.seed(time.time()) 62 | self.k = k 63 | self.width = width 64 | self.depth = depth 65 | self.decay = decay 66 | self.hash_arrays = [HashArray(width, decay) for _ in range(depth)] 67 | self.min_heap: List[Tuple[int, bytes]] = list() 68 | 69 | def _index(self, val: bytes) -> int: 70 | for ind, item in enumerate(self.min_heap): 71 | if item[1] == val: 72 | return ind 73 | return -1 74 | 75 | def add(self, item: bytes, incr: int) -> Optional[bytes]: 76 | max_count = 0 77 | for i in range(self.depth): 78 | count = self.hash_arrays[i].add(item, incr) 79 | max_count = max(max_count, count) 80 | if len(self.min_heap) < self.k: 81 | heapq.heappush(self.min_heap, (max_count, item)) 82 | return None 83 | ind = self._index(item) 84 | if ind >= 0: 85 | self.min_heap[ind] = (max_count, item) 86 | heapq.heapify(self.min_heap) 87 | return None 88 | if max_count > self.min_heap[0][0]: 89 | expelled = heapq.heapreplace(self.min_heap, (max_count, item)) 90 | return expelled[1] 91 | return None 92 | 93 | def count(self, item: bytes) -> int: 94 | ind = self._index(item) 95 | if ind > 0: 96 | return self.min_heap[ind][0] 97 | return max([ha.count(item) for ha in self.hash_arrays]) 98 | 99 | def list(self, k: Optional[int] = None) -> List[Tuple[int, bytes]]: 100 | sorted_list = sorted(self.min_heap, key=lambda x: x[0], reverse=True) 101 | if k is None: 102 | return sorted_list 103 | return sorted_list[:k] 104 | -------------------------------------------------------------------------------- /fakeredis/model/_zset.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Tuple, Optional, Generator, Dict, ItemsView 2 | 3 | import sortedcontainers 4 | 5 | 6 | class ZSet: 7 | def __init__(self) -> None: 8 | self._bylex: Dict[bytes, float] = {} # Maps value to score 9 | self._byscore = sortedcontainers.SortedList() 10 | 11 | def __contains__(self, value: bytes) -> bool: 12 | return value in self._bylex 13 | 14 | def add(self, value: bytes, score: float) -> bool: 15 | """Update the item and return whether it modified the zset""" 16 | old_score = self._bylex.get(value, None) 17 | if old_score is not None: 18 | if score == old_score: 19 | return False 20 | self._byscore.remove((old_score, value)) 21 | self._bylex[value] = score 22 | self._byscore.add((score, value)) 23 | return True 24 | 25 | def __setitem__(self, value: bytes, score: float) -> None: 26 | self.add(value, score) 27 | 28 | def __getitem__(self, key: bytes) -> float: 29 | return self._bylex[key] 30 | 31 | def get(self, key: bytes, default: Optional[float] = None) -> Optional[float]: 32 | return self._bylex.get(key, default) 33 | 34 | def __len__(self) -> int: 35 | return len(self._bylex) 36 | 37 | def __iter__(self) -> Generator[Any, Any, None]: 38 | def gen() -> Generator[Any, Any, None]: 39 | for score, value in self._byscore: 40 | yield value 41 | 42 | return gen() 43 | 44 | def discard(self, key: bytes) -> None: 45 | try: 46 | score = self._bylex.pop(key) 47 | except KeyError: 48 | return 49 | else: 50 | self._byscore.remove((score, key)) 51 | 52 | def zcount(self, _min: float, _max: float) -> int: 53 | pos1: int = self._byscore.bisect_left(_min) 54 | pos2: int = self._byscore.bisect_left(_max) 55 | return max(0, pos2 - pos1) 56 | 57 | def zlexcount(self, min_value: float, min_exclusive: bool, max_value: float, max_exclusive: bool) -> int: 58 | pos1: int 59 | pos2: int 60 | if not self._byscore: 61 | return 0 62 | score = self._byscore[0][0] 63 | if min_exclusive: 64 | pos1 = self._byscore.bisect_right((score, min_value)) 65 | else: 66 | pos1 = self._byscore.bisect_left((score, min_value)) 67 | if max_exclusive: 68 | pos2 = self._byscore.bisect_left((score, max_value)) 69 | else: 70 | pos2 = self._byscore.bisect_right((score, max_value)) 71 | return max(0, pos2 - pos1) 72 | 73 | def islice_score(self, start: int, stop: int, reverse: bool = False) -> Any: 74 | return self._byscore.islice(start, stop, reverse) 75 | 76 | def irange_lex( 77 | self, start: bytes, stop: bytes, inclusive: Tuple[bool, bool] = (True, True), reverse: bool = False 78 | ) -> Any: 79 | if not self._byscore: 80 | return iter([]) 81 | default_score = self._byscore[0][0] 82 | start_score = self._bylex.get(start, default_score) 83 | stop_score = self._bylex.get(stop, default_score) 84 | it = self._byscore.irange( 85 | (start_score, start), 86 | (stop_score, stop), 87 | inclusive=inclusive, 88 | reverse=reverse, 89 | ) 90 | return (item[1] for item in it) 91 | 92 | def irange_score(self, start: Tuple[Any, bytes], stop: Tuple[Any, bytes], reverse: bool) -> Any: 93 | return self._byscore.irange(start, stop, reverse=reverse) 94 | 95 | def rank(self, member: bytes) -> Tuple[int, float]: 96 | ind: int = self._byscore.index((self._bylex[member], member)) 97 | return ind, self._byscore[ind][0] 98 | 99 | def items(self) -> ItemsView[bytes, Any]: 100 | return self._bylex.items() 101 | -------------------------------------------------------------------------------- /fakeredis/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/fakeredis/py.typed -------------------------------------------------------------------------------- /fakeredis/server_specific_commands/__init__.py: -------------------------------------------------------------------------------- 1 | from fakeredis.server_specific_commands.dragonfly_mixin import DragonflyCommandsMixin 2 | 3 | __all__ = [ 4 | "DragonflyCommandsMixin", 5 | ] 6 | -------------------------------------------------------------------------------- /fakeredis/server_specific_commands/dragonfly_mixin.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from fakeredis._commands import command, Key, Int, CommandItem 4 | from fakeredis._helpers import Database, current_time 5 | from fakeredis.model import ExpiringMembersSet 6 | 7 | 8 | class DragonflyCommandsMixin(object): 9 | _expireat: Callable[[CommandItem, int], int] 10 | 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self._db: Database 14 | 15 | @command(name="SADDEX", fixed=(Key(ExpiringMembersSet), Int, bytes), repeat=(bytes,), server_types=("dragonfly",)) 16 | def saddex(self, key: CommandItem, seconds: int, *members: bytes) -> int: 17 | val = key.value 18 | old_size = len(val) 19 | new_members = set(members) - set(val) 20 | expire_at_ms = current_time() + seconds * 1000 21 | for member in new_members: 22 | val.set_member_expireat(member, expire_at_ms) 23 | key.updated() 24 | return len(val) - old_size 25 | -------------------------------------------------------------------------------- /fakeredis/stack/__init__.py: -------------------------------------------------------------------------------- 1 | from ._tdigest_mixin import TDigestCommandsMixin 2 | from ._timeseries_mixin import TimeSeriesCommandsMixin 3 | from ._topk_mixin import TopkCommandsMixin # noqa: F401 4 | 5 | try: 6 | from jsonpath_ng.ext import parse # noqa: F401 7 | from redis.commands.json.path import Path # noqa: F401 8 | from ._json_mixin import JSONCommandsMixin, JSONObject # noqa: F401 9 | except ImportError as e: 10 | if e.name == "fakeredis.stack._json_mixin": 11 | raise e 12 | 13 | class JSONCommandsMixin: # type: ignore # noqa: E303 14 | pass 15 | 16 | 17 | try: 18 | import probables # noqa: F401 19 | 20 | from ._bf_mixin import BFCommandsMixin # noqa: F401 21 | from ._cf_mixin import CFCommandsMixin # noqa: F401 22 | from ._cms_mixin import CMSCommandsMixin # noqa: F401 23 | except ImportError as e: 24 | if e.name == "fakeredis.stack._bf_mixin" or e.name == "fakeredis.stack._cf_mixin": 25 | raise e 26 | 27 | class BFCommandsMixin: # type: ignore # noqa: E303 28 | pass 29 | 30 | class CFCommandsMixin: # type: ignore # noqa: E303 31 | pass 32 | 33 | class CMSCommandsMixin: # type: ignore # noqa: E303 34 | pass 35 | 36 | 37 | __all__ = [ 38 | "TopkCommandsMixin", 39 | "JSONCommandsMixin", 40 | "JSONObject", 41 | "BFCommandsMixin", 42 | "CFCommandsMixin", 43 | "CMSCommandsMixin", 44 | "TDigestCommandsMixin", 45 | "TimeSeriesCommandsMixin", 46 | ] 47 | -------------------------------------------------------------------------------- /fakeredis/stack/_cms_mixin.py: -------------------------------------------------------------------------------- 1 | """Command mixin for emulating `redis-py`'s Count-min sketch functionality.""" 2 | 3 | from typing import Optional, Tuple, List, Any, Dict 4 | 5 | import probables 6 | 7 | from fakeredis import _msgs as msgs 8 | from fakeredis._commands import command, CommandItem, Int, Key, Float 9 | from fakeredis._helpers import OK, SimpleString, SimpleError, casematch, Database 10 | 11 | 12 | class CountMinSketch(probables.CountMinSketch): 13 | def __init__( 14 | self, 15 | width: Optional[int] = None, 16 | depth: Optional[int] = None, 17 | probability: Optional[float] = None, 18 | error_rate: Optional[float] = None, 19 | ): 20 | super().__init__(width=width, depth=depth, error_rate=error_rate, confidence=probability) 21 | 22 | 23 | class CMSCommandsMixin: 24 | def __init__(self, *args: Any, **kwargs: Any) -> None: 25 | super().__init__(*args, **kwargs) 26 | self._db: Database 27 | 28 | @command( 29 | name="CMS.INCRBY", 30 | fixed=(Key(CountMinSketch), bytes, bytes), 31 | repeat=(bytes, bytes), 32 | flags=msgs.FLAG_DO_NOT_CREATE, 33 | ) 34 | def cms_incrby(self, key: CommandItem, *args: bytes) -> List[Tuple[bytes, int]]: 35 | if key.value is None: 36 | raise SimpleError("CMS: key does not exist") 37 | pairs: List[Tuple[bytes, int]] = [] 38 | for i in range(0, len(args), 2): 39 | try: 40 | pairs.append((args[i], int(args[i + 1]))) 41 | except ValueError: 42 | raise SimpleError("CMS: Cannot parse number") 43 | res = [] 44 | for pair in pairs: 45 | res.append(key.value.add(pair[0], pair[1])) 46 | key.updated() 47 | return res 48 | 49 | @command(name="CMS.INFO", fixed=(Key(CountMinSketch),), repeat=(), flags=msgs.FLAG_DO_NOT_CREATE) 50 | def cms_info(self, key: CommandItem) -> Dict[bytes, Any]: 51 | if key.value is None: 52 | raise SimpleError("CMS: key does not exist") 53 | return { 54 | b"width": key.value.width, 55 | b"depth": key.value.depth, 56 | b"count": key.value.elements_added, 57 | } 58 | 59 | @command(name="CMS.INITBYDIM", fixed=(Key(CountMinSketch), Int, Int), repeat=(), flags=msgs.FLAG_DO_NOT_CREATE) 60 | def cms_initbydim(self, key: CommandItem, width: int, depth: int) -> SimpleString: 61 | if key.value is not None: 62 | raise SimpleError("CMS key already set") 63 | if width < 1: 64 | raise SimpleError("CMS: invalid width") 65 | if depth < 1: 66 | raise SimpleError("CMS: invalid depth") 67 | key.update(CountMinSketch(width=width, depth=depth)) 68 | return OK 69 | 70 | @command(name="CMS.INITBYPROB", fixed=(Key(CountMinSketch), Float, Float), repeat=(), flags=msgs.FLAG_DO_NOT_CREATE) 71 | def cms_initby_prob(self, key: CommandItem, error_rate: float, probability: float) -> SimpleString: 72 | if key.value is not None: 73 | raise SimpleError("CMS key already set") 74 | if error_rate <= 0 or error_rate >= 1: 75 | raise SimpleError("CMS: invalid overestimation value") 76 | if probability <= 0 or probability >= 1: 77 | raise SimpleError("CMS: invalid prob value") 78 | key.update(CountMinSketch(probability=probability, error_rate=error_rate)) 79 | return OK 80 | 81 | @command(name="CMS.MERGE", fixed=(Key(CountMinSketch), Int, bytes), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) 82 | def cms_merge(self, dest_key: CommandItem, num_keys: int, *args: bytes) -> SimpleString: 83 | if dest_key.value is None: 84 | raise SimpleError("CMS: key does not exist") 85 | 86 | if num_keys < 1: 87 | raise SimpleError("CMS: Number of keys must be positive") 88 | weights = [ 89 | 1, 90 | ] 91 | for i, arg in enumerate(args): 92 | if casematch(b"weights", arg): 93 | weights = [int(i) for i in args[i + 1 :]] 94 | if len(weights) != num_keys: 95 | raise SimpleError("CMS: wrong number of keys/weights") 96 | args = args[:i] 97 | break 98 | dest_key.value.clear() 99 | for i, arg in enumerate(args): 100 | item = self._db.get(arg, None) 101 | if item is None or not isinstance(item.value, CountMinSketch): 102 | raise SimpleError("CMS: key does not exist") 103 | for _ in range(weights[i % len(weights)]): 104 | dest_key.value.join(item.value) 105 | return OK 106 | 107 | @command(name="CMS.QUERY", fixed=(Key(CountMinSketch), bytes), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) 108 | def cms_query(self, key: CommandItem, *items: bytes) -> List[int]: 109 | if key.value is None: 110 | raise SimpleError("CMS: key does not exist") 111 | return [key.value.check(item) for item in items] 112 | -------------------------------------------------------------------------------- /fakeredis/stack/_topk_mixin.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional, Tuple, Dict 2 | 3 | from fakeredis import _msgs as msgs 4 | from fakeredis._command_args_parsing import extract_args 5 | from fakeredis._commands import Key, Int, Float, command, CommandItem 6 | from fakeredis._helpers import OK, SimpleError, SimpleString 7 | from fakeredis.model import HeavyKeeper 8 | 9 | 10 | class TopkCommandsMixin: 11 | """`CommandsMixin` for enabling TopK compatibility in `fakeredis`.""" 12 | 13 | def __init__(self, *args: Any, **kwargs: Any) -> None: 14 | super().__init__(*args, **kwargs) 15 | 16 | @command(name="TOPK.ADD", fixed=(Key(HeavyKeeper), bytes), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) 17 | def topk_add(self, key: CommandItem, *args: bytes) -> List[Optional[bytes]]: 18 | if key.value is None: 19 | raise SimpleError("TOPK: key does not exist") 20 | if not isinstance(key.value, HeavyKeeper): 21 | raise SimpleError("TOPK: key is not a HeavyKeeper") 22 | res = [key.value.add(_item, 1) for _item in args] 23 | key.updated() 24 | return res 25 | 26 | @command(name="TOPK.COUNT", fixed=(Key(HeavyKeeper), bytes), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) 27 | def topk_count(self, key: CommandItem, *args: bytes) -> List[int]: 28 | if key.value is None: 29 | raise SimpleError("TOPK: key does not exist") 30 | if not isinstance(key.value, HeavyKeeper): 31 | raise SimpleError("TOPK: key is not a HeavyKeeper") 32 | res: List[int] = [key.value.count(_item) for _item in args] 33 | return res 34 | 35 | @command(name="TOPK.QUERY", fixed=(Key(HeavyKeeper), bytes), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) 36 | def topk_query(self, key: CommandItem, *args: bytes) -> List[int]: 37 | if key.value is None: 38 | raise SimpleError("TOPK: key does not exist") 39 | if not isinstance(key.value, HeavyKeeper): 40 | raise SimpleError("TOPK: key is not a HeavyKeeper") 41 | topk = {item[1] for item in key.value.list()} 42 | res: List[int] = [1 if _item in topk else 0 for _item in args] 43 | return res 44 | 45 | @command(name="TOPK.INCRBY", fixed=(Key(), bytes, Int), repeat=(bytes, Int), flags=msgs.FLAG_DO_NOT_CREATE) 46 | def topk_incrby(self, key: CommandItem, *args: Any) -> List[Optional[bytes]]: 47 | if key.value is None: 48 | raise SimpleError("TOPK: key does not exist") 49 | if not isinstance(key.value, HeavyKeeper): 50 | raise SimpleError("TOPK: key is not a HeavyKeeper") 51 | if len(args) % 2 != 0: 52 | raise SimpleError("TOPK: number of arguments must be even") 53 | res = list() 54 | for i in range(0, len(args), 2): 55 | val, count = args[i], int(args[i + 1]) 56 | res.append(key.value.add(val, count)) 57 | key.updated() 58 | return res 59 | 60 | @command(name="TOPK.INFO", fixed=(Key(),), repeat=(), flags=msgs.FLAG_DO_NOT_CREATE) 61 | def topk_info(self, key: CommandItem) -> Dict[bytes, Any]: 62 | if key.value is None: 63 | raise SimpleError("TOPK: key does not exist") 64 | if not isinstance(key.value, HeavyKeeper): 65 | raise SimpleError("TOPK: key is not a HeavyKeeper") 66 | return { 67 | b"k": key.value.k, 68 | b"width": key.value.width, 69 | b"depth": key.value.depth, 70 | b"decay": key.value.decay, 71 | } 72 | 73 | @command(name="TOPK.LIST", fixed=(Key(),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) 74 | def topk_list(self, key: CommandItem, *args: Any) -> List[Any]: 75 | (withcount,), _ = extract_args(args, ("withcount",)) 76 | if key.value is None: 77 | raise SimpleError("TOPK: key does not exist") 78 | if not isinstance(key.value, HeavyKeeper): 79 | raise SimpleError("TOPK: key is not a HeavyKeeper") 80 | value_list: List[Tuple[int, bytes]] = key.value.list() 81 | if not withcount: 82 | return [item[1] for item in value_list] 83 | else: 84 | temp = [[item[1], item[0]] for item in value_list] 85 | return [item for sublist in temp for item in sublist] 86 | 87 | @command(name="TOPK.RESERVE", fixed=(Key(), Int), repeat=(Int, Int, Float), flags=msgs.FLAG_DO_NOT_CREATE) 88 | def topk_reserve(self, key: CommandItem, topk: int, *args: Any) -> SimpleString: 89 | if len(args) == 3: 90 | width, depth, decay = args 91 | else: 92 | width, depth, decay = 8, 7, 0.9 93 | if key.value is not None: 94 | raise SimpleError("TOPK: key already set") 95 | key.update(HeavyKeeper(topk, width, depth, decay)) 96 | return OK 97 | -------------------------------------------------------------------------------- /fakeredis/typing.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 11): 4 | from typing import Self 5 | from asyncio import timeout as async_timeout 6 | else: 7 | from async_timeout import timeout as async_timeout 8 | from typing_extensions import Self 9 | 10 | __all__ = [ 11 | "Self", 12 | "async_timeout", 13 | ] 14 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: fakeredis 3 | site_author: Daniel Moran 4 | site_description: >- 5 | Documentation for fakeredis python library 6 | # Repository 7 | repo_name: cunla/fakeredis 8 | repo_url: https://github.com/cunla/fakeredis 9 | 10 | # Copyright 11 | copyright: Copyright © 2022 - 2023 Daniel Moran 12 | 13 | extra: 14 | generator: false 15 | analytics: 16 | provider: google 17 | property: G-GJBJBKXT19 18 | markdown_extensions: 19 | - abbr 20 | - admonition 21 | - attr_list 22 | - def_list 23 | - footnotes 24 | - md_in_html 25 | - pymdownx.arithmatex: 26 | generic: true 27 | - pymdownx.betterem: 28 | smart_enable: all 29 | - pymdownx.caret 30 | - pymdownx.details 31 | - pymdownx.emoji: 32 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 33 | emoji_index: !!python/name:material.extensions.emoji.twemoji 34 | - pymdownx.highlight: 35 | anchor_linenums: true 36 | - pymdownx.inlinehilite 37 | - pymdownx.keys 38 | - pymdownx.magiclink: 39 | repo_url_shorthand: true 40 | user: cunla 41 | repo: fakeredis 42 | - pymdownx.mark 43 | - pymdownx.smartsymbols 44 | - pymdownx.superfences: 45 | custom_fences: 46 | - name: mermaid 47 | class: mermaid 48 | format: !!python/name:pymdownx.superfences.fence_code_format 49 | - pymdownx.tabbed: 50 | alternate_style: true 51 | - pymdownx.tasklist: 52 | custom_checkbox: true 53 | - pymdownx.tilde 54 | - toc: 55 | permalink: true 56 | 57 | nav: 58 | - Home: index.md 59 | - Redis stack: redis-stack.md 60 | - Dragonfly support: dragonfly-support.md 61 | - Valkey support: valkey-support.md 62 | - Supported commands: 63 | - 'supported-commands/index.md' 64 | - Redis commands: 65 | - Bitmap: supported-commands/Redis/BITMAP.md 66 | - Cluster: supported-commands/Redis/CLUSTER.md 67 | - Connection: supported-commands/Redis/CONNECTION.md 68 | - Generic: supported-commands/Redis/GENERIC.md 69 | - Geospatial indices: supported-commands/Redis/GEO.md 70 | - Bitmap: supported-commands/Redis/BITMAP.md 71 | - Hash: supported-commands/Redis/HASH.md 72 | - HyperLogLog: supported-commands/Redis/HYPERLOGLOG.md 73 | - List: supported-commands/Redis/LIST.md 74 | - Pub/Sub: supported-commands/Redis/PUBSUB.md 75 | - Scripting: supported-commands/Redis/SCRIPTING.md 76 | - Server: supported-commands/Redis/SERVER.md 77 | - Set: supported-commands/Redis/SET.md 78 | - Sorted Set: supported-commands/Redis/SORTEDSET.md 79 | - Stream: supported-commands/Redis/STREAM.md 80 | - String: supported-commands/Redis/STRING.md 81 | - Transactions: supported-commands/Redis/TRANSACTIONS.md 82 | - RedisJSON commands: supported-commands/RedisJson/JSON.md 83 | - Time Series commands: supported-commands/RedisTimeSeries/TIMESERIES.md 84 | - Probabilistic commands: 85 | - Bloom Filter: supported-commands/RedisBloom/BF.md 86 | - Cuckoo Filter: supported-commands/RedisBloom/CF.md 87 | - Count-Min Sketch: supported-commands/RedisBloom/CMS.md 88 | - t-digest: supported-commands/RedisBloom/TDIGEST.md 89 | - top-k: supported-commands/RedisBloom/TOPK.md 90 | - Search commands: supported-commands/RedisSearch/SEARCH.md 91 | - Suggestion commands: supported-commands/RedisSearch/SUGGESTION.md 92 | - Dragonfly commands: supported-commands/DRAGONFLY.md 93 | - Guides: 94 | - Implementing support for a command: guides/implement-command.md 95 | - Write a new test case: guides/test-case.md 96 | - About: 97 | - Release Notes: about/changelog.md 98 | - Contributing: about/contributing.md 99 | - License: about/license.md 100 | 101 | theme: 102 | name: material 103 | custom_dir: docs/overrides 104 | palette: 105 | - scheme: default 106 | primary: indigo 107 | accent: indigo 108 | toggle: 109 | icon: material/brightness-7 110 | name: Switch to dark mode 111 | - scheme: slate 112 | primary: indigo 113 | accent: indigo 114 | toggle: 115 | icon: material/brightness-4 116 | name: Switch to light mode 117 | features: 118 | # - announce.dismiss 119 | - content.action.edit 120 | - content.action.view 121 | - content.code.annotate 122 | - content.code.copy 123 | # - content.tabs.link 124 | - content.tooltips 125 | # - header.autohide 126 | # - navigation.expand 127 | - navigation.footer 128 | - navigation.indexes 129 | # - navigation.instant 130 | - navigation.prune 131 | - navigation.sections 132 | # - navigation.tabs.sticky 133 | - navigation.tracking 134 | - search.highlight 135 | - search.share 136 | - search.suggest 137 | - toc.follow 138 | # - toc.integrate 139 | highlightjs: true 140 | hljs_languages: 141 | - yaml 142 | - django 143 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "fakeredis" 7 | version = "2.30.0" 8 | description = "Python implementation of redis API, can be used for testing purposes." 9 | authors = [ 10 | { name = "Daniel Moran", email = "daniel@moransoftware.ca" }, 11 | { name = "Bruce Merry", email = "bmerry@ska.ac.za" }, 12 | { name = "James Saryerwinnie", email = "js@jamesls.com" }, 13 | ] 14 | requires-python = "~=3.7" 15 | readme = "README.md" 16 | license = "BSD-3-Clause" 17 | maintainers = [{ name = "Daniel Moran", email = "daniel@moransoftware.ca" }] 18 | keywords = [ 19 | "redis", 20 | "RedisJson", 21 | "RedisBloom", 22 | "RedisTimeSeries", 23 | "testing", 24 | "redis-stack", 25 | "valkey", 26 | "dragonfly", 27 | ] 28 | classifiers = [ 29 | "Development Status :: 5 - Production/Stable", 30 | "Development Status :: 6 - Mature", 31 | "Intended Audience :: Developers", 32 | "License :: OSI Approved :: BSD License", 33 | "Operating System :: OS Independent", 34 | "Programming Language :: Python", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | "Programming Language :: Python :: 3.12", 39 | "Programming Language :: Python :: 3.13", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | ] 42 | dependencies = [ 43 | "redis>=4 ; python_version < '3.8'", 44 | "redis>=4.3 ; python_version > '3.8'", 45 | "sortedcontainers>=2,<3", 46 | "typing-extensions~=4.7 ; python_version < '3.11'", 47 | ] 48 | 49 | [project.optional-dependencies] 50 | lua = ["lupa>=2.1,<3.0"] 51 | json = ["jsonpath-ng~=1.6"] 52 | bf = ["pyprobables>=0.6"] 53 | cf = ["pyprobables>=0.6"] 54 | probabilistic = ["pyprobables>=0.6"] 55 | 56 | [project.urls] 57 | Homepage = "https://github.com/cunla/fakeredis-py" 58 | Repository = "https://github.com/cunla/fakeredis-py" 59 | Documentation = "https://fakeredis.moransoftware.ca/" 60 | "Bug Tracker" = "https://github.com/cunla/fakeredis-py/issues" 61 | Funding = "https://github.com/sponsors/cunla" 62 | 63 | [dependency-groups] 64 | dev = [ 65 | "ruff>=0.11 ; python_version >= '3.10'", 66 | "mypy>=1.15 ; python_version >= '3.10'", 67 | "pre-commit~=4.2 ; python_version >= '3.10'", 68 | ] 69 | test = [ 70 | "coverage~=7.6 ; python_version >= '3.9'", 71 | "pytest~=8.3 ; python_version >= '3.9'", 72 | "hypothesis~=6.111 ; python_version >= '3.9'", 73 | "pytest-timeout>=2.3.1,<3 ; python_version >= '3.9'", 74 | "pytest-asyncio>=0.24,<0.25 ; python_version >= '3.9'", 75 | "pytest-cov~=6.0 ; python_version >= '3.9'", 76 | "pytest-mock~=3.14 ; python_version >= '3.9'", 77 | "pytest-html~=4.1 ; python_version >= '3.9'", 78 | ] 79 | docs = [ 80 | "python-dotenv>=1,<2 ; python_version >= '3.10'", 81 | "pygithub~=2.3 ; python_version >= '3.10'", 82 | ] 83 | 84 | [tool.uv] 85 | default-groups = [ 86 | "dev", 87 | "test", 88 | "docs", 89 | ] 90 | 91 | [tool.hatch.build.targets.sdist] 92 | include = [ 93 | "fakeredis", 94 | "LICENSE", 95 | "test", 96 | ] 97 | 98 | [tool.hatch.build.targets.wheel] 99 | include = [ 100 | "fakeredis", 101 | "LICENSE", 102 | ] 103 | 104 | [tool.hatch.build.targets.wheel.sources] 105 | LICENSE = "fakeredis/LICENSE" 106 | 107 | [tool.pytest.ini_options] 108 | asyncio_default_fixture_loop_scope = "function" 109 | markers = [ 110 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 111 | "fake: run tests only with fake redis", 112 | "real: run tests with a locally running real Redis server", 113 | "disconnected", 114 | "min_server", 115 | "max_server", 116 | "decode_responses", 117 | "unsupported_server_types", 118 | "resp2_only: run tests only with RESP2", 119 | "resp3_only: run tests only with RESP3", 120 | ] 121 | asyncio_mode = "strict" 122 | generate_report_on_test = true 123 | 124 | [tool.mypy] 125 | packages = ['fakeredis', ] 126 | strict = true 127 | follow_imports = "silent" 128 | ignore_missing_imports = true 129 | scripts_are_modules = true 130 | check_untyped_defs = true 131 | 132 | [tool.ruff] 133 | line-length = 120 134 | exclude = [ 135 | '.venv', 136 | '__pycache__', 137 | ] 138 | 139 | [tool.ruff.format] 140 | quote-style = "double" 141 | indent-style = "space" 142 | skip-magic-trailing-comma = false 143 | line-ending = "auto" 144 | -------------------------------------------------------------------------------- /redis-conf/redis-stack.conf: -------------------------------------------------------------------------------- 1 | aclfile /etc/redis/users.acl 2 | 3 | -------------------------------------------------------------------------------- /redis-conf/users.acl: -------------------------------------------------------------------------------- 1 | user default on nopass sanitize-payload ~* &* +@all 2 | -------------------------------------------------------------------------------- /scripts/generate_command_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generates a JSON file with the following structure: 3 | { 4 | "command_name": [ 5 | 1. Name 6 | 2. Arity 7 | 3. Flags 8 | 4. First key 9 | 5. Last key 10 | 6. Step 11 | 7. ACL categories (as of Redis 6.0) 12 | 8. Tips (as of Redis 7.0) 13 | 9. Key specifications (as of Redis 7.0) 14 | 10. Subcommands (as of Redis 7.0) 15 | ] 16 | } 17 | that is used for the `COMMAND` redis command. 18 | """ 19 | 20 | import json 21 | import os 22 | from typing import Any, List, Dict 23 | 24 | from fakeredis._commands import SUPPORTED_COMMANDS 25 | from scripts.generate_supported_commands_doc import METADATA, download_single_stack_commands 26 | 27 | THIS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) 28 | 29 | CATEGORIES = { 30 | "BF.": "@bloom", 31 | "CF.": "@cuckoo", 32 | "CMS.": "@cms", 33 | "TOPK.": "@topk", 34 | "TS.": "@timeseries", 35 | "JSON.": "@json", 36 | "FT.": "@search", 37 | "TDIGEST.": "@tdigest", 38 | } 39 | 40 | 41 | def implemented_commands() -> set: 42 | res = set(SUPPORTED_COMMANDS.keys()) 43 | if "json.type" not in res: 44 | raise ValueError("Make sure jsonpath_ng is installed to get accurate documentation") 45 | if "eval" not in res: 46 | raise ValueError("Make sure lupa is installed to get accurate documentation") 47 | return res 48 | 49 | 50 | def dict_deep_get(d: Dict[Any, Any], *keys, default_value: Any = None) -> Any: 51 | res = d 52 | for key in keys: 53 | if isinstance(res, list) and isinstance(key, int): 54 | res = res[key] 55 | else: 56 | res = res.get(key, None) 57 | if res is None: 58 | return default_value 59 | return default_value if res is None else res 60 | 61 | 62 | def key_specs_array(cmd_info: Dict[str, Any]) -> List[Any]: 63 | return [] 64 | 65 | 66 | def get_command_info(cmd_name: str, all_commands: Dict[str, Any]) -> List[Any]: 67 | """Returns a list 68 | 1 Name // 69 | 2 Arity // 70 | 3 Flags // 71 | 4 First key // 72 | 5 Last key // 73 | 6 Step // 74 | 7 ACL categories (as of Redis 6.0) // 75 | 8 Tips (as of Redis 7.0) // 76 | 9 Key specifications (as of Redis 7.0) 77 | 10 Subcommands (as of Redis 7.0) 78 | """ 79 | print(f"Command {cmd_name}") 80 | cmd_info = all_commands[cmd_name] 81 | first_key = dict_deep_get(cmd_info, "key_specs", 0, "begin_search", "spec", "index", default_value=0) 82 | last_key = dict_deep_get(cmd_info, "key_specs", -1, "begin_search", "spec", "index", default_value=0) 83 | step = dict_deep_get(cmd_info, "key_specs", 0, "find_keys", "spec", "keystep", default_value=0) 84 | tips = [] # todo 85 | subcommands = [ 86 | get_command_info(cmd, all_commands) for cmd in all_commands if cmd_name != cmd and cmd.startswith(cmd_name) 87 | ] # todo 88 | categories = set(cmd_info.get("acl_categories", [])) 89 | for prefix, category in CATEGORIES.items(): 90 | if cmd_name.startswith(prefix.lower()): 91 | categories.add(category) 92 | res = [ 93 | cmd_name.lower().replace(" ", "|"), 94 | cmd_info.get("arity", -1), 95 | cmd_info.get("command_flags", []), 96 | first_key, 97 | last_key, 98 | step, 99 | list(categories), 100 | tips, 101 | key_specs_array(cmd_info), 102 | subcommands, 103 | ] 104 | return res 105 | 106 | 107 | if __name__ == "__main__": 108 | implemented = implemented_commands() 109 | command_info_dict: Dict[str, List[Any]] = dict() 110 | for cmd_meta in METADATA: 111 | cmds = download_single_stack_commands(cmd_meta.local_filename, cmd_meta.url) 112 | for cmd in cmds: 113 | if cmd not in implemented: 114 | continue 115 | command_info_dict[cmd] = get_command_info(cmd, cmds) 116 | subcommand = cmd.split(" ") 117 | if len(subcommand) > 1: 118 | ( 119 | command_info_dict.setdefault(subcommand[0], [subcommand[0], -1, [], 0, 0, 0, [], [], [], []])[ 120 | 9 121 | ].append(command_info_dict[cmd]) 122 | ) 123 | 124 | print(command_info_dict[cmd]) 125 | with open(os.path.join(os.path.dirname(__file__), "..", "fakeredis", "commands.json"), "w") as f: 126 | json.dump(command_info_dict, f) 127 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/test/__init__.py -------------------------------------------------------------------------------- /test/test_hypothesis/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "TestConnection", 3 | "TestHash", 4 | "TestList", 5 | "TestServer", 6 | "TestSet", 7 | "TestString", 8 | "TestTransaction", 9 | "TestZSet", 10 | "TestZSetNoScores", 11 | ] 12 | 13 | from test.test_hypothesis.test_connection import TestConnection 14 | from test.test_hypothesis.test_hash import TestHash 15 | from test.test_hypothesis.test_list import TestList 16 | from test.test_hypothesis.test_server import TestServer 17 | from test.test_hypothesis.test_set import TestSet 18 | from test.test_hypothesis.test_string import TestString 19 | from test.test_hypothesis.test_transaction import TestTransaction 20 | from test.test_hypothesis.test_zset import TestZSet, TestZSetNoScores 21 | -------------------------------------------------------------------------------- /test/test_hypothesis/_server_info.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Union 2 | 3 | import pytest 4 | import redis 5 | 6 | from fakeredis._server import _create_version 7 | 8 | 9 | def server_info() -> Tuple[str, Union[None, Tuple[int, ...]]]: 10 | """Returns server's version or None if server is not running""" 11 | client = None 12 | try: 13 | client = redis.Redis("localhost", port=6390, db=2) 14 | client_info = client.info() 15 | server_type = "dragonfly" if "dragonfly_version" in client_info else "redis" 16 | server_version = client_info["redis_version"] if server_type != "dragonfly" else (7, 0) 17 | server_version = _create_version(server_version) or (7,) 18 | return server_type, server_version 19 | except redis.ConnectionError as e: 20 | print(e) 21 | pytest.exit("Redis is not running") 22 | return "redis", (6,) 23 | finally: 24 | if hasattr(client, "close"): 25 | client.close() # Absent in older versions of redis-py 26 | 27 | 28 | server_type, redis_ver = server_info() 29 | floats_kwargs = dict() 30 | 31 | if server_type == "dragonfly": 32 | floats_kwargs = dict(allow_nan=False, allow_subnormal=False, allow_infinity=False) 33 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_connection.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | BaseTest, 3 | commands, 4 | values, 5 | st, 6 | dbnums, 7 | common_commands, 8 | ) 9 | 10 | 11 | class TestConnection(BaseTest): 12 | # TODO: tests for select 13 | connection_commands = commands(st.just("echo"), values) | commands(st.just("ping"), st.lists(values, max_size=2)) 14 | command_strategy_redis_only = commands(st.just("swapdb"), dbnums, dbnums) 15 | command_strategy = connection_commands | common_commands 16 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_hash.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | BaseTest, 3 | commands, 4 | values, 5 | st, 6 | keys, 7 | common_commands, 8 | zero_or_more, 9 | expires_seconds, 10 | expires_ms, 11 | fields, 12 | ints, 13 | ) 14 | 15 | 16 | class TestHash(BaseTest): 17 | hash_commands = ( 18 | commands(st.just("hset"), keys, st.lists(st.tuples(fields, values))) 19 | | commands(st.just("hdel"), keys, st.lists(fields)) 20 | | commands(st.just("hexists"), keys, fields) 21 | | commands(st.just("hget"), keys, fields) 22 | | commands(st.sampled_from(["hgetall", "hkeys", "hvals"]), keys) 23 | | commands(st.just("hincrby"), keys, fields, ints) 24 | | commands(st.just("hlen"), keys) 25 | | commands(st.just("hmget"), keys, st.lists(fields)) 26 | | commands(st.just("hset"), keys, st.lists(st.tuples(fields, values))) 27 | | commands(st.just("hsetnx"), keys, fields, values) 28 | | commands(st.just("hstrlen"), keys, fields) 29 | | commands( 30 | st.just("hpersist"), 31 | st.just("fields"), 32 | st.just(2), 33 | st.lists(fields, min_size=2, max_size=2), 34 | ) 35 | | commands( 36 | st.just("hexpire"), 37 | keys, 38 | expires_seconds, 39 | st.just("fields"), 40 | st.just(2), 41 | st.lists(fields, min_size=2, max_size=2, unique=True), 42 | ) 43 | ) 44 | command_strategy_redis7 = ( 45 | commands(st.just("hpersist"), st.just("fields"), st.just(2), st.lists(fields, min_size=2, max_size=2)) 46 | | commands(st.just("hexpiretime"), st.just("fields"), st.just(2), st.lists(fields, min_size=2, max_size=2)) 47 | | commands(st.just("hpexpiretime"), st.just("fields"), st.just(2), st.lists(fields, min_size=2, max_size=2)) 48 | | commands( 49 | st.just("hexpire"), 50 | keys, 51 | expires_seconds, 52 | *zero_or_more("nx", "xx", "gt", "lt"), 53 | st.just("fields"), 54 | st.just(2), 55 | st.lists(fields, min_size=2, max_size=2, unique=True), 56 | ) 57 | | commands( 58 | st.just("hpexpire"), 59 | keys, 60 | expires_ms, 61 | *zero_or_more("nx", "xx", "gt", "lt"), 62 | st.just("fields"), 63 | st.just(2), 64 | st.lists(fields, min_size=2, max_size=2, unique=True), 65 | ) 66 | ) 67 | create_command_strategy = commands(st.just("hset"), keys, st.lists(st.tuples(fields, values), min_size=1)) 68 | command_strategy = hash_commands | common_commands 69 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_list.py: -------------------------------------------------------------------------------- 1 | import hypothesis.strategies as st 2 | 3 | from .base import ( 4 | BaseTest, 5 | commands, 6 | values, 7 | keys, 8 | common_commands, 9 | counts, 10 | ints, 11 | ) 12 | 13 | 14 | class TestList(BaseTest): 15 | # TODO: blocking commands 16 | list_commands = ( 17 | commands(st.just("lindex"), keys, counts) 18 | | commands( 19 | st.just("linsert"), 20 | keys, 21 | st.sampled_from(["before", "after", "BEFORE", "AFTER"]) | st.binary(), 22 | values, 23 | values, 24 | ) 25 | | commands(st.just("llen"), keys) 26 | | commands(st.sampled_from(["lpop", "rpop"]), keys, st.just(None) | st.just([]) | ints) 27 | | commands(st.sampled_from(["lpush", "lpushx", "rpush", "rpushx"]), keys, st.lists(values)) 28 | | commands(st.just("lrange"), keys, counts, counts) 29 | | commands(st.just("lrem"), keys, counts, values) 30 | | commands(st.just("lset"), keys, counts, values) 31 | | commands(st.just("ltrim"), keys, counts, counts) 32 | | commands(st.just("rpoplpush"), keys, keys) 33 | ) 34 | create_command_strategy = commands(st.just("rpush"), keys, st.lists(values, min_size=1)) 35 | command_strategy = list_commands | common_commands 36 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_server.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTest, commands, st, common_commands, keys, values 2 | from .test_string import string_commands 3 | 4 | 5 | class TestServer(BaseTest): 6 | # TODO: real redis raises an error if there is a save already in progress. 7 | # Find a better way to test this. commands(st.just('bgsave')) 8 | server_commands = ( 9 | commands(st.just("dbsize")) 10 | | commands(st.sampled_from(["flushdb", "flushall"])) 11 | # TODO: result is non-deterministic 12 | # | commands(st.just('lastsave')) 13 | | commands(st.just("save")) 14 | ) 15 | command_strategy_redis_only = commands(st.sampled_from(["flushdb", "flushall"]), st.sampled_from([[], "async"])) 16 | create_command_strategy = commands(st.just("set"), keys, values) 17 | command_strategy = server_commands | string_commands | common_commands 18 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_set.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTest, st, commands, keys, fields, common_commands 2 | 3 | 4 | class TestSet(BaseTest): 5 | set_commands = ( 6 | commands( 7 | st.just("sadd"), 8 | keys, 9 | st.lists( 10 | fields, 11 | ), 12 | ) 13 | | commands(st.just("scard"), keys) 14 | | commands(st.sampled_from(["sdiff", "sinter", "sunion"]), st.lists(keys)) 15 | | commands(st.sampled_from(["sdiffstore", "sinterstore", "sunionstore"]), keys, st.lists(keys)) 16 | | commands(st.just("sismember"), keys, fields) 17 | | commands(st.just("smembers"), keys) 18 | | commands(st.just("smove"), keys, keys, fields) 19 | | commands(st.just("srem"), keys, st.lists(fields)) 20 | ) 21 | # TODO: 22 | # - find a way to test srandmember, spop which are random 23 | # - sscan 24 | create_command_strategy = commands(st.just("sadd"), keys, st.lists(fields, min_size=1)) 25 | command_strategy = set_commands | common_commands 26 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_string.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | BaseTest, 3 | commands, 4 | values, 5 | st, 6 | keys, 7 | common_commands, 8 | counts, 9 | zero_or_more, 10 | expires_seconds, 11 | expires_ms, 12 | ints, 13 | int_as_bytes, 14 | ) 15 | 16 | optional_bitcount_range = st.just(()) | st.tuples(int_as_bytes, int_as_bytes) 17 | str_len = st.integers(min_value=-3, max_value=3) | st.integers(min_value=-3000, max_value=3000) 18 | 19 | string_commands = ( 20 | commands(st.just("append"), keys, values) 21 | | commands(st.just("bitcount"), keys, optional_bitcount_range) 22 | | commands(st.sampled_from(["incr", "decr"]), keys) 23 | | commands(st.sampled_from(["incrby", "decrby"]), keys, values) 24 | | commands(st.just("get"), keys) 25 | | commands(st.just("getbit"), keys, counts) 26 | | commands(st.just("setbit"), keys, counts, st.integers(min_value=0, max_value=1) | ints) 27 | | commands(st.sampled_from(["substr", "getrange"]), keys, str_len, counts) 28 | | commands(st.just("getset"), keys, values) 29 | | commands(st.just("mget"), st.lists(keys)) 30 | | commands(st.sampled_from(["mset", "msetnx"]), st.lists(st.tuples(keys, values))) 31 | | commands(st.just("set"), keys, values, *zero_or_more("nx", "xx", "keepttl")) 32 | | commands(st.just("setex"), keys, expires_seconds, values) 33 | | commands(st.just("psetex"), keys, expires_ms, values) 34 | | commands(st.just("setnx"), keys, values) 35 | | commands(st.just("setrange"), keys, str_len, values) 36 | | commands(st.just("strlen"), keys) 37 | ) 38 | 39 | 40 | class TestString(BaseTest): 41 | create_command_strategy = commands(st.just("set"), keys, values) 42 | command_strategy = string_commands | common_commands 43 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_transaction.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | BaseTest, 3 | commands, 4 | st, 5 | keys, 6 | values, 7 | common_commands, 8 | zero_or_more, 9 | expires_seconds, 10 | expires_ms, 11 | counts, 12 | ints, 13 | ) 14 | 15 | 16 | class TestTransaction(BaseTest): 17 | transaction_commands = ( 18 | commands(st.sampled_from(["multi", "discard", "exec", "unwatch"])) 19 | | commands(st.just("watch"), keys) 20 | | commands(st.just("append"), keys, values) 21 | | commands(st.just("bitcount"), keys) 22 | | commands(st.just("bitcount"), keys, values, values) 23 | | commands(st.sampled_from(["incr", "decr"]), keys) 24 | | commands(st.sampled_from(["incrby", "decrby"]), keys, values) 25 | | commands(st.just("get"), keys) 26 | | commands(st.just("getbit"), keys, counts) 27 | | commands(st.just("setbit"), keys, counts, st.integers(min_value=0, max_value=1) | ints) 28 | | commands(st.sampled_from(["substr", "getrange"]), keys, counts, counts) 29 | | commands(st.just("getset"), keys, values) 30 | | commands(st.just("mget"), st.lists(keys)) 31 | | commands(st.sampled_from(["mset", "msetnx"]), st.lists(st.tuples(keys, values))) 32 | | commands( 33 | st.just("set"), 34 | keys, 35 | values, 36 | *zero_or_more("nx", "xx", "keepttl"), 37 | ) 38 | | commands(st.just("setex"), keys, expires_seconds, values) 39 | | commands(st.just("psetex"), keys, expires_ms, values) 40 | | commands(st.just("setnx"), keys, values) 41 | | commands(st.just("setrange"), keys, counts, values) 42 | | commands(st.just("strlen"), keys) 43 | ) 44 | create_command_strategy = commands(st.just("set"), keys, values) 45 | command_strategy = transaction_commands | common_commands 46 | -------------------------------------------------------------------------------- /test/test_hypothesis/test_zset.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | BaseTest, 3 | scores, 4 | fields, 5 | commands, 6 | st, 7 | keys, 8 | common_commands, 9 | counts, 10 | zero_or_more, 11 | optional, 12 | string_tests, 13 | float_as_bytes, 14 | Command, 15 | ) 16 | 17 | limits = st.just(()) | st.tuples(st.just("limit"), counts, counts) 18 | score_tests = scores | st.builds(lambda x: b"(" + repr(x).encode(), scores) 19 | zset_no_score_create_commands = commands(st.just("zadd"), keys, st.lists(st.tuples(st.just(0), fields), min_size=1)) 20 | zset_no_score_commands = ( # TODO: test incr 21 | commands( 22 | st.just("zadd"), 23 | keys, 24 | *zero_or_more("nx", "xx", "ch", "incr"), 25 | st.lists(st.tuples(st.just(0), fields)), 26 | ) 27 | | commands(st.just("zlexcount"), keys, string_tests, string_tests) 28 | | commands(st.sampled_from(["zrangebylex", "zrevrangebylex"]), keys, string_tests, string_tests, limits) 29 | | commands(st.just("zremrangebylex"), keys, string_tests, string_tests) 30 | ) 31 | 32 | 33 | def build_zstore(command, dest, sources, weights, aggregate) -> Command: 34 | args = [command, dest, len(sources)] 35 | args += [source[0] for source in sources] 36 | if weights: 37 | args.append("weights") 38 | args += [source[1] for source in sources] 39 | if aggregate: 40 | args += ["aggregate", aggregate] 41 | return Command(args) 42 | 43 | 44 | class TestZSet(BaseTest): 45 | zset_commands = ( 46 | commands( 47 | st.just("zadd"), 48 | keys, 49 | *zero_or_more("nx", "xx", "ch", "incr"), 50 | st.lists(st.tuples(scores, fields)), 51 | ) 52 | | commands(st.just("zcard"), keys) 53 | | commands(st.just("zcount"), keys, score_tests, score_tests) 54 | | commands(st.just("zincrby"), keys, scores, fields) 55 | | commands(st.sampled_from(["zrange", "zrevrange"]), keys, counts, counts, optional("withscores")) 56 | | commands( 57 | st.sampled_from(["zrangebyscore", "zrevrangebyscore"]), 58 | keys, 59 | score_tests, 60 | score_tests, 61 | limits, 62 | optional("withscores"), 63 | ) 64 | | commands(st.sampled_from(["zrank", "zrevrank"]), keys, fields) 65 | | commands(st.just("zrem"), keys, st.lists(fields)) 66 | | commands(st.just("zremrangebyrank"), keys, counts, counts) 67 | | commands(st.just("zremrangebyscore"), keys, score_tests, score_tests) 68 | | commands(st.just("zscore"), keys, fields) 69 | | st.builds( 70 | build_zstore, 71 | command=st.sampled_from(["zunionstore", "zinterstore"]), 72 | dest=keys, 73 | sources=st.lists(st.tuples(keys, float_as_bytes)), 74 | weights=st.booleans(), 75 | aggregate=st.sampled_from([None, "sum", "min", "max"]), 76 | ) 77 | ) 78 | # TODO: zscan, zpopmin/zpopmax, bzpopmin/bzpopmax, probably more 79 | create_command_strategy = commands(st.just("zadd"), keys, st.lists(st.tuples(scores, fields), min_size=1)) 80 | command_strategy = zset_commands | common_commands 81 | 82 | 83 | class TestZSetNoScores(BaseTest): 84 | create_command_strategy = zset_no_score_create_commands 85 | command_strategy = zset_no_score_commands | common_commands 86 | -------------------------------------------------------------------------------- /test/test_hypothesis_joint.py: -------------------------------------------------------------------------------- 1 | import hypothesis.strategies as st 2 | 3 | from . import test_hypothesis as tests 4 | from .test_hypothesis.base import BaseTest, common_commands, commands 5 | from .test_hypothesis.test_string import string_commands 6 | 7 | bad_commands = ( 8 | # redis-py splits the command on spaces, and hangs if that ends up being an empty list 9 | commands(st.text().filter(lambda x: bool(x.split())), st.lists(st.binary() | st.text())) 10 | ) 11 | 12 | 13 | class TestJoint(BaseTest): 14 | create_command_strategy = ( 15 | tests.TestString.create_command_strategy 16 | | tests.TestHash.create_command_strategy 17 | | tests.TestList.create_command_strategy 18 | | tests.TestSet.create_command_strategy 19 | | tests.TestZSet.create_command_strategy 20 | ) 21 | command_strategy = ( 22 | tests.TestServer.server_commands 23 | | tests.TestConnection.connection_commands 24 | | string_commands 25 | | tests.TestHash.hash_commands 26 | | tests.TestList.list_commands 27 | | tests.TestSet.set_commands 28 | | tests.TestZSet.zset_commands 29 | | common_commands 30 | | bad_commands 31 | ) 32 | -------------------------------------------------------------------------------- /test/test_internals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/test/test_internals/__init__.py -------------------------------------------------------------------------------- /test/test_internals/test_acl_save_load.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from fakeredis import FakeServer, FakeStrictRedis 4 | 5 | 6 | def test_acl_save_load(): 7 | acl_filename = b"./users.acl" 8 | server = FakeServer(config={b"aclfile": acl_filename}) 9 | r = FakeStrictRedis(server=server) 10 | username = "fakeredis-user" 11 | assert r.acl_setuser( 12 | username, 13 | enabled=True, 14 | reset=True, 15 | passwords=["+pass1", "+pass2"], 16 | categories=["+set", "+@hash", "-geo"], 17 | commands=["+get", "+mget", "-hset"], 18 | keys=["cache:*", "objects:*"], 19 | channels=["message:*"], 20 | selectors=[("+set", "%W~app*"), ("+get", "%RW~app* &x"), ("-hset", "%W~app*")], 21 | ) 22 | r.acl_save() 23 | 24 | # assert acl file contains all data 25 | with open(acl_filename, "r") as f: 26 | lines = f.readlines() 27 | assert len(lines) == 2 28 | user_rule = lines[1] 29 | assert user_rule.startswith("user fakeredis-user") 30 | assert "nopass" not in user_rule 31 | assert "#e6c3da5b206634d7f3f3586d747ffdb36b5c675757b380c6a5fe5c570c714349" in user_rule 32 | assert "#1ba3d16e9881959f8c9a9762854f72c6e6321cdd44358a10a4e939033117eab9" in user_rule 33 | assert "on" in user_rule 34 | assert "~cache:*" in user_rule 35 | assert "~objects:*" in user_rule 36 | assert "resetchannels &message:*" in user_rule 37 | assert "(%W~app* resetchannels -@all +set)" in user_rule 38 | assert "(~app* resetchannels &x -@all +get)" in user_rule 39 | assert "(%W~app* resetchannels -@all -hset)" in user_rule 40 | 41 | # assert acl file is loaded correctly 42 | server2 = FakeServer(config={b"aclfile": acl_filename}) 43 | r2 = FakeStrictRedis(server=server2) 44 | r2.acl_load() 45 | rules = r2.acl_list() 46 | user_rule = next(filter(lambda x: x.startswith(f"user {username}"), rules), None) 47 | assert user_rule.startswith("user fakeredis-user") 48 | assert "nopass" not in user_rule 49 | assert "#e6c3da5b206634d7f3f3586d747ffdb36b5c675757b380c6a5fe5c570c714349" in user_rule 50 | assert "#1ba3d16e9881959f8c9a9762854f72c6e6321cdd44358a10a4e939033117eab9" in user_rule 51 | assert "on" in user_rule 52 | assert "~cache:*" in user_rule 53 | assert "~objects:*" in user_rule 54 | assert "resetchannels &message:*" in user_rule 55 | assert "(%W~app* resetchannels -@all +set)" in user_rule 56 | assert "(~app* resetchannels &x -@all +get)" in user_rule 57 | assert "(%W~app* resetchannels -@all -hset)" in user_rule 58 | 59 | os.remove(acl_filename) 60 | -------------------------------------------------------------------------------- /test/test_internals/test_asyncredis.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | import redis 5 | import redis.asyncio 6 | 7 | from fakeredis import FakeServer, aioredis, FakeAsyncRedis, FakeStrictRedis 8 | from test import testtools 9 | 10 | pytestmark = [] 11 | fake_only = pytest.mark.parametrize("async_redis", [pytest.param("fake", marks=pytest.mark.fake)], indirect=True) 12 | pytestmark.extend( 13 | [ 14 | pytest.mark.asyncio, 15 | ] 16 | ) 17 | 18 | 19 | @fake_only 20 | @testtools.run_test_if_redispy_ver("gte", "5.1") 21 | async def test_repr_redis_51(async_redis: redis.asyncio.Redis): 22 | assert re.fullmatch( 23 | r",db=0\)>\)>", 25 | repr(async_redis.connection_pool), 26 | ) 27 | 28 | 29 | @fake_only 30 | @pytest.mark.disconnected 31 | async def test_not_connected(async_redis: redis.asyncio.Redis): 32 | with pytest.raises(redis.asyncio.ConnectionError): 33 | await async_redis.ping() 34 | 35 | 36 | @fake_only 37 | async def test_disconnect_server(async_redis, fake_server): 38 | await async_redis.ping() 39 | fake_server.connected = False 40 | with pytest.raises(redis.asyncio.ConnectionError): 41 | await async_redis.ping() 42 | fake_server.connected = True 43 | 44 | 45 | @pytest.mark.fake 46 | async def test_from_url(): 47 | r0 = aioredis.FakeRedis.from_url("redis://localhost?db=0") 48 | r1 = aioredis.FakeRedis.from_url("redis://localhost?db=1") 49 | # Check that they are indeed different databases 50 | await r0.set("foo", "a") 51 | await r1.set("foo", "b") 52 | assert await r0.get("foo") == b"a" 53 | assert await r1.get("foo") == b"b" 54 | await r0.connection_pool.disconnect() 55 | await r1.connection_pool.disconnect() 56 | 57 | 58 | @pytest.mark.fake 59 | async def test_from_url_with_version(): 60 | r0 = aioredis.FakeRedis.from_url("redis://localhost?db=0", version=(6,)) 61 | r1 = aioredis.FakeRedis.from_url("redis://localhost?db=1", version=(6,)) 62 | # Check that they are indeed different databases 63 | await r0.set("foo", "a") 64 | await r1.set("foo", "b") 65 | assert await r0.get("foo") == b"a" 66 | assert await r1.get("foo") == b"b" 67 | await r0.connection_pool.disconnect() 68 | await r1.connection_pool.disconnect() 69 | 70 | 71 | @fake_only 72 | async def test_from_url_with_server(async_redis, fake_server): 73 | r2 = aioredis.FakeRedis.from_url("redis://localhost", server=fake_server) 74 | await async_redis.set("foo", "bar") 75 | assert await r2.get("foo") == b"bar" 76 | await r2.connection_pool.disconnect() 77 | 78 | 79 | @pytest.mark.fake 80 | async def test_without_server(): 81 | r = aioredis.FakeRedis() 82 | assert await r.ping() 83 | 84 | 85 | @pytest.mark.fake 86 | async def test_without_server_disconnected(): 87 | r = aioredis.FakeRedis(connected=False) 88 | with pytest.raises(redis.asyncio.ConnectionError): 89 | await r.ping() 90 | 91 | 92 | @pytest.mark.fake 93 | async def test_async(): 94 | # arrange 95 | cache = aioredis.FakeRedis() 96 | # act 97 | await cache.set("fakeredis", "plz") 98 | x = await cache.get("fakeredis") 99 | # assert 100 | assert x == b"plz" 101 | 102 | 103 | @testtools.run_test_if_redispy_ver("gte", "4.4.0") 104 | @pytest.mark.parametrize("nowait", [False, True]) 105 | @pytest.mark.fake 106 | async def test_connection_disconnect(nowait): 107 | server = FakeServer() 108 | r = aioredis.FakeRedis(server=server) 109 | conn = await r.connection_pool.get_connection("_") 110 | assert conn is not None 111 | 112 | await conn.disconnect(nowait=nowait) 113 | 114 | assert conn._sock is None 115 | 116 | 117 | @pytest.mark.fake 118 | async def test_init_args(): 119 | sync_r1 = FakeStrictRedis() 120 | r1 = FakeAsyncRedis() 121 | r5 = FakeAsyncRedis() 122 | r2 = FakeAsyncRedis(server=FakeServer()) 123 | 124 | shared_server = FakeServer() 125 | r3 = FakeAsyncRedis(server=shared_server) 126 | r4 = FakeAsyncRedis(server=shared_server) 127 | 128 | await r1.set("foo", "bar") 129 | await r3.set("bar", "baz") 130 | 131 | assert await r1.get("foo") == b"bar" 132 | assert await r5.get("foo") is None 133 | assert sync_r1.get("foo") is None 134 | assert await r2.get("foo") is None 135 | assert await r3.get("foo") is None 136 | 137 | assert await r3.get("bar") == b"baz" 138 | assert await r4.get("bar") == b"baz" 139 | assert await r1.get("bar") is None 140 | -------------------------------------------------------------------------------- /test/test_internals/test_extract_args.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fakeredis._command_args_parsing import extract_args 4 | from fakeredis._helpers import SimpleError 5 | 6 | 7 | def test_extract_args(): 8 | args = ( 9 | b"nx", 10 | b"ex", 11 | b"324", 12 | b"xx", 13 | ) 14 | (xx, nx, ex, keepttl), _ = extract_args(args, ("nx", "xx", "+ex", "keepttl")) 15 | assert xx 16 | assert nx 17 | assert ex == 324 18 | assert not keepttl 19 | 20 | 21 | def test_extract_args__should_raise_error(): 22 | args = (b"nx", b"ex", b"324", b"xx", b"something") 23 | with pytest.raises(SimpleError): 24 | _, _ = extract_args(args, ("nx", "xx", "+ex", "keepttl")) 25 | 26 | 27 | def test_extract_args__should_return_something(): 28 | args = (b"nx", b"ex", b"324", b"xx", b"something") 29 | 30 | (xx, nx, ex, keepttl), left = extract_args(args, ("nx", "xx", "+ex", "keepttl"), error_on_unexpected=False) 31 | assert xx 32 | assert nx 33 | assert ex == 324 34 | assert not keepttl 35 | assert left == (b"something",) 36 | 37 | args = ( 38 | b"nx", 39 | b"something", 40 | b"ex", 41 | b"324", 42 | b"xx", 43 | ) 44 | 45 | (xx, nx, ex, keepttl), left = extract_args( 46 | args, ("nx", "xx", "+ex", "keepttl"), error_on_unexpected=False, left_from_first_unexpected=False 47 | ) 48 | assert xx 49 | assert nx 50 | assert ex == 324 51 | assert not keepttl 52 | assert left == [ 53 | b"something", 54 | ] 55 | 56 | 57 | def test_extract_args__multiple_numbers(): 58 | args = ( 59 | b"nx", 60 | b"limit", 61 | b"324", 62 | b"123", 63 | b"xx", 64 | ) 65 | 66 | (xx, nx, limit, keepttl), _ = extract_args(args, ("nx", "xx", "++limit", "keepttl")) 67 | assert xx 68 | assert nx 69 | assert limit == [324, 123] 70 | assert not keepttl 71 | 72 | (xx, nx, limit, keepttl), _ = extract_args( 73 | ( 74 | b"nx", 75 | b"xx", 76 | ), 77 | ("nx", "xx", "++limit", "keepttl"), 78 | ) 79 | assert xx 80 | assert nx 81 | assert not keepttl 82 | assert limit == [None, None] 83 | 84 | 85 | def test_extract_args__extract_non_numbers(): 86 | args = ( 87 | b"by", 88 | b"dd", 89 | b"nx", 90 | b"limit", 91 | b"324", 92 | b"123", 93 | b"xx", 94 | ) 95 | 96 | (xx, nx, limit, sortby), _ = extract_args(args, ("nx", "xx", "++limit", "*by")) 97 | assert xx 98 | assert nx 99 | assert limit == [324, 123] 100 | assert sortby == b"dd" 101 | 102 | 103 | def test_extract_args__extract_maxlen(): 104 | args = (b"MAXLEN", b"5") 105 | (nomkstream, limit, maxlen, maxid), left_args = extract_args( 106 | args, ("nomkstream", "+limit", "~+maxlen", "~maxid"), error_on_unexpected=False 107 | ) 108 | assert not nomkstream 109 | assert limit is None 110 | assert maxlen == 5 111 | assert maxid is None 112 | 113 | args = (b"MAXLEN", b"~", b"5", b"maxid", b"~", b"1") 114 | (nomkstream, limit, maxlen, maxid), left_args = extract_args( 115 | args, ("nomkstream", "+limit", "~+maxlen", "~maxid"), error_on_unexpected=False 116 | ) 117 | assert not nomkstream 118 | assert limit is None 119 | assert maxlen == 5 120 | assert maxid == b"1" 121 | 122 | args = ( 123 | b"by", 124 | b"dd", 125 | b"nx", 126 | b"maxlen", 127 | b"~", 128 | b"10", 129 | b"limit", 130 | b"324", 131 | b"123", 132 | b"xx", 133 | ) 134 | 135 | (nx, maxlen, xx, limit, sortby), _ = extract_args(args, ("nx", "~+maxlen", "xx", "++limit", "*by")) 136 | assert xx 137 | assert nx 138 | assert maxlen == 10 139 | assert limit == [324, 123] 140 | assert sortby == b"dd" 141 | -------------------------------------------------------------------------------- /test/test_internals/test_lua_modules.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import redis 5 | 6 | pytestmark = [] 7 | pytestmark.extend( 8 | [ 9 | pytest.mark.asyncio, 10 | ] 11 | ) 12 | 13 | lua_modules_test = pytest.importorskip("lupa") 14 | 15 | 16 | @pytest.mark.load_lua_modules("cjson") 17 | async def test_async_asgi_ratelimit_script(async_redis: redis.Redis): 18 | script = """ 19 | local ruleset = cjson.decode(ARGV[1]) 20 | 21 | -- Set limits 22 | for i, key in pairs(KEYS) do 23 | redis.call('SET', key, ruleset[key][1], 'EX', ruleset[key][2], 'NX') 24 | end 25 | 26 | -- Check limits 27 | for i = 1, #KEYS do 28 | local value = redis.call('GET', KEYS[i]) 29 | if value and tonumber(value) < 1 then 30 | return ruleset[KEYS[i]][2] 31 | end 32 | end 33 | 34 | -- Decrease limits 35 | for i, key in pairs(KEYS) do 36 | redis.call('DECR', key) 37 | end 38 | return 0 39 | """ 40 | 41 | script = async_redis.register_script(script) 42 | ruleset = {"path:get:user:name": (1, 1)} 43 | await script(keys=list(ruleset.keys()), args=[json.dumps(ruleset)]) 44 | 45 | 46 | @pytest.mark.load_lua_modules("cjson") 47 | def test_asgi_ratelimit_script(r: redis.Redis): 48 | script = """ 49 | local ruleset = cjson.decode(ARGV[1]) 50 | 51 | -- Set limits 52 | for i, key in pairs(KEYS) do 53 | redis.call('SET', key, ruleset[key][1], 'EX', ruleset[key][2], 'NX') 54 | end 55 | 56 | -- Check limits 57 | for i = 1, #KEYS do 58 | local value = redis.call('GET', KEYS[i]) 59 | if value and tonumber(value) < 1 then 60 | return ruleset[KEYS[i]][2] 61 | end 62 | end 63 | 64 | -- Decrease limits 65 | for i, key in pairs(KEYS) do 66 | redis.call('DECR', key) 67 | end 68 | return 0 69 | """ 70 | 71 | script = r.register_script(script) 72 | ruleset = {"path:get:user:name": (1, 1)} 73 | script(keys=list(ruleset.keys()), args=[json.dumps(ruleset)]) 74 | -------------------------------------------------------------------------------- /test/test_internals/test_mock.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import redis 4 | 5 | from fakeredis import FakeRedis 6 | 7 | 8 | def test_mock(): 9 | # Mock Redis connection 10 | def bar(redis_host: str, redis_port: int): 11 | redis.Redis(redis_host, redis_port) 12 | 13 | with patch("redis.Redis", FakeRedis): 14 | # Call function 15 | bar("localhost", 6000) 16 | 17 | # Related to #36 - this should fail if mocking Redis does not work 18 | -------------------------------------------------------------------------------- /test/test_internals/test_transactions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | import fakeredis 6 | 7 | 8 | @pytest.mark.fake 9 | def test_get_within_pipeline_w_host(): 10 | r = fakeredis.FakeRedis("localhost") 11 | r.set("test", "foo") 12 | r.set("test2", "foo2") 13 | expected_keys = set(r.keys()) 14 | with r.pipeline() as p: 15 | assert set(r.keys()) == expected_keys 16 | p.watch("test") 17 | assert set(r.keys()) == expected_keys 18 | 19 | 20 | @pytest.mark.fake 21 | def test_get_within_pipeline_no_args(): 22 | r = fakeredis.FakeRedis() 23 | r.set("test", "foo") 24 | r.set("test2", "foo2") 25 | expected_keys = set(r.keys()) 26 | with r.pipeline() as p: 27 | assert set(r.keys()) == expected_keys 28 | p.watch("test") 29 | assert set(r.keys()) == expected_keys 30 | 31 | 32 | @pytest.mark.fake 33 | def test_socket_cleanup_watch(fake_server): 34 | r1 = fakeredis.FakeStrictRedis(server=fake_server) 35 | r2 = fakeredis.FakeStrictRedis(server=fake_server) 36 | pipeline = r1.pipeline(transaction=False) 37 | # This needs some poking into redis-py internals to ensure that we reach 38 | # FakeSocket._cleanup. We need to close the socket while there is still 39 | # a watch in place, but not allow it to be garbage collected (hence we 40 | # set 'sock' even though it is unused). 41 | with pipeline: 42 | pipeline.watch("test") 43 | sock = pipeline.connection._sock # noqa: F841 44 | pipeline.connection.disconnect() 45 | r2.set("test", "foo") 46 | -------------------------------------------------------------------------------- /test/test_internals/test_xstream.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fakeredis.model import XStream, StreamRangeTest 4 | 5 | 6 | @pytest.mark.fake 7 | def test_xstream(): 8 | stream = XStream() 9 | stream.add([0, 0, 1, 1, 2, 2, 3, 3], "0-1") 10 | stream.add([1, 1, 2, 2, 3, 3, 4, 4], "1-2") 11 | stream.add([2, 2, 3, 3, 4, 4], "1-3") 12 | stream.add([3, 3, 4, 4], "2-1") 13 | stream.add([3, 3, 4, 4], "2-2") 14 | stream.add([3, 3, 4, 4], "3-1") 15 | assert stream.add([3, 3, 4, 4], "4-*") == b"4-0" 16 | assert stream.last_item_key() == b"4-0" 17 | assert stream.add([3, 3, 4, 4], "4-*-*") is None 18 | assert len(stream) == 7 19 | i = iter(stream) 20 | assert next(i) == [b"0-1", [0, 0, 1, 1, 2, 2, 3, 3]] 21 | assert next(i) == [b"1-2", [1, 1, 2, 2, 3, 3, 4, 4]] 22 | assert next(i) == [b"1-3", [2, 2, 3, 3, 4, 4]] 23 | assert next(i) == [b"2-1", [3, 3, 4, 4]] 24 | assert next(i) == [b"2-2", [3, 3, 4, 4]] 25 | 26 | assert stream.find_index_key_as_str("1-2") == (1, True) 27 | assert stream.find_index_key_as_str("0-1") == (0, True) 28 | assert stream.find_index_key_as_str("2-1") == (3, True) 29 | assert stream.find_index_key_as_str("1-4") == (3, False) 30 | 31 | lst = stream.irange(StreamRangeTest.decode(b"0-2"), StreamRangeTest.decode(b"3-0")) 32 | assert len(lst) == 4 33 | 34 | stream = XStream() 35 | assert stream.delete(["1"]) == 0 36 | entry_key: bytes = stream.add([0, 0, 1, 1, 2, 2, 3, 3]) 37 | assert len(stream) == 1 38 | assert ( 39 | stream.delete( 40 | [ 41 | entry_key, 42 | ] 43 | ) 44 | == 1 45 | ) 46 | assert len(stream) == 0 47 | -------------------------------------------------------------------------------- /test/test_json/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/test/test_json/__init__.py -------------------------------------------------------------------------------- /test/test_json/test_json_commands.py: -------------------------------------------------------------------------------- 1 | """Tests for `fakeredis-py`'s emulation of Redis's JSON command subset.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import ( 6 | Any, 7 | Dict, 8 | List, 9 | Tuple, 10 | ) 11 | 12 | import pytest 13 | 14 | json_tests = pytest.importorskip("jsonpath_ng") 15 | 16 | SAMPLE_DATA = { 17 | "a": ["foo"], 18 | "nested1": {"a": ["hello", None, "world"]}, 19 | "nested2": {"a": 31}, 20 | } 21 | 22 | 23 | @pytest.fixture(scope="function") 24 | def json_data() -> Dict[str, Any]: 25 | """A module-scoped "blob" of JSON-encodable data.""" 26 | return { 27 | "L1": { 28 | "a": { 29 | "A1_B1": 10, 30 | "A1_B2": False, 31 | "A1_B3": { 32 | "A1_B3_C1": None, 33 | "A1_B3_C2": [ 34 | "A1_B3_C2_D1_1", 35 | "A1_B3_C2_D1_2", 36 | -19.5, 37 | "A1_B3_C2_D1_4", 38 | "A1_B3_C2_D1_5", 39 | {"A1_B3_C2_D1_6_E1": True}, 40 | ], 41 | "A1_B3_C3": [1], 42 | }, 43 | "A1_B4": {"A1_B4_C1": "foo"}, 44 | } 45 | }, 46 | "L2": { 47 | "a": { 48 | "A2_B1": 20, 49 | "A2_B2": False, 50 | "A2_B3": { 51 | "A2_B3_C1": None, 52 | "A2_B3_C2": [ 53 | "A2_B3_C2_D1_1", 54 | "A2_B3_C2_D1_2", 55 | -37.5, 56 | "A2_B3_C2_D1_4", 57 | "A2_B3_C2_D1_5", 58 | {"A2_B3_C2_D1_6_E1": False}, 59 | ], 60 | "A2_B3_C3": [2], 61 | }, 62 | "A2_B4": {"A2_B4_C1": "bar"}, 63 | } 64 | }, 65 | } 66 | 67 | 68 | def load_types_data(nested_key_name: str) -> Tuple[Dict[str, Any], List[bytes]]: 69 | """Generate a structure with sample of all types""" 70 | type_samples = { 71 | "object": {}, 72 | "array": [], 73 | "string": "str", 74 | "integer": 42, 75 | "number": 1.2, 76 | "boolean": False, 77 | "null": None, 78 | } 79 | jdata = {} 80 | 81 | for k, v in type_samples.items(): 82 | jdata[f"nested_{k}"] = {nested_key_name: v} 83 | 84 | return jdata, [k.encode() for k in type_samples.keys()] 85 | -------------------------------------------------------------------------------- /test/test_mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/test/test_mixins/__init__.py -------------------------------------------------------------------------------- /test/test_mixins/test_redis6.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import redis 3 | import redis.client 4 | 5 | from test import testtools 6 | from test.test_mixins.test_streams_commands import get_stream_message 7 | from test.testtools import raw_command 8 | 9 | 10 | @pytest.mark.max_server("6.2.7") 11 | def test_bitcount_mode_redis6(r: redis.Redis): 12 | r.set("key", "foobar") 13 | with pytest.raises(redis.ResponseError): 14 | r.bitcount("key", start=1, end=1, mode="byte") 15 | with pytest.raises(redis.ResponseError): 16 | r.bitcount("key", start=1, end=1, mode="bit") 17 | with pytest.raises(redis.ResponseError): 18 | raw_command(r, "bitcount", "key", "1", "2", "dsd", "cd") 19 | 20 | 21 | @pytest.mark.max_server("6.2.7") 22 | def test_bitops_mode_redis6(r: redis.Redis): 23 | key = "key:bitpos" 24 | r.set(key, b"\xff\xf0\x00") 25 | with pytest.raises(redis.ResponseError): 26 | assert r.bitpos(key, 0, 8, -1, "bit") == 12 27 | 28 | 29 | @pytest.mark.max_server("7.2") 30 | def test_bitcount_error_v6(r: redis.Redis): 31 | r = raw_command(r, b"BITCOUNT", b"", b"", b"") 32 | assert r == 0 33 | 34 | 35 | @pytest.mark.max_server("6.2.7") 36 | def test_pubsub_help_redis6(r: redis.Redis): 37 | assert testtools.raw_command(r, "PUBSUB HELP") == [ 38 | b"PUBSUB [ [value] [opt] ...]. Subcommands are:", 39 | b"CHANNELS []", 40 | b" Return the currently active channels matching a (default: '*').", 41 | b"NUMPAT", 42 | b" Return number of subscriptions to patterns.", 43 | b"NUMSUB [ ...]", 44 | b" Return the number of subscribers for the specified channels, excluding", 45 | b" pattern subscriptions(default: no channels).", 46 | b"HELP", 47 | b" Prints this help.", 48 | ] 49 | 50 | 51 | @pytest.mark.max_server("6.2.7") 52 | def test_script_exists_redis6(r: redis.Redis): 53 | # test response for no arguments by bypassing the py-redis command 54 | # as it requires at least one argument 55 | assert raw_command(r, "SCRIPT EXISTS") == [] 56 | 57 | # use single character characters for non-existing scripts, as those 58 | # will never be equal to an actual sha1 hash digest 59 | assert r.script_exists("a") == [0] 60 | assert r.script_exists("a", "b", "c", "d", "e", "f") == [0, 0, 0, 0, 0, 0] 61 | 62 | sha1_one = r.script_load("return 'a'") 63 | assert r.script_exists(sha1_one) == [1] 64 | assert r.script_exists(sha1_one, "a") == [1, 0] 65 | assert r.script_exists("a", "b", "c", sha1_one, "e") == [0, 0, 0, 1, 0] 66 | 67 | sha1_two = r.script_load("return 'b'") 68 | assert r.script_exists(sha1_one, sha1_two) == [1, 1] 69 | assert r.script_exists("a", sha1_one, "c", sha1_two, "e", "f") == [0, 1, 0, 1, 0, 0] 70 | 71 | 72 | @pytest.mark.max_server("6.3") 73 | @testtools.run_test_if_redispy_ver("gte", "4.4") 74 | def test_xautoclaim_redis6(r: redis.Redis): 75 | stream, group, consumer1, consumer2 = "stream", "group", "consumer1", "consumer2" 76 | 77 | message_id1 = r.xadd(stream, {"john": "wick"}) 78 | message_id2 = r.xadd(stream, {"johny": "deff"}) 79 | message = get_stream_message(r, stream, message_id1) 80 | r.xgroup_create(stream, group, 0) 81 | 82 | # trying to claim a message that isn't already pending doesn't 83 | # do anything 84 | assert r.xautoclaim(stream, group, consumer2, min_idle_time=0) == [b"0-0", []] 85 | 86 | # read the group as consumer1 to initially claim the messages 87 | r.xreadgroup(group, consumer1, streams={stream: ">"}) 88 | 89 | # claim one message as consumer2 90 | response = r.xautoclaim(stream, group, consumer2, min_idle_time=0, count=1) 91 | assert response[1] == [message] 92 | 93 | # reclaim the messages as consumer1, but use the justid argument 94 | # which only returns message ids 95 | assert r.xautoclaim(stream, group, consumer1, min_idle_time=0, start_id=0, justid=True) == [ 96 | message_id1, 97 | message_id2, 98 | ] 99 | assert r.xautoclaim(stream, group, consumer1, min_idle_time=0, start_id=message_id2, justid=True) == [message_id2] 100 | 101 | 102 | @pytest.mark.min_server("6.2") 103 | @pytest.mark.max_server("6.2.7") 104 | def test_set_get_nx_redis6(r: redis.Redis): 105 | # Note: this will most likely fail on a 7.0 server, based on the docs for SET 106 | with pytest.raises(redis.ResponseError): 107 | raw_command(r, "set", "foo", "bar", "NX", "GET") 108 | 109 | 110 | @pytest.mark.max_server("6.2.7") 111 | def test_zadd_minus_zero_redis6(r: redis.Redis): 112 | # Changing -0 to +0 is ignored 113 | r.zadd("foo", {"a": -0.0}) 114 | r.zadd("foo", {"a": 0.0}) 115 | assert raw_command(r, "zscore", "foo", "a") == b"-0" 116 | -------------------------------------------------------------------------------- /test/test_mixins/test_server_commands.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | from time import sleep 4 | 5 | import pytest 6 | import redis 7 | from redis.exceptions import ResponseError 8 | 9 | from fakeredis._commands import SUPPORTED_COMMANDS 10 | from test import testtools 11 | 12 | 13 | @pytest.mark.unsupported_server_types("dragonfly") 14 | def test_swapdb(r, create_connection): 15 | r1 = create_connection(db=3) 16 | r.set("foo", "abc") 17 | r.set("bar", "xyz") 18 | r1.set("foo", "foo") 19 | r1.set("baz", "baz") 20 | assert r.swapdb(2, 3) 21 | assert r.get("foo") == b"foo" 22 | assert r.get("bar") is None 23 | assert r.get("baz") == b"baz" 24 | assert r1.get("foo") == b"abc" 25 | assert r1.get("bar") == b"xyz" 26 | assert r1.get("baz") is None 27 | 28 | 29 | @pytest.mark.unsupported_server_types("dragonfly") 30 | def test_swapdb_same_db(r: redis.Redis): 31 | assert r.swapdb(1, 1) 32 | 33 | 34 | def test_save(r: redis.Redis): 35 | assert r.save() 36 | 37 | 38 | @pytest.mark.unsupported_server_types("dragonfly") 39 | def test_bgsave(r: redis.Redis): 40 | assert r.bgsave() 41 | time.sleep(0.1) 42 | with pytest.raises(ResponseError): 43 | r.execute_command("BGSAVE", "SCHEDULE", "FOO") 44 | with pytest.raises(ResponseError): 45 | r.execute_command("BGSAVE", "FOO") 46 | 47 | 48 | def test_lastsave(r: redis.Redis): 49 | assert isinstance(r.lastsave(), datetime) 50 | 51 | 52 | @testtools.fake_only 53 | def test_command(r: redis.Redis): 54 | commands_dict = r.command() 55 | one_word_commands = {cmd for cmd in SUPPORTED_COMMANDS if " " not in cmd and SUPPORTED_COMMANDS[cmd].server_types} 56 | server_unsupported_commands = one_word_commands - set(commands_dict.keys()) 57 | server_unsupported_commands = server_unsupported_commands - {"hgetdel", "hgetex", "hsetex"} 58 | for command in server_unsupported_commands: 59 | assert "redis" not in SUPPORTED_COMMANDS[command].server_types, ( 60 | f"Command {command} is not supported by fakeredis" 61 | ) 62 | 63 | 64 | @testtools.fake_only 65 | def test_command_count(r: redis.Redis): 66 | assert r.command_count() >= len( 67 | [cmd for (cmd, cmd_info) in SUPPORTED_COMMANDS.items() if " " not in cmd and "redis" in cmd_info.server_types] 68 | ) 69 | 70 | 71 | @pytest.mark.unsupported_server_types("dragonfly") 72 | @pytest.mark.slow 73 | def test_bgsave_timestamp_update(r: redis.Redis): 74 | early_timestamp = r.lastsave() 75 | sleep(1) 76 | assert r.bgsave() 77 | sleep(1) 78 | late_timestamp = r.lastsave() 79 | assert early_timestamp < late_timestamp 80 | 81 | 82 | @pytest.mark.slow 83 | def test_save_timestamp_update(r: redis.Redis): 84 | early_timestamp = r.lastsave() 85 | sleep(1) 86 | assert r.save() 87 | late_timestamp = r.lastsave() 88 | assert early_timestamp < late_timestamp 89 | 90 | 91 | def test_dbsize(r: redis.Redis): 92 | assert r.dbsize() == 0 93 | r.set("foo", "bar") 94 | r.set("bar", "foo") 95 | assert r.dbsize() == 2 96 | 97 | 98 | def test_flushdb_redispy4(r: redis.Redis): 99 | r.set("foo", "bar") 100 | assert r.keys() == [b"foo"] 101 | assert r.flushdb() is True 102 | assert r.keys() == [] 103 | -------------------------------------------------------------------------------- /test/test_stack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/test/test_stack/__init__.py -------------------------------------------------------------------------------- /test/test_stack/test_cuckoofilter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import redis 3 | 4 | from test.testtools import get_protocol_version 5 | 6 | cuckoofilters_tests = pytest.importorskip("probables") 7 | 8 | 9 | @pytest.mark.min_server("7") 10 | @pytest.mark.unsupported_server_types("dragonfly") 11 | def test_cf_add_and_insert(r: redis.Redis): 12 | assert r.cf().create("cuckoo", 1000) 13 | assert r.cf().add("cuckoo", "filter") 14 | assert not r.cf().addnx("cuckoo", "filter") 15 | assert 1 == r.cf().addnx("cuckoo", "newItem") 16 | assert [1] == r.cf().insert("captest", ["foo"]) 17 | assert [1] == r.cf().insert("captest", ["foo"], capacity=1000) 18 | assert [1] == r.cf().insertnx("captest", ["bar"]) 19 | assert [1] == r.cf().insertnx("captest", ["food"], nocreate="1") 20 | assert [0, 0, 1] == r.cf().insertnx("captest", ["foo", "bar", "baz"]) 21 | assert [0] == r.cf().insertnx("captest", ["bar"], capacity=1000) 22 | assert [1] == r.cf().insert("empty1", ["foo"], capacity=1000) 23 | assert [1] == r.cf().insertnx("empty2", ["bar"], capacity=1000) 24 | info = r.cf().info("captest") 25 | if get_protocol_version(r) == 2: 26 | assert info.get("insertedNum") == 5 27 | assert info.get("deletedNum") == 0 28 | assert info.get("filterNum") == 1 29 | else: 30 | assert info.get(b"Number of items inserted") == 5 31 | assert info.get(b"Number of items deleted") == 0 32 | assert info.get(b"Number of filters") == 1 33 | 34 | 35 | @pytest.mark.unsupported_server_types("dragonfly") 36 | def test_cf_exists_and_del(r: redis.Redis): 37 | assert r.cf().create("cuckoo", 1000) 38 | assert r.cf().add("cuckoo", "filter") 39 | assert r.cf().exists("cuckoo", "filter") 40 | assert not r.cf().exists("cuckoo", "notexist") 41 | assert [1, 0] == r.cf().mexists("cuckoo", "filter", "notexist") 42 | assert 1 == r.cf().count("cuckoo", "filter") 43 | assert 0 == r.cf().count("cuckoo", "notexist") 44 | assert r.cf().delete("cuckoo", "filter") 45 | assert 0 == r.cf().count("cuckoo", "filter") 46 | -------------------------------------------------------------------------------- /test/test_stack/test_topk.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import redis 3 | 4 | from test.testtools import resp_conversion, get_protocol_version 5 | 6 | topk_tests = pytest.importorskip("probables") 7 | 8 | 9 | @pytest.mark.unsupported_server_types("dragonfly", "valkey") 10 | def test_topk_incrby(r: redis.Redis): 11 | assert r.topk().reserve("topk", 3, 10, 3, 1) 12 | assert [None, None, None] == r.topk().incrby("topk", ["bar", "baz", "42"], [3, 6, 4]) 13 | assert resp_conversion(r, [None, b"bar"], [None, "bar"]) == r.topk().incrby("topk", ["42", "xyzzy"], [8, 4]) 14 | with pytest.deprecated_call(): 15 | assert [3, 6, 12, 4, 0] == r.topk().count("topk", "bar", "baz", "42", "xyzzy", 4) 16 | 17 | 18 | @pytest.mark.min_server("7") 19 | @pytest.mark.unsupported_server_types("dragonfly", "valkey") 20 | def test_topk(r: redis.Redis): 21 | # test list with empty buckets 22 | assert r.topk().reserve("topk", 3, 50, 4, 0.9) 23 | ret = r.topk().add("topk", "A", "B", "C", "D", "D", "E", "A", "A", "B", "C", "G", "D", "B", "D", "A", "E", "E", 1) 24 | assert len(ret) == 18 25 | 26 | with pytest.deprecated_call(): 27 | assert r.topk().count("topk", "A", "B", "C", "D", "E", "F", "G") == [4, 3, 2, 4, 3, 0, 1] 28 | ret = r.topk().query("topk", "A", "B", "C", "D", "E", "F", "G") 29 | assert (ret == [1, 0, 0, 1, 1, 0, 0]) or (ret == [1, 1, 0, 1, 0, 0, 0]) 30 | # test full list 31 | assert r.topk().reserve("topklist", 3, 50, 3, 0.9) 32 | assert r.topk().add("topklist", "A", "B", "D", "E", "A", "A", "B", "C", "G", "D", "B", "A", "B", "E", "E") 33 | with pytest.deprecated_call(): 34 | assert r.topk().count("topklist", "A", "B", "C", "D", "E", "F", "G") == [4, 4, 1, 2, 3, 0, 1] 35 | assert r.topk().list("topklist") == resp_conversion(r, [b"A", b"B", b"E"], ["A", "B", "E"]) 36 | assert r.topk().list("topklist", withcount=True) == resp_conversion( 37 | r, [b"A", 4, b"B", 4, b"E", 3], ["A", 4, "B", 4, "E", 3] 38 | ) 39 | info = r.topk().info("topklist") 40 | if get_protocol_version(r) == 2: 41 | assert 3 == info["k"] 42 | assert 50 == info["width"] 43 | assert 3 == info["depth"] 44 | assert 0.9 == round(float(info["decay"]), 1) 45 | else: 46 | assert 3 == info[b"k"] 47 | assert 50 == info[b"width"] 48 | assert 3 == info[b"depth"] 49 | assert 0.9 == round(float(info[b"decay"]), 1) 50 | -------------------------------------------------------------------------------- /test/test_tcp_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cunla/fakeredis-py/8e9123bfefbe9e8f8a5a74decd87471b0f57c6c0/test/test_tcp_server/__init__.py -------------------------------------------------------------------------------- /test/test_tcp_server/test_connectivity.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from threading import Thread 4 | 5 | import pytest 6 | import redis 7 | 8 | from fakeredis import TcpFakeServer 9 | 10 | 11 | @pytest.mark.skipif(sys.version_info < (3, 11), reason="TcpFakeServer is only available in Python 3.11+") 12 | def test_tcp_server_started(): 13 | server_address = ("127.0.0.1", 19000) 14 | server = TcpFakeServer(server_address) 15 | t = Thread(target=server.serve_forever, daemon=True) 16 | t.start() 17 | time.sleep(0.1) 18 | with redis.Redis(host=server_address[0], port=server_address[1]) as r: 19 | r.set("foo", "bar") 20 | assert r.get("foo") == b"bar" 21 | server.shutdown() 22 | -------------------------------------------------------------------------------- /test/testtools.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import itertools 3 | import time 4 | from datetime import datetime 5 | from typing import Any 6 | 7 | import pytest 8 | import redis 9 | from packaging.version import Version 10 | 11 | from fakeredis._commands import Float 12 | 13 | REDIS_PY_VERSION = Version(redis.__version__) 14 | 15 | 16 | def tuple_to_list(x: Any) -> Any: 17 | if isinstance(x, (tuple, list)): 18 | return [tuple_to_list(x) for x in x] 19 | return x 20 | 21 | 22 | def get_protocol_version(r: redis.Redis) -> int: 23 | return int(r.connection_pool.connection_kwargs.get("protocol", 2)) 24 | 25 | 26 | def convert_to_resp2(val: Any) -> Any: 27 | if isinstance(val, str): 28 | return val.encode() 29 | if isinstance(val, float): 30 | return Float.encode(val, humanfriendly=False) 31 | if isinstance(val, dict): 32 | result = list(itertools.chain(*val.items())) 33 | return [convert_to_resp2(item) for item in result] 34 | if isinstance(val, list): 35 | res = [convert_to_resp2(item) for item in val] 36 | return res 37 | if isinstance(val, tuple): 38 | res = tuple(convert_to_resp2(item) for item in val) 39 | return res 40 | return val 41 | 42 | 43 | def resp_conversion(r: redis.Redis, val_resp3: Any, val_resp2: Any) -> Any: 44 | res = val_resp2 if get_protocol_version(r) == 2 else val_resp3 45 | return res 46 | 47 | 48 | def resp_conversion_from_resp2(r: redis.Redis, val: Any) -> Any: 49 | return resp_conversion(r, tuple_to_list(val), val) 50 | 51 | 52 | def key_val_dict(size=100): 53 | return {f"key:{i}".encode(): f"val:{i}".encode() for i in range(size)} 54 | 55 | 56 | def raw_command(r: redis.Redis, *args): 57 | """Like execute_command, but does not do command-specific response parsing""" 58 | response_callbacks = r.response_callbacks 59 | try: 60 | r.response_callbacks = {} 61 | return r.execute_command(*args) 62 | finally: 63 | r.response_callbacks = response_callbacks 64 | 65 | 66 | ALLOWED_CONDITIONS = {"eq", "gte", "lte", "lt", "gt", "ne"} 67 | 68 | 69 | def run_test_if_redispy_ver(condition: str, ver: str): 70 | if condition not in ALLOWED_CONDITIONS: 71 | raise ValueError(f"condition {condition} is not in allowed conditions ({ALLOWED_CONDITIONS})") 72 | cond = False 73 | cond = cond or condition == "eq" and REDIS_PY_VERSION == Version(ver) 74 | cond = cond or condition == "gte" and REDIS_PY_VERSION >= Version(ver) 75 | cond = cond or condition == "lte" and REDIS_PY_VERSION <= Version(ver) 76 | cond = cond or condition == "lt" and REDIS_PY_VERSION < Version(ver) 77 | cond = cond or condition == "gt" and REDIS_PY_VERSION > Version(ver) 78 | cond = cond or condition == "ne" and REDIS_PY_VERSION != Version(ver) 79 | return pytest.mark.skipif( 80 | not cond, reason=f"Test is not applicable to redis-py {REDIS_PY_VERSION} ({condition}, {ver})" 81 | ) 82 | 83 | 84 | _lua_module = importlib.util.find_spec("lupa") 85 | run_test_if_lupa = pytest.mark.skipif(_lua_module is None, reason="Test is only applicable if lupa is installed") 86 | 87 | fake_only = pytest.mark.parametrize( 88 | "create_connection", [pytest.param("FakeStrictRedis2", marks=pytest.mark.fake)], indirect=True 89 | ) 90 | 91 | 92 | def redis_server_time(r: redis.Redis) -> datetime: 93 | seconds, milliseconds = r.time() 94 | timestamp = float(f"{seconds}.{milliseconds}") 95 | return datetime.fromtimestamp(timestamp) 96 | 97 | 98 | def current_time() -> int: 99 | """Return current_time in ms""" 100 | return int(time.time() * 1000) 101 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{39,310,311,312,313} 4 | isolated_build = True 5 | 6 | [testenv] 7 | allowlist_externals = 8 | uv 9 | podman 10 | 11 | usedevelop = True 12 | passenv = DOCKER_HOST 13 | commands = 14 | uv sync --all-extras 15 | podman run -d -p 6390:6379 --name redis7fakeredis redis:8.0.0 16 | uv run pytest -v 17 | podman stop redis7fakeredis 18 | podman rm redis7fakeredis 19 | --------------------------------------------------------------------------------