-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Added custom Claude command + XNACK command support #4030
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a4c8ba7
fee6e93
a8e4214
6cde5fe
f6f7483
71f04b1
87392ec
91a0509
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # $COMMAND_NAME command specification | ||
|
|
||
| ## Supported version | ||
|
|
||
| Add supported Redis version here. For example: Redis >= 6.2.0 | ||
|
|
||
| ## Command description | ||
|
|
||
| Add a description of the command here. | ||
|
|
||
| ## Command API | ||
|
|
||
| Specify an API for the command in the format that official docs uses. For example: | ||
|
|
||
| ``` | ||
| $COMMAND_NAME $key $member [NX|XX] [CH] [INCR] | ||
| ``` | ||
|
|
||
| ## Redis-CLI examples | ||
|
|
||
| Add relevant Redis-CLI examples here. | ||
|
|
||
| ## Test plan | ||
|
|
||
| Specify how you want to test the command in terms of integration testing. For example: | ||
|
|
||
| - Test only with required arguments, assert that single value returned | ||
| - Test with required arguments and optional XX modifier, ensure that 1 returned | ||
| - ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| --- | ||
| description: Adds support for a new Redis command from a given specification. Check command-specification-template.md. | ||
| argument-hint: [path-to-specification] | ||
| --- | ||
|
|
||
| # Execute: Add new Redis command support | ||
|
|
||
| ## Plan to Execute | ||
|
|
||
| Read specification file: `$ARGUMENTS` | ||
|
|
||
| ## Execution Instructions | ||
|
|
||
| ### 1. Preparations | ||
|
|
||
| - Follow instructions from `.agent/instructions.md` | ||
| - Go through the guide `specs/redis_commands_guide.md` | ||
|
|
||
| ### 2. Read and Understand | ||
|
|
||
| - Read the ENTIRE specification carefully | ||
| - Go through Command Description and identify command type (string, list, set, etc.) | ||
| - Go through the Command API: | ||
| - Identify required and optional arguments | ||
| - Identify how to match Redis command arguments type to Python types | ||
| - Identify return value and possible response types | ||
| - Check relevant Redis-Cli examples, if provided | ||
| - Review the Test Plan | ||
|
|
||
| ### 3. Execute Tasks in Order | ||
|
|
||
| #### a. Navigate to the task | ||
| - Identify the files and action required | ||
| - Read existing related files if modifying | ||
|
|
||
| #### b. Implement the command | ||
| - Add new command method within matching trait object (for example: `redis/commands/core.py` for core Redis commands) | ||
| - Ensure overloading is implemented for sync and async API | ||
| - Follow Arguments definition section from `specs/redis_commands_guide.md` before defining command arguments | ||
| - Ensure arguments and response types consider bytes representation | ||
| - Ensure response RESP2 and RESP3 compatibility via `response_callbacks` | ||
|
|
||
| #### c. Verify as you go | ||
| - After each file change, check syntax | ||
| - Ensure imports are correct | ||
| - Verify types are properly defined | ||
| - Verify that response schema is similar for RESP2 and RESP3 | ||
|
|
||
| ### 4. Implement Testing Plan | ||
|
|
||
| After completing implementation tasks: | ||
|
|
||
| - Identify matching test file or create new one if needed | ||
| - Implement all test cases as separate test methods | ||
| - Ensure adding version constraint if specified in the specification | ||
| - Ensure tests cover edge cases | ||
|
|
||
| ### 5. Run tests | ||
|
|
||
| - Run newly added test cases using `pytest` with RESP2 and RESP3 protocol specified via `--protocol` option | ||
| - Ensure that the same test cases passed with both protocols | ||
| - Get back to the Implementation stage if any test failed | ||
|
|
||
| ### 6. Final Verification | ||
|
|
||
| Before completing: | ||
|
|
||
| - ✅ All tasks from plan completed | ||
| - ✅ All tests created and passing | ||
| - ✅ Code follows project conventions | ||
| - ✅ Documentation added/updated as needed | ||
|
|
||
| ## Output Report | ||
|
|
||
| Provide summary: | ||
|
|
||
| ### Completed Tasks | ||
| - List of all tasks completed | ||
| - Files created (with paths) | ||
| - Files modified (with paths) | ||
|
|
||
| ### Tests Added | ||
| - Test files created | ||
| - Test cases implemented | ||
| - Test results | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6801,6 +6801,78 @@ def xlen(self, name: KeyT) -> int | Awaitable[int]: | |
| """ | ||
| return self.execute_command("XLEN", name, keys=[name]) | ||
|
|
||
| @overload | ||
| def xnack( | ||
| self: SyncClientProtocol, | ||
| name: KeyT, | ||
| groupname: GroupT, | ||
| mode: Literal["SILENT", "FAIL", "FATAL"], | ||
| *ids: StreamIdT, | ||
| retrycount: int | None = None, | ||
| force: bool = False, | ||
| ) -> int: ... | ||
|
|
||
| @overload | ||
| def xnack( | ||
| self: AsyncClientProtocol, | ||
| name: KeyT, | ||
| groupname: GroupT, | ||
| mode: Literal["SILENT", "FAIL", "FATAL"], | ||
| *ids: StreamIdT, | ||
| retrycount: int | None = None, | ||
| force: bool = False, | ||
| ) -> Awaitable[int]: ... | ||
|
|
||
| def xnack( | ||
| self, | ||
| name: KeyT, | ||
| groupname: GroupT, | ||
| mode: Literal["SILENT", "FAIL", "FATAL"], | ||
| *ids: StreamIdT, | ||
| retrycount: int | None = None, | ||
| force: bool = False, | ||
| ) -> int | Awaitable[int]: | ||
| """ | ||
| Negatively acknowledges one or more messages in a consumer group's | ||
| Pending Entries List (PEL). | ||
|
|
||
| Args: | ||
| name: name of the stream. | ||
| groupname: name of the consumer group. | ||
| mode: the nacking mode. One of SILENT, FAIL, or FATAL. | ||
| SILENT: consumer shutting down; decrements delivery counter. | ||
| FAIL: consumer unable to process; delivery counter unchanged. | ||
| FATAL: invalid/malicious message; delivery counter set to max. | ||
| *ids: one or more message IDs to NACK. | ||
| retrycount: optional integer >= 0. Overrides the mode's implicit | ||
| delivery counter adjustment with an exact value. | ||
| force: if True, creates a new unowned PEL entry for any ID not | ||
| already in the group's PEL. | ||
|
|
||
| Returns: | ||
| The number of messages successfully NACKed. | ||
|
|
||
| For more information, see https://redis.io/commands/xnack | ||
| """ | ||
| if not ids: | ||
| raise DataError("XNACK requires at least one message ID") | ||
|
|
||
| if mode not in {"SILENT", "FAIL", "FATAL"}: | ||
| raise DataError("XNACK mode must be one of: SILENT, FAIL, FATAL") | ||
|
|
||
| pieces: list = [name, groupname, mode, "IDS", len(ids)] | ||
| pieces.extend(ids) | ||
|
|
||
| if retrycount is not None: | ||
| if retrycount < 0: | ||
| raise DataError("XNACK retrycount must be >= 0") | ||
| pieces.extend([b"RETRYCOUNT", retrycount]) | ||
|
|
||
| if force: | ||
| pieces.append(b"FORCE") | ||
|
|
||
| return self.execute_command("XNACK", *pieces) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. XNACK is a non-existent hallucinated Redis commandHigh Severity
Additional Locations (2)Reviewed by Cursor Bugbot for commit fee6e93. Configure here. |
||
|
|
||
| @overload | ||
| def xpending( | ||
| self: SyncClientProtocol, name: KeyT, groupname: GroupT | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # Redis commands guide | ||
|
|
||
| ## Commands API specification | ||
|
|
||
| The Redis API is specified in the [Redis documentation](https://redis.io/commands). This is the source of truth | ||
| for all command-related information. However, Redis is a living project and new commands are added all the time. | ||
|
|
||
| A new command may not yet be available in the documentation. In this case, the developer needs to create a new | ||
| command specification from the `.claude/command-specification-template.md` template. | ||
|
|
||
| ## Files structure | ||
|
|
||
| ``` | ||
| redis/ | ||
| ├── commands/ # Base directory | ||
| │ ├── module/ # Module commands directory | ||
| │ │ ├── commands.py # Module commands public API | ||
| │ │ └── file.py # Custom helpers | ||
| │ │... # Other modules | ||
| │ ├── __init__.py # Module exports | ||
| │ ├── cluster.py # Cluster commands public API | ||
| │ ├── core.py # Core commands public API | ||
| │ ├── helpers.py # Helpers for commands modules | ||
| │ ├── redismodules.py # Trait for all Redis modules | ||
| │ └── sentinel.py # Sentinel commands public API | ||
| ``` | ||
|
|
||
| ## Commands Public API | ||
|
|
||
| Commands public API exposed through the different types of clients (Standalone, Cluster, Sentinel), inheriting trait | ||
| objects defined in the directories according to the Files structure section above. Each command implementation at the | ||
| end calls generic `execute_command(*args, **kwargs)` defined by `CommandProtocol`. | ||
|
|
||
| SDK expose sync and async commands API through methods overloading, see `.agent/sync_async_type_hints_overload_guide.md` | ||
| for more information. | ||
|
|
||
| ### Binary representation | ||
|
|
||
| Our SDK can be configured with `decode_response=False` option which means that command response will be returned | ||
| as bytes, as well as we allow to pass bytes arguments instead of strings. For this purpose, we define a custom types | ||
| like `KeyT`, `EncodableT`, etc. in `redis/typing.py`. | ||
|
|
||
| ## Protocols compatibility | ||
|
|
||
| SDK supports two types of Redis wire protocol: RESP2 and RESP3. And aims to provide a compatibility between them | ||
| for seamless user experience. However, there are some differences in types that defined by these protocols. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. types that defined --> types that are defined |
||
|
|
||
| Because, RESP3 introduce new types that wasn't previously supported by RESP2, we're aiming for forward compatibility | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. types that wasn't --> types that weren't |
||
| and ensure that all new types supported by RESP3 can be used with RESP2. In most cases the semantic of RESP3 can be | ||
| easily recognized and converting existing RESP2 response in RESP3 is a matter of parsing strategy. | ||
|
|
||
| All new commands should be added with RESP3 in mind and by default should hide protocol incompatibility from user. | ||
|
|
||
| To understand how does Redis types defined by RESP2 and RESP3 protocol maps to Python types, | ||
| see `redis/_parsers/resp2.py` and `redis/_parsers/resp3.py`. | ||
|
|
||
| Parsers are responsible for RESP protocol parsing. However, protocols compatibility is achieved by 2nd layer | ||
| parsing as `response_callbacks` defined in `redis/_parsers/helpers.py` and can be extended/updated on client level. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For modules the callbacks are defined usually in module's |
||
|
|
||
| ## Arguments definition | ||
|
|
||
| Some of the Redis commands may have a complex API structure, so we need to make it user-friendly and apply | ||
| some transformation within specific command method. For example, some commands may need to have | ||
| a COUNT argument for aggregated types followed by number of arguments and arguments itself. In this case | ||
| we hide this complexity from user and instead expose argument as list in public API and transform it | ||
| to Redis-friendly format. | ||
|
|
||
| ```python | ||
| def scan( | ||
| self, | ||
| cursor: int = 0, | ||
| count: int | None = None, | ||
| _type: str | None = None, | ||
| **kwargs, | ||
| ): | ||
| pieces: list = [cursor] | ||
|
|
||
| if count is not None: | ||
| pieces.extend([b"COUNT", count]) | ||
| ``` | ||
|
|
||
| In terms of required and optional arguments we follow the specification and trying to reflect it as close | ||
| as possible. | ||
|
|
||
| ### Testing | ||
|
|
||
| Command tests are located in `tests/test_*command_type*.py` and `tests/test_asyncio/test_*command_type*.py` for async | ||
| commands. So it's important to identify command type upfront to resolve correct test file. | ||
|
|
||
| We usually provide only integration testing for commands with defined version constraint (if required). It's controlled | ||
| by custom annotations `@skip_if_server_version_lt()` and `@skip_if_server_version_gte()` defined in `tests/conftest.py`. | ||


Uh oh!
There was an error while loading. Please reload this page.