sqlsaber-viz 0.1.1__py3-none-any.whl
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.
- sqlsaber_viz/__init__.py +19 -0
- sqlsaber_viz/data_loader.py +143 -0
- sqlsaber_viz/prompts.py +31 -0
- sqlsaber_viz/renderers/__init__.py +6 -0
- sqlsaber_viz/renderers/base.py +13 -0
- sqlsaber_viz/renderers/html_renderer.py +17 -0
- sqlsaber_viz/renderers/plotext_renderer.py +385 -0
- sqlsaber_viz/spec.py +130 -0
- sqlsaber_viz/spec_agent.py +144 -0
- sqlsaber_viz/templates.py +175 -0
- sqlsaber_viz/tools.py +234 -0
- sqlsaber_viz/transforms.py +155 -0
- sqlsaber_viz-0.1.1.dist-info/METADATA +12 -0
- sqlsaber_viz-0.1.1.dist-info/RECORD +16 -0
- sqlsaber_viz-0.1.1.dist-info/WHEEL +4 -0
- sqlsaber_viz-0.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Transform pipeline for visualization data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from .spec import (
|
|
9
|
+
FilterConfig,
|
|
10
|
+
FilterTransform,
|
|
11
|
+
LimitTransform,
|
|
12
|
+
SortItem,
|
|
13
|
+
SortTransform,
|
|
14
|
+
Transform,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def apply_transforms(rows: list[dict], transforms: list[Transform]) -> list[dict]:
|
|
19
|
+
"""Apply transform pipeline to rows."""
|
|
20
|
+
result = list(rows)
|
|
21
|
+
for transform in transforms:
|
|
22
|
+
if isinstance(transform, SortTransform):
|
|
23
|
+
result = apply_sort(result, transform.sort)
|
|
24
|
+
elif isinstance(transform, LimitTransform):
|
|
25
|
+
result = result[: transform.limit]
|
|
26
|
+
elif isinstance(transform, FilterTransform):
|
|
27
|
+
result = apply_filter(result, transform.filter)
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def apply_sort(rows: list[dict], sorts: list[SortItem]) -> list[dict]:
|
|
32
|
+
"""Sort rows by multiple fields."""
|
|
33
|
+
result = list(rows)
|
|
34
|
+
for sort in reversed(sorts):
|
|
35
|
+
field = sort.field
|
|
36
|
+
descending = sort.dir == "desc"
|
|
37
|
+
result = sorted(result, key=lambda row: _sort_key(row.get(field)))
|
|
38
|
+
if descending:
|
|
39
|
+
result.reverse()
|
|
40
|
+
# Always push None/missing values to the end.
|
|
41
|
+
non_none = [row for row in result if row.get(field) is not None]
|
|
42
|
+
none_rows = [row for row in result if row.get(field) is None]
|
|
43
|
+
result = non_none + none_rows
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def apply_filter(rows: list[dict], filter_config: FilterConfig) -> list[dict]:
|
|
48
|
+
"""Filter rows by condition."""
|
|
49
|
+
field = filter_config.field
|
|
50
|
+
op = filter_config.op
|
|
51
|
+
target = filter_config.value
|
|
52
|
+
|
|
53
|
+
filtered: list[dict] = []
|
|
54
|
+
for row in rows:
|
|
55
|
+
value = row.get(field)
|
|
56
|
+
if _compare(value, op, target):
|
|
57
|
+
filtered.append(row)
|
|
58
|
+
return filtered
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _sort_key(value: object) -> tuple[int, object]:
|
|
62
|
+
if value is None:
|
|
63
|
+
return (3, "")
|
|
64
|
+
|
|
65
|
+
numeric = _coerce_number(value)
|
|
66
|
+
if numeric is not None:
|
|
67
|
+
return (0, numeric)
|
|
68
|
+
|
|
69
|
+
time_val = _coerce_time(value)
|
|
70
|
+
if time_val is not None:
|
|
71
|
+
return (1, time_val)
|
|
72
|
+
|
|
73
|
+
return (2, str(value).lower())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _compare(value: object, op: str, target: object) -> bool:
|
|
77
|
+
if op in ("==", "!="):
|
|
78
|
+
result = _equals(value, target)
|
|
79
|
+
return result if op == "==" else not result
|
|
80
|
+
|
|
81
|
+
left_num = _coerce_number(value)
|
|
82
|
+
right_num = _coerce_number(target)
|
|
83
|
+
if left_num is not None and right_num is not None:
|
|
84
|
+
return _compare_ordered(left_num, right_num, op)
|
|
85
|
+
|
|
86
|
+
left_time = _coerce_time(value)
|
|
87
|
+
right_time = _coerce_time(target)
|
|
88
|
+
if left_time is not None and right_time is not None:
|
|
89
|
+
return _compare_ordered(left_time, right_time, op)
|
|
90
|
+
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _equals(value: object, target: object) -> bool:
|
|
95
|
+
if value is None or target is None:
|
|
96
|
+
return value is target
|
|
97
|
+
|
|
98
|
+
left_num = _coerce_number(value)
|
|
99
|
+
right_num = _coerce_number(target)
|
|
100
|
+
if left_num is not None and right_num is not None:
|
|
101
|
+
return left_num == right_num
|
|
102
|
+
|
|
103
|
+
left_time = _coerce_time(value)
|
|
104
|
+
right_time = _coerce_time(target)
|
|
105
|
+
if left_time is not None and right_time is not None:
|
|
106
|
+
return left_time == right_time
|
|
107
|
+
|
|
108
|
+
return value == target
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _compare_ordered(left: object, right: object, op: str) -> bool:
|
|
112
|
+
if op == ">":
|
|
113
|
+
return left > right
|
|
114
|
+
if op == "<":
|
|
115
|
+
return left < right
|
|
116
|
+
if op == ">=":
|
|
117
|
+
return left >= right
|
|
118
|
+
if op == "<=":
|
|
119
|
+
return left <= right
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _coerce_number(value: object) -> float | None:
|
|
124
|
+
if isinstance(value, bool):
|
|
125
|
+
return None
|
|
126
|
+
if isinstance(value, (int, float)):
|
|
127
|
+
return float(value)
|
|
128
|
+
if isinstance(value, str):
|
|
129
|
+
try:
|
|
130
|
+
return float(value)
|
|
131
|
+
except ValueError:
|
|
132
|
+
return None
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _coerce_time(value: object) -> datetime | None:
|
|
137
|
+
if isinstance(value, datetime):
|
|
138
|
+
return value
|
|
139
|
+
if isinstance(value, str):
|
|
140
|
+
# Handle Z suffix (e.g., "2024-01-01T00:00:00Z")
|
|
141
|
+
normalized = value
|
|
142
|
+
if value.endswith("Z"):
|
|
143
|
+
normalized = value[:-1] + "+00:00"
|
|
144
|
+
try:
|
|
145
|
+
return datetime.fromisoformat(normalized)
|
|
146
|
+
except ValueError:
|
|
147
|
+
pass
|
|
148
|
+
# Try YYYY-MM format (e.g., "2023-06")
|
|
149
|
+
if re.match(r"^\d{4}-\d{2}$", value):
|
|
150
|
+
try:
|
|
151
|
+
return datetime.fromisoformat(f"{value}-01")
|
|
152
|
+
except ValueError:
|
|
153
|
+
pass
|
|
154
|
+
return None
|
|
155
|
+
return None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlsaber-viz
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: SQLsaber terminal visualization plugin
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: plotext>=5.3.0
|
|
7
|
+
Requires-Dist: sqlsaber>=0.54.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# SQLSaber Visualization Plugin
|
|
11
|
+
|
|
12
|
+
Provides the `viz` tool for SQLSaber via the `sqlsaber-viz` plugin. The tool generates a declarative chart spec and renders ASCII charts in the terminal using plotext.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
sqlsaber_viz/__init__.py,sha256=HdXfKzS8O7f1qD1nQo9XBPIB5Y6NvIroWmlaulqdvDs,394
|
|
2
|
+
sqlsaber_viz/data_loader.py,sha256=mTFGCd6GZP_9KK110gupKumJQYsg0QoHVGXeqBefYbY,4380
|
|
3
|
+
sqlsaber_viz/prompts.py,sha256=UCoIH32qfv8FguL3T0L77l8n-DjSYR7q_FOrZg-4_3U,1508
|
|
4
|
+
sqlsaber_viz/spec.py,sha256=_zgUBrHPrjztnyECRZycLps1KeEiLn29x9B0fSdqf10,2923
|
|
5
|
+
sqlsaber_viz/spec_agent.py,sha256=LDH5gOd6zw-dno-0yNT7wKR7OnU4ILlH829cdzxOwXc,4694
|
|
6
|
+
sqlsaber_viz/templates.py,sha256=8WDYYVIjEDmKGUo9VPuv_vOOoieqnqGLl39hQpGEWL0,5022
|
|
7
|
+
sqlsaber_viz/tools.py,sha256=d6axy4Ac_TRrJJqqGr9u9k_-5vFHE5XZUVEjzy0jEIM,7756
|
|
8
|
+
sqlsaber_viz/transforms.py,sha256=I_PxG4fy1ckdwC_9in3KVEa9cAQ9oLmq113eEGmvLEk,4440
|
|
9
|
+
sqlsaber_viz/renderers/__init__.py,sha256=oIDE7QqaulKVN_yVKXqERuPskS-Pvxa2ILk234JbkIs,174
|
|
10
|
+
sqlsaber_viz/renderers/base.py,sha256=V-8dCvgvE1AMpVlwOPRxnNwqumUbjlZmwMr-KpculyI,312
|
|
11
|
+
sqlsaber_viz/renderers/html_renderer.py,sha256=f48JyfpcYIcnwCb_zmi9HlWflir94OOMuSVgi8PQhPA,367
|
|
12
|
+
sqlsaber_viz/renderers/plotext_renderer.py,sha256=QFZbaFxvfEr32BCOFRq_fxtZYD_dhAJtbhkcr9VGzP8,13371
|
|
13
|
+
sqlsaber_viz-0.1.1.dist-info/METADATA,sha256=LPq6Wg5944bec4J41-3YreZeUM3K7CPzLnoX2n5ri8M,432
|
|
14
|
+
sqlsaber_viz-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
sqlsaber_viz-0.1.1.dist-info/entry_points.txt,sha256=us67FM9sQiA3XG-UxYbHI9GAKQB5Xpqe_STfQtKEWys,51
|
|
16
|
+
sqlsaber_viz-0.1.1.dist-info/RECORD,,
|