Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bd3f5f1
feat(cli): add JSON output helpers for rvl
limjoobin Apr 24, 2026
ddcd751
feat(cli): add --json to rvl version
limjoobin Apr 24, 2026
96209c9
test(cli): cover rvl version --json and JSON CLI helpers
limjoobin Apr 24, 2026
bead749
feat(cli): add --json to rvl stats with shared _stats_rows
limjoobin Apr 26, 2026
d9bc7e9
test: add unit tests for rvl stats CLI
limjoobin Apr 26, 2026
f77532f
feat(cli): add --json to rvl index listall
limjoobin Apr 26, 2026
7276fa5
test: add unit tests for rvl index listall
limjoobin Apr 26, 2026
8f6118a
feat(cli): add --json to rvl index info
limjoobin Apr 26, 2026
76d45cc
test: add unit tests for rvl index info --json
limjoobin Apr 26, 2026
f2b7e77
Merge branch 'main' into feat/RAAE-1557/cli-json
limjoobin Apr 27, 2026
8b3b2cc
chore: Format index.py and utils.py with black
limjoobin Apr 27, 2026
db1313f
fix(types): coerce index info inputs before make_dict in cli index js…
limjoobin Apr 27, 2026
ce70d09
fix(cli): preserve dict index_definition in index info json
limjoobin Apr 27, 2026
25d0dc7
fix(cli-stats): preserve numeric/null types in --json output
limjoobin Apr 27, 2026
165b755
chore: minor refactor for readability
limjoobin Apr 29, 2026
316e542
chore: updated parser description for readability
limjoobin Apr 29, 2026
6322e19
Revert "feat(cli): add --json to rvl version"
limjoobin Apr 29, 2026
81518d3
reverted tests for JSON output in version.py
limjoobin Apr 29, 2026
0374033
Merge branch 'main' into feat/RAAE-1557/cli-json
limjoobin Apr 30, 2026
2414320
fix: Fixed syntax error from typo
limjoobin Apr 30, 2026
0f0ed61
fix: Resolved merge conflict
limjoobin Apr 30, 2026
dc90c26
fix: Removed failing exit code assertions
limjoobin Apr 30, 2026
9e8790b
fix: Fixed exit codes for Stats CLI error handling
limjoobin Apr 30, 2026
e1d2bcf
Chore: removed unused import
limjoobin Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions redisvl/cli/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import yaml
from pydantic import ValidationError

from redisvl.cli.utils import add_index_parsing_options, create_redis_url
from redisvl.cli.utils import (
add_index_parsing_options,
add_json_output_flag,
cli_print_json,
create_redis_url,
)
from redisvl.exceptions import RedisSearchError
from redisvl.index import SearchIndex
from redisvl.redis.connection import RedisConnectionFactory
Expand Down Expand Up @@ -51,11 +56,11 @@ class Index:
[
"rvl index <command> [<args>]\n",
"Commands:",
"\tinfo Obtain information about an index",
"\tinfo Obtain information about an index (use --json for machine output)",
"\tcreate Create a new index",
"\tdelete Delete an existing index",
"\tdestroy Delete an existing index and all of its data",
"\tlistall List all indexes",
"\tlistall List all indexes (use --json for machine output)",
"\n",
]
)
Expand All @@ -65,6 +70,7 @@ def __init__(self):

parser.add_argument("command", help="Subcommand to run")
parser = add_index_parsing_options(parser)
parser = add_json_output_flag(parser)

args = parser.parse_args(sys.argv[2:])

Expand Down Expand Up @@ -105,26 +111,34 @@ def info(self, args: Namespace):
"""Obtain information about an index.

Usage:
rvl index info -i <index_name> | -s <schema_path>
rvl index info -i <index_name> | -s <schema_path> [--json]
"""
index = self._connect_to_index(args)
try:
_display_in_table(index.info())
index_info = index.info()
except RedisSearchError as e:
exit_redis_search_error(args, index, e)

if args.json:
cli_print_json(_index_info_for_json(index_info))
else:
_display_in_table(index_info)

def listall(self, args: Namespace):
"""List all indices.

Usage:
rvl index listall
rvl index listall [--json]
"""
redis_url = create_redis_url(args)
conn = RedisConnectionFactory.get_redis_connection(redis_url=redis_url)
indices = convert_bytes(conn.execute_command("FT._LIST"))
print("Indices:")
for i, index in enumerate(indices):
print(str(i + 1) + ". " + index)
if args.json:
cli_print_json({"indices": indices})
else:
print("Indices:")
for i, index in enumerate(indices, start=1):
print(str(i) + ". " + index)

