ragbits-cli 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/CHANGELOG.md +6 -0
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/PKG-INFO +2 -2
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/pyproject.toml +2 -2
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/src/ragbits/cli/state.py +53 -18
- ragbits_cli-0.8.0/tests/unit/test_state.py +121 -0
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/.gitignore +0 -0
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/README.md +0 -0
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/py.typed +0 -0
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/src/ragbits/cli/__init__.py +0 -0
- {ragbits_cli-0.7.0 → ragbits_cli-0.8.0}/src/ragbits/cli/_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ragbits-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A CLI application for ragbits - building blocks for rapid development of GenAI applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/deepsense-ai/ragbits
|
|
6
6
|
Project-URL: Bug Reports, https://github.com/deepsense-ai/ragbits/issues
|
|
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
22
22
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
|
-
Requires-Dist: ragbits-core==0.
|
|
25
|
+
Requires-Dist: ragbits-core==0.8.0
|
|
26
26
|
Requires-Dist: typer>=0.12.5
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ragbits-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "A CLI application for ragbits - building blocks for rapid development of GenAI applications"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -31,7 +31,7 @@ classifiers = [
|
|
|
31
31
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
32
32
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
33
|
]
|
|
34
|
-
dependencies = ["typer>=0.12.5", "ragbits-core==0.
|
|
34
|
+
dependencies = ["typer>=0.12.5", "ragbits-core==0.8.0"]
|
|
35
35
|
|
|
36
36
|
[project.urls]
|
|
37
37
|
"Homepage" = "https://github.com/deepsense-ai/ragbits"
|
|
@@ -2,10 +2,12 @@ import json
|
|
|
2
2
|
from collections.abc import Mapping, Sequence
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from
|
|
5
|
+
from types import UnionType
|
|
6
|
+
from typing import Optional, TypeVar, Union, get_args, get_origin
|
|
6
7
|
|
|
7
8
|
import typer
|
|
8
9
|
from pydantic import BaseModel
|
|
10
|
+
from pydantic.fields import FieldInfo
|
|
9
11
|
from rich.console import Console
|
|
10
12
|
from rich.table import Column, Table
|
|
11
13
|
|
|
@@ -48,39 +50,72 @@ def print_output_table(
|
|
|
48
50
|
console.print("No results")
|
|
49
51
|
return
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Human readable titles for columns
|
|
54
|
-
titles = {
|
|
55
|
-
key: value.get("title", key)
|
|
56
|
-
for key, value in data[0].model_json_schema(mode="serialization")["properties"].items()
|
|
57
|
-
}
|
|
53
|
+
base_fields = {**data[0].model_fields, **data[0].model_computed_fields}
|
|
58
54
|
|
|
59
55
|
# Normalize the list of columns
|
|
60
56
|
if columns is None:
|
|
61
|
-
columns = {key: Column() for key in
|
|
57
|
+
columns = {key: Column() for key in base_fields}
|
|
62
58
|
elif isinstance(columns, str):
|
|
63
59
|
columns = {key: Column() for key in columns.split(",")}
|
|
64
60
|
elif isinstance(columns, Sequence):
|
|
65
61
|
columns = {key: Column() for key in columns}
|
|
66
62
|
|
|
67
|
-
#
|
|
68
|
-
for
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise typer.Exit(1)
|
|
72
|
-
|
|
73
|
-
column = columns[key]
|
|
63
|
+
# check if columns are correct
|
|
64
|
+
for column_name in columns:
|
|
65
|
+
field = _get_nested_field(column_name, base_fields)
|
|
66
|
+
column = columns[column_name]
|
|
74
67
|
if column.header == "":
|
|
75
|
-
column.header =
|
|
68
|
+
column.header = field.title if field.title else column_name.replace("_", " ").replace(".", " ").title()
|
|
76
69
|
|
|
77
70
|
# Create and print the table
|
|
78
71
|
table = Table(*columns.values(), show_header=True, header_style="bold magenta")
|
|
72
|
+
|
|
79
73
|
for row in data:
|
|
80
|
-
|
|
74
|
+
row_to_add = []
|
|
75
|
+
for key in columns:
|
|
76
|
+
*path_fragments, field_name = key.strip().split(".")
|
|
77
|
+
base_row = row
|
|
78
|
+
for fragment in path_fragments:
|
|
79
|
+
base_row = getattr(base_row, fragment)
|
|
80
|
+
z = getattr(base_row, field_name)
|
|
81
|
+
row_to_add.append(str(z))
|
|
82
|
+
table.add_row(*row_to_add)
|
|
83
|
+
|
|
81
84
|
console.print(table)
|
|
82
85
|
|
|
83
86
|
|
|
87
|
+
def _get_nested_field(column_name: str, base_fields: dict) -> FieldInfo:
|
|
88
|
+
"""
|
|
89
|
+
Check if column name exists in the model schema.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
column_name: name of the column to check
|
|
93
|
+
base_fields: model fields
|
|
94
|
+
Returns:
|
|
95
|
+
field: nested field
|
|
96
|
+
"""
|
|
97
|
+
fields = base_fields
|
|
98
|
+
*path_fragments, field_name = column_name.strip().split(".")
|
|
99
|
+
for fragment in path_fragments:
|
|
100
|
+
if fragment not in fields:
|
|
101
|
+
Console(stderr=True).print(
|
|
102
|
+
f"Unknown column: {'.'.join(path_fragments + [field_name])} ({fragment} not found)"
|
|
103
|
+
)
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
model_class = fields[fragment].annotation
|
|
106
|
+
if get_origin(model_class) in [UnionType, Optional, Union]:
|
|
107
|
+
types = get_args(model_class)
|
|
108
|
+
model_class = next((t for t in types if t is not type(None)), None)
|
|
109
|
+
if model_class and issubclass(model_class, BaseModel):
|
|
110
|
+
fields = {**model_class.model_fields, **model_class.model_computed_fields}
|
|
111
|
+
if field_name not in fields:
|
|
112
|
+
Console(stderr=True).print(
|
|
113
|
+
f"Unknown column: {'.'.join(path_fragments + [field_name])} ({field_name} not found)"
|
|
114
|
+
)
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
return fields[field_name]
|
|
117
|
+
|
|
118
|
+
|
|
84
119
|
def print_output_json(data: Sequence[ModelT]) -> None:
|
|
85
120
|
"""
|
|
86
121
|
Display data from Pydantic models in a JSON format.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import typer
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic.fields import Field, FieldInfo
|
|
8
|
+
from rich.table import Column, Table
|
|
9
|
+
|
|
10
|
+
from ragbits.cli.state import OutputType, _get_nested_field, print_output, print_output_table
|
|
11
|
+
from ragbits.document_search.documents.sources import LocalFileSource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InnerTestModel(BaseModel):
|
|
15
|
+
id: int
|
|
16
|
+
name: str = Field(title="Name of the inner model", description="Name of the inner model")
|
|
17
|
+
location: LocalFileSource
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OtherTestModel(BaseModel):
|
|
21
|
+
id: int
|
|
22
|
+
name: str
|
|
23
|
+
location: InnerTestModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MainTestModel(BaseModel):
|
|
27
|
+
id: int
|
|
28
|
+
name: str
|
|
29
|
+
model: OtherTestModel | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
data = [
|
|
33
|
+
MainTestModel(
|
|
34
|
+
id=1,
|
|
35
|
+
name="A",
|
|
36
|
+
model=OtherTestModel(
|
|
37
|
+
id=11,
|
|
38
|
+
name="aa",
|
|
39
|
+
location=InnerTestModel(id=111, name="aa1", location=LocalFileSource(path=Path("folder_1"))),
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
MainTestModel(
|
|
43
|
+
id=2,
|
|
44
|
+
name="B",
|
|
45
|
+
model=OtherTestModel(
|
|
46
|
+
id=22,
|
|
47
|
+
name="bb",
|
|
48
|
+
location=InnerTestModel(id=222, name="aa2", location=LocalFileSource(path=Path("folder_2"))),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@patch("ragbits.cli.state.print_output_table")
|
|
55
|
+
@patch("ragbits.cli.state.print_output_json")
|
|
56
|
+
def test_print_output_text(mock_print_output_json: MagicMock, mock_print_output_table: MagicMock):
|
|
57
|
+
with patch("ragbits.cli.state.cli_state") as mock_cli_state:
|
|
58
|
+
mock_cli_state.output_type = OutputType.text
|
|
59
|
+
columns = {"id": Column(), "name": Column()}
|
|
60
|
+
print_output(data, columns=columns)
|
|
61
|
+
mock_print_output_table.assert_called_once_with(data, columns)
|
|
62
|
+
mock_print_output_json.assert_not_called()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@patch("ragbits.cli.state.print_output_table")
|
|
66
|
+
@patch("ragbits.cli.state.print_output_json")
|
|
67
|
+
def test_print_output_json(mock_print_output_json: MagicMock, mock_print_output_table: MagicMock):
|
|
68
|
+
with patch("ragbits.cli.state.cli_state") as mock_cli_state:
|
|
69
|
+
mock_cli_state.output_type = OutputType.json
|
|
70
|
+
print_output(data)
|
|
71
|
+
mock_print_output_table.assert_not_called()
|
|
72
|
+
mock_print_output_json.assert_called_once_with(data)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_print_output_unsupported_output_type():
|
|
76
|
+
with patch("ragbits.cli.state.cli_state") as mock_cli_state:
|
|
77
|
+
mock_cli_state.output_type = "unsupported_type"
|
|
78
|
+
with pytest.raises(ValueError, match="Unsupported output type: unsupported_type"):
|
|
79
|
+
print_output(data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_print_output_table():
|
|
83
|
+
with patch("rich.console.Console.print") as mock_print:
|
|
84
|
+
columns = {"id": Column(), "model.location.location.path": Column(), "model.location.name": Column()}
|
|
85
|
+
print_output_table(data, columns)
|
|
86
|
+
mock_print.assert_called_once()
|
|
87
|
+
args, _ = mock_print.call_args_list[0]
|
|
88
|
+
printed_table = args[0]
|
|
89
|
+
assert isinstance(printed_table, Table)
|
|
90
|
+
assert printed_table.columns[0].header == "Id"
|
|
91
|
+
assert printed_table.columns[1].header == "Model Location Location Path"
|
|
92
|
+
assert printed_table.columns[2].header == "Name of the inner model"
|
|
93
|
+
assert printed_table.row_count == 2
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_get_nested_field():
|
|
97
|
+
column = "model.location.location.path"
|
|
98
|
+
fields = {"name": FieldInfo(annotation=str), "model": FieldInfo(annotation=OtherTestModel)}
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
result = _get_nested_field(column, fields)
|
|
102
|
+
assert result.annotation == Path
|
|
103
|
+
except typer.Exit:
|
|
104
|
+
pytest.fail("typer.Exit was raised unexpectedly")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_get_nested_field_wrong_field():
|
|
108
|
+
column_names = [
|
|
109
|
+
("model.location.wrong_field", "wrong_field"),
|
|
110
|
+
("model.wrong_path.location.path", "wrong_path"),
|
|
111
|
+
("wrong_path.location.location.path", "wrong_path"),
|
|
112
|
+
("model.location.path", "path"),
|
|
113
|
+
("model.location.location.path.additional_field", "additional_field"),
|
|
114
|
+
]
|
|
115
|
+
fields = {"name": FieldInfo(annotation=str), "model": FieldInfo(annotation=OtherTestModel)}
|
|
116
|
+
|
|
117
|
+
for wrong_column, wrong_fragment in column_names:
|
|
118
|
+
with patch("rich.console.Console.print") as mock_print:
|
|
119
|
+
with pytest.raises(typer.Exit, match="1"):
|
|
120
|
+
_get_nested_field(wrong_column, fields)
|
|
121
|
+
mock_print.assert_called_once_with(f"Unknown column: {wrong_column} ({wrong_fragment} not found)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|