Skip to content
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

Python: adds json module and JSON.SET JSON.GET commands #1056

Merged
merged 6 commits into from
Mar 11, 2024

Conversation

shohamazon
Copy link
Collaborator

Issue #, if available:

Description of changes:

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@shohamazon shohamazon requested a review from a team as a code owner March 3, 2024 12:16
@shohamazon shohamazon added the python Python wrapper label Mar 3, 2024
@shohamazon shohamazon requested a review from barshaul March 3, 2024 14:11
client: TRedisClient,
key: str,
path: str,
value: TJson,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is always translated to a string, and always returned as a string, right?
If so, isn't it a bit confusing that you set a dictionary and get a string?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, but I think it could be nice to be able to pass a dictionary (or another complex object) and also to be able to pass a string representation of the dictionary.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Shachar, if we expect that the user will use python's json module to convert the response from a string to a complex object, it's relevant to the passed value too. If we except only string as a value, we can avoid importing 'json' module, which might not be in the standard libraries in other languages.

@@ -1,4 +1,4 @@
[pytest]
markers =
smoke_test: mark a test as a build verification testing.
addopts = -k "not test_redis_modules.py"
addopts = -k "not test_redis_modules.py and not test_json.py"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding this for each new module we add, can you check if we can move all module tests into a folder (tests/modules) and skip the whole folder by default?

TJson = Union[str, int, float, bool, None, List["TJson"], Dict[str, "TJson"]]


class Json:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class doesn't save any state, not sure what's the gain of using it as a class rather than just a module, e.g.:

json.py

def set():
    ...

def get():
    ...

main.py

import json

json.set()
json.get()

The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added.
value (TJSON): The value to set at the specific path.
set_condition (Optional[ConditionalChange]): Set the value only if the given condition is met (within the key or path).
Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None.
Equivalent to `XX`/`NX` in the Redis API. Defaults to None.

"[]" # Returns an empty array since the path '$.non_existing_path' does not exist in the JSON document stored at 'doc'.
>>> import json
>>> json.loads(await json_get(client, "doc", "$"))
[{"a": 1.0, "b" :2}] # JSON object retrieved from the key "doc" using json.loads()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[{"a": 1.0, "b" :2}] # JSON object retrieved from the key "doc" using json.loads()
[{"a": 1.0, "b" :2}] # JSON object retrieved from the key "doc" using `json.loads()`

Comment on lines 62 to 64
indent: Optional[str] = None,
newline: Optional[str] = None,
space: Optional[str] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to merge this into JsonOptions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding Yuri's proposal, are those options being utilized in other JSON functions as well?

python/python/tests/test_json.py Outdated Show resolved Hide resolved
@Yury-Fridlyand
Copy link
Collaborator

Transaction test?

If value isn't set because of `set_condition`, return None.

Examples:
>>> await json_set(client, "doc", "$", {'a': 1.0, 'b': 2})
Copy link
Collaborator

@barshaul barshaul Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand json examples to be more informative, for example:

import json
value = {'a': 1.0, 'b': 2}
json_str = json.dumps(value)
await json.set(client, "doc", "$", json_str)
'OK'  # Indicates successful setting of the value at path '$' in the key stored at 'doc'.

and for json.get show how you use json to decode the response to python object

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we can have a single example and remove the second you did here)

