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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [sqlsaber.tools]
2
+ viz = sqlsaber_viz:register_tools