def delete(self, args: Namespace, drop=False):
"""Delete an index.
Expand Down Expand Up @@ -167,6 +181,56 @@ def _connect_to_index(self, args: Namespace) -> SearchIndex:
sys.exit(2)


def _index_info_for_json(index_info: dict) -> dict:
"""Build the JSON payload from the same fields shown in table mode."""
definition_src = index_info.get("index_definition")
if isinstance(definition_src, list):
definition = convert_bytes(make_dict(definition_src))
elif isinstance(definition_src, tuple):
definition = convert_bytes(make_dict(list(definition_src)))
elif isinstance(definition_src, dict):
definition = convert_bytes(dict(definition_src))
else:
definition = {}
Comment thread
cursor[bot] marked this conversation as resolved.
attributes = index_info.get("attributes", [])
index_fields = []

for attrs in attributes:
if isinstance(attrs, list):
attr = convert_bytes(make_dict(attrs))
elif isinstance(attrs, tuple):
attr = convert_bytes(make_dict(list(attrs)))
elif isinstance(attrs, dict):
attr = convert_bytes(dict(attrs))
else:
attr = {}
field = {
"name": attr.get("identifier"),
"attribute": attr.get("attribute"),
"type": attr.get("type"),
}
field_options = {
k: v
for k, v in attr.items()
if k not in {"identifier", "attribute", "type"}
}
if field_options:
field["field_options"] = field_options
index_fields.append(field)

payload = {
"index_information": {
"index_name": index_info.get("index_name"),
"storage_type": definition.get("key_type"),
"prefixes": definition.get("prefixes"),
"index_options": index_info.get("index_options"),
"indexing": index_info.get("indexing"),
},
"index_fields": index_fields,
}
return convert_bytes(payload)


def _display_in_table(index_info):
print("\n")
attributes = index_info.get("attributes", [])
Expand Down
35 changes: 28 additions & 7 deletions redisvl/cli/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import yaml
from pydantic import ValidationError

from redisvl.cli.utils import add_index_parsing_options, create_redis_url
from redisvl.cli.utils import (
add_index_parsing_options,
add_json_output_flag,
cli_print_json,
create_redis_url,
)
from redisvl.exceptions import RedisSearchError
from redisvl.index import SearchIndex
from redisvl.schema.schema import IndexSchema
Expand Down Expand Up @@ -67,6 +72,17 @@ def exit_redis_search_error(
]


def _stats_rows(index_info: dict) -> list[tuple[str, object]]:
"""Normalize ``index.info()`` for both JSON and table output.

Returns ordered ``(key, value)`` pairs: keys follow ``STATS_KEYS``, values
preserve native types from ``index_info``. Missing keys remain ``None``.
For JSON, wrap the result in ``dict(...)``; the table stringifies values
when rendering.
"""
return [(key, index_info.get(key)) for key in STATS_KEYS]


class Stats:
usage = "\n".join(
[
Expand All @@ -77,6 +93,7 @@ class Stats:
def __init__(self):
parser = argparse.ArgumentParser(usage=self.usage)
parser = add_index_parsing_options(parser)
parser = add_json_output_flag(parser)
args = parser.parse_args(sys.argv[2:])

try:
Expand All @@ -94,10 +111,16 @@ def stats(self, args: Namespace):
"""
index = self._connect_to_index(args)
try:
_display_stats(index.info())
index_info = index.info()
except RedisSearchError as e:
exit_redis_search_error(args, index, e)

rows = _stats_rows(index_info)
if args.json:
cli_print_json(dict(rows))
else:
_display_stats(rows)

def _connect_to_index(self, args: Namespace) -> SearchIndex:
redis_url = create_redis_url(args)

Expand All @@ -118,10 +141,7 @@ def _connect_to_index(self, args: Namespace) -> SearchIndex:
sys.exit(2)


def _display_stats(index_info):
# Extracting the statistics
stats_data = [(key, str(index_info.get(key))) for key in STATS_KEYS]

def _display_stats(stats_data: list[tuple[str, object]]) -> None:
# Display the statistics in tabular format
print("\nStatistics:")
max_key_length = max(len(key) for key, _ in stats_data)
Expand All @@ -130,5 +150,6 @@ def _display_stats(index_info):
print("│ Stat Key │ Value │") # header row
print(f"├{horizontal_line}┼────────────┤") # separator row
for key, value in stats_data:
print(f"│ {key:<27} │ {value[0:10]:<10} │") # data rows
value_str = str(value)
print(f"│ {key:<27} │ {value_str[0:10]:<10} │") # data rows
print(f"╰{horizontal_line}┴────────────╯") # bottom row
26 changes: 26 additions & 0 deletions redisvl/cli/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
import os
from argparse import ArgumentParser, Namespace
from typing import Any, Mapping
from urllib.parse import quote, urlparse, urlunparse

from redisvl.redis.constants import REDIS_URL_ENV_VAR
Expand Down Expand Up @@ -91,3 +93,27 @@ def add_index_parsing_options(parser: ArgumentParser) -> ArgumentParser:
default=None,
)
return parser


def _cli_json_default(obj: object) -> object:
"""Handle common non-JSON-native types (e.g. bytes from Redis) for cli_print_json."""
if isinstance(obj, bytes):
return obj.decode("utf-8", errors="replace")
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")


def add_json_output_flag(parser: ArgumentParser) -> ArgumentParser:
"""Register ``--json`` for machine-readable stdout (one JSON object on success)."""
parser.add_argument(
"--json",
action="store_true",
dest="json",
default=False,
help="Format output (when successful) as machine-readable JSON",
)
return parser


def cli_print_json(data: Mapping[str, Any]) -> None:
"""Write a single JSON object to stdout (deterministic key order for tests and scripts)."""
print(json.dumps(data, default=_cli_json_default))
Loading
Loading