Examples:
>>> await json_set(client, "doc", "$", {'a': 1.0, 'b': 2})
'OK' # Indicates successful setting of the value at path '$' in the key stored at 'doc'.
>>> await json_set(client, "doc", "$.a", 1.5 , ConditionalChange.ONLY_IF_DOES_NOT_EXIST)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(it should be changed after we'll change Json to a module instead of a class - but note that json_set function is incorrect)

"{\n \"$.a\": [\n 1.0\n ],\n \"$.b\": [\n 2\n ]\n}" # Returns the values at paths '$.a' and '$.b' in the JSON document stored at 'doc', with specified formatting options.
>>> await json_get(client, "doc", "$.non_existing_path")
"[]" # Returns an empty array since the path '$.non_existing_path' does not exist in the JSON document stored at 'doc'.
>>> import json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets do the import on top and have one/two examples only that use json.loads

Copy link
Collaborator

@barshaul barshaul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round

@shohamazon
Copy link
Collaborator Author

@barshaul round

Copy link
Collaborator

@barshaul barshaul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will continue reviewing tests tomorrow

@@ -1,4 +1,4 @@
[pytest]
markers =
smoke_test: mark a test as a build verification testing.
addopts = -k "not test_redis_modules.py"
addopts = --ignore "tests_redis_modules"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's already under tests folder, please rename the folder name to "redis_modules"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about changing the GitHub action that tests modules accordingly?

Copy link
Collaborator Author

@shohamazon shohamazon Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to change a thing, the GitHub action overrides addopts

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

( pytest --asyncio-mode=auto --override-ini=addopts= --load-module=$GITHUB_WORKSPACE/redisearch.so --load-module=$GITHUB_WORKSPACE/redisjson.so)

@@ -0,0 +1,104 @@
# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove the json folder, we can always add it later on if the json module will need to be extended

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General note: please add json to the all in glide/init

Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None.

Returns:
Optional[TOK]: If the value is successfully set, return OK.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: return-> returns
(In both lines)

If value isn't set because of `set_condition`, return None.

Examples:
>>> from glide.async_commands.json import json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After adding it to all, change to '''
from glide import json as redisJson
Import json
...


Examples:
>>> from glide.async_commands.json import json
>>> import json as OuterJson
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Same as in the comment above - don't use OuterJson)

Args:
client (TRedisClient): The Redis client to execute the command.
key (str): The key of the JSON document.
paths (ptional[Union[str, List[str]]]): The path or list of paths within the JSON document.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it defaults to None? What is the default path?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: ptional-> optional

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default is root $.


Examples:
>>> import json as OuterJson
>>> import json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

>>> import json as OuterJson
>>> import json
>>> OuterJson.loads(await json_get(client, "doc", "$"))
[{"a": 1.0, "b" :2}] # JSON object retrieved from the key "doc" using json.loads()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json_get -> json.get

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have it in two steps so you'll explain you're getting a jsonstring and you load it to a python object

paths = [paths]

for path in paths:
args.append(path)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using extend?

Args:
client (TRedisClient): The Redis client to execute the command.
key (str): The key of the JSON document.
paths (Optional[Union[str, List[str]]]): The path or list of paths within the JSON document.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets write that the default is root, it feels not readable enough without this info

import json as OuterJson

import pytest
from glide import json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be consistent, please use the full import path

class TestJson:
@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_json_module_loadded(self, redis_client: TRedisClient):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadded => is_loaded

key = get_random_string(5)

assert (
await json.set(redis_client, key, "$", OuterJson.dumps({"a": 1.0, "b": 2}))
Copy link
Collaborator

@barshaul barshaul Mar 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets save it in a variable, something like:

json_value = {"a": 1.0, "b": 2}
await json.set(redis_client, key, "$", OuterJson.dumps(json_value))
...
result = await json.get(redis_client, key, ".")
assert isinstance(result, str)
assert OuterJson.loads(result) == json_value

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add test with boolean too

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this file? looks like these tests are a part of the specific module tests

@@ -125,6 +125,8 @@ enum RequestType {
XGroupDestroy = 81;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this change to CHANGELOG

'OK' # Indicates successful setting of the value at path '$' in the key stored at 'doc'.
>>> await redisJson.get(client, "doc", "$")
"[{\"a\":1.0,\"b\":2}]" # Returns the value at path '$' in the JSON document stored at 'doc'.
>>> json_get = await redisJson.get(client, "doc", "$")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove duplication of json get

Copy link
Collaborator

@barshaul barshaul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix & merge

@shohamazon shohamazon force-pushed the python/json branch 3 times, most recently from 0ab2c95 to e3633d9 Compare March 11, 2024 12:09
@shohamazon shohamazon merged commit ac1c8d5 into valkey-io:main Mar 11, 2024
41 of 44 checks passed
@shohamazon shohamazon deleted the python/json branch March 11, 2024 15:31
cyip10 pushed a commit to Bit-Quill/valkey-glide that referenced this pull request Jun 24, 2024
@shohamazon shohamazon mentioned this pull request Oct 28, 2024
22 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
python Python wrapper
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants