relationalai 1.0.0a3__py3-none-any.whl → 1.0.0a5__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.
- relationalai/config/config.py +47 -21
- relationalai/config/connections/__init__.py +5 -2
- relationalai/config/connections/duckdb.py +2 -2
- relationalai/config/connections/local.py +31 -0
- relationalai/config/connections/snowflake.py +0 -1
- relationalai/config/external/raiconfig_converter.py +235 -0
- relationalai/config/external/raiconfig_models.py +202 -0
- relationalai/config/external/utils.py +31 -0
- relationalai/config/shims.py +1 -0
- relationalai/semantics/__init__.py +10 -8
- relationalai/semantics/backends/sql/sql_compiler.py +1 -4
- relationalai/semantics/experimental/__init__.py +0 -0
- relationalai/semantics/experimental/builder.py +295 -0
- relationalai/semantics/experimental/builtins.py +154 -0
- relationalai/semantics/frontend/base.py +67 -42
- relationalai/semantics/frontend/core.py +34 -6
- relationalai/semantics/frontend/front_compiler.py +209 -37
- relationalai/semantics/frontend/pprint.py +6 -2
- relationalai/semantics/metamodel/__init__.py +7 -0
- relationalai/semantics/metamodel/metamodel.py +2 -0
- relationalai/semantics/metamodel/metamodel_analyzer.py +58 -16
- relationalai/semantics/metamodel/pprint.py +6 -1
- relationalai/semantics/metamodel/rewriter.py +11 -7
- relationalai/semantics/metamodel/typer.py +116 -41
- relationalai/semantics/reasoners/__init__.py +11 -0
- relationalai/semantics/reasoners/graph/__init__.py +35 -0
- relationalai/semantics/reasoners/graph/core.py +9028 -0
- relationalai/semantics/std/__init__.py +30 -10
- relationalai/semantics/std/aggregates.py +641 -12
- relationalai/semantics/std/common.py +146 -13
- relationalai/semantics/std/constraints.py +71 -1
- relationalai/semantics/std/datetime.py +904 -21
- relationalai/semantics/std/decimals.py +143 -2
- relationalai/semantics/std/floats.py +57 -4
- relationalai/semantics/std/integers.py +98 -4
- relationalai/semantics/std/math.py +857 -35
- relationalai/semantics/std/numbers.py +216 -20
- relationalai/semantics/std/re.py +213 -5
- relationalai/semantics/std/strings.py +437 -44
- relationalai/shims/executor.py +60 -52
- relationalai/shims/fixtures.py +85 -0
- relationalai/shims/helpers.py +26 -2
- relationalai/shims/hoister.py +28 -9
- relationalai/shims/mm2v0.py +204 -173
- relationalai/tools/cli/cli.py +192 -10
- relationalai/tools/cli/components/progress_reader.py +1 -1
- relationalai/tools/cli/docs.py +394 -0
- relationalai/tools/debugger.py +11 -4
- relationalai/tools/qb_debugger.py +435 -0
- relationalai/tools/typer_debugger.py +1 -2
- relationalai/util/dataclasses.py +3 -5
- relationalai/util/docutils.py +1 -2
- relationalai/util/error.py +2 -5
- relationalai/util/python.py +23 -0
- relationalai/util/runtime.py +1 -2
- relationalai/util/schema.py +2 -4
- relationalai/util/structures.py +4 -2
- relationalai/util/tracing.py +8 -2
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/METADATA +8 -5
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/RECORD +118 -95
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/WHEEL +1 -1
- v0/relationalai/__init__.py +1 -1
- v0/relationalai/clients/client.py +52 -18
- v0/relationalai/clients/exec_txn_poller.py +122 -0
- v0/relationalai/clients/local.py +23 -8
- v0/relationalai/clients/resources/azure/azure.py +36 -11
- v0/relationalai/clients/resources/snowflake/__init__.py +4 -4
- v0/relationalai/clients/resources/snowflake/cli_resources.py +12 -1
- v0/relationalai/clients/resources/snowflake/direct_access_resources.py +124 -100
- v0/relationalai/clients/resources/snowflake/engine_service.py +381 -0
- v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +35 -29
- v0/relationalai/clients/resources/snowflake/error_handlers.py +43 -2
- v0/relationalai/clients/resources/snowflake/snowflake.py +277 -179
- v0/relationalai/clients/resources/snowflake/use_index_poller.py +8 -0
- v0/relationalai/clients/types.py +5 -0
- v0/relationalai/errors.py +19 -1
- v0/relationalai/semantics/lqp/algorithms.py +173 -0
- v0/relationalai/semantics/lqp/builtins.py +199 -2
- v0/relationalai/semantics/lqp/executor.py +68 -37
- v0/relationalai/semantics/lqp/ir.py +28 -2
- v0/relationalai/semantics/lqp/model2lqp.py +215 -45
- v0/relationalai/semantics/lqp/passes.py +13 -658
- v0/relationalai/semantics/lqp/rewrite/__init__.py +12 -0
- v0/relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
- v0/relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
- v0/relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
- v0/relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
- v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
- v0/relationalai/semantics/lqp/rewrite/period_math.py +77 -0
- v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
- v0/relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
- v0/relationalai/semantics/lqp/utils.py +11 -1
- v0/relationalai/semantics/lqp/validators.py +14 -1
- v0/relationalai/semantics/metamodel/builtins.py +2 -1
- v0/relationalai/semantics/metamodel/compiler.py +2 -1
- v0/relationalai/semantics/metamodel/dependency.py +12 -3
- v0/relationalai/semantics/metamodel/executor.py +11 -1
- v0/relationalai/semantics/metamodel/factory.py +2 -2
- v0/relationalai/semantics/metamodel/helpers.py +7 -0
- v0/relationalai/semantics/metamodel/ir.py +3 -2
- v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
- v0/relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
- v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
- v0/relationalai/semantics/metamodel/typer/checker.py +6 -4
- v0/relationalai/semantics/metamodel/typer/typer.py +4 -3
- v0/relationalai/semantics/metamodel/visitor.py +4 -3
- v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
- v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +336 -86
- v0/relationalai/semantics/rel/compiler.py +2 -1
- v0/relationalai/semantics/rel/executor.py +3 -2
- v0/relationalai/semantics/tests/lqp/__init__.py +0 -0
- v0/relationalai/semantics/tests/lqp/algorithms.py +345 -0
- v0/relationalai/tools/cli.py +339 -186
- v0/relationalai/tools/cli_controls.py +216 -67
- v0/relationalai/tools/cli_helpers.py +410 -6
- v0/relationalai/util/format.py +5 -2
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/entry_points.txt +0 -0
- {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from v0.relationalai.debugging import DEBUG_LOG_FILE # type: ignore[import-not-found]
|
|
7
|
+
|
|
8
|
+
#--------------------------------------------------
|
|
9
|
+
# Terminal nodes
|
|
10
|
+
#--------------------------------------------------
|
|
11
|
+
|
|
12
|
+
TERMINAL_NODES = [
|
|
13
|
+
"query"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
#--------------------------------------------------
|
|
17
|
+
# Debug log helpers
|
|
18
|
+
#--------------------------------------------------
|
|
19
|
+
|
|
20
|
+
class SpanNode:
|
|
21
|
+
def __init__(self, id, type, parent_id, start_timestamp, attrs=None):
|
|
22
|
+
self.id = id
|
|
23
|
+
self.type = type
|
|
24
|
+
self.parent_id = parent_id
|
|
25
|
+
self.start_timestamp = start_timestamp
|
|
26
|
+
self.end_timestamp = None
|
|
27
|
+
self.elapsed = None
|
|
28
|
+
self.start_attrs = attrs or {}
|
|
29
|
+
self.end_attrs = {}
|
|
30
|
+
self.children = []
|
|
31
|
+
|
|
32
|
+
def add_end_data(self, end_timestamp, elapsed, end_attrs=None):
|
|
33
|
+
self.end_timestamp = end_timestamp
|
|
34
|
+
self.elapsed = elapsed
|
|
35
|
+
if end_attrs:
|
|
36
|
+
# Merge end_attrs into attrs
|
|
37
|
+
self.end_attrs.update(end_attrs)
|
|
38
|
+
|
|
39
|
+
def add_child(self, child):
|
|
40
|
+
self.children.append(child)
|
|
41
|
+
|
|
42
|
+
def __str__(self, level=0):
|
|
43
|
+
indent = " " * level
|
|
44
|
+
result = f"{indent}{self.type} ({self.id}):\n"
|
|
45
|
+
result += f"{indent} elapsed: {self.elapsed:.6f}s\n"
|
|
46
|
+
|
|
47
|
+
if self.start_attrs or self.end_attrs:
|
|
48
|
+
result += f"{indent} start attributes:\n"
|
|
49
|
+
for key, value in self.start_attrs.items():
|
|
50
|
+
# Truncate long values for display
|
|
51
|
+
if isinstance(value, str) and len(value) > 100:
|
|
52
|
+
value = value[:97] + "..."
|
|
53
|
+
result += f"{indent} {key}: {value}\n"
|
|
54
|
+
result += f"{indent} end attributes:\n"
|
|
55
|
+
for key, value in self.end_attrs.items():
|
|
56
|
+
# Truncate long values for display
|
|
57
|
+
if isinstance(value, str) and len(value) > 100:
|
|
58
|
+
value = value[:97] + "..."
|
|
59
|
+
result += f"{indent} {key}: {value}\n"
|
|
60
|
+
|
|
61
|
+
if self.children:
|
|
62
|
+
result += f"{indent} children:\n"
|
|
63
|
+
for child in self.children:
|
|
64
|
+
result += child.__str__(level + 2)
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
def parse_jsonl_to_tree(jsonl_content):
|
|
69
|
+
lines = jsonl_content.strip().split('\n')
|
|
70
|
+
nodes_by_id = {}
|
|
71
|
+
root_nodes = []
|
|
72
|
+
|
|
73
|
+
for line in lines:
|
|
74
|
+
data = json.loads(line)
|
|
75
|
+
event_type = data["event"]
|
|
76
|
+
|
|
77
|
+
if event_type == "span_start":
|
|
78
|
+
span = data["span"]
|
|
79
|
+
node = SpanNode(
|
|
80
|
+
id=span["id"],
|
|
81
|
+
type=span["type"],
|
|
82
|
+
parent_id=span["parent_id"],
|
|
83
|
+
start_timestamp=span["start_timestamp"],
|
|
84
|
+
attrs=span.get("attrs", {})
|
|
85
|
+
)
|
|
86
|
+
nodes_by_id[node.id] = node
|
|
87
|
+
|
|
88
|
+
# Link to parent if exists
|
|
89
|
+
if node.parent_id and node.parent_id in nodes_by_id:
|
|
90
|
+
nodes_by_id[node.parent_id].add_child(node)
|
|
91
|
+
elif node.parent_id is None:
|
|
92
|
+
root_nodes.append(node)
|
|
93
|
+
|
|
94
|
+
elif event_type == "span_end":
|
|
95
|
+
node_id = data["id"]
|
|
96
|
+
if node_id in nodes_by_id:
|
|
97
|
+
nodes_by_id[node_id].add_end_data(
|
|
98
|
+
end_timestamp=data["end_timestamp"],
|
|
99
|
+
elapsed=data.get("elapsed", 0),
|
|
100
|
+
end_attrs=data.get("end_attrs", {})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return root_nodes
|
|
104
|
+
|
|
105
|
+
#--------------------------------------------------
|
|
106
|
+
# UI
|
|
107
|
+
#--------------------------------------------------
|
|
108
|
+
|
|
109
|
+
last_mod_time = None
|
|
110
|
+
current_json_objects = []
|
|
111
|
+
active_ix = None
|
|
112
|
+
active_item = None
|
|
113
|
+
|
|
114
|
+
# this is more than toggles now that we accept regex for passes
|
|
115
|
+
toggles = {
|
|
116
|
+
"v1": False,
|
|
117
|
+
"dsl": False,
|
|
118
|
+
"metamodel": False,
|
|
119
|
+
"emitter": False,
|
|
120
|
+
"passes": False,
|
|
121
|
+
"pass_regex": None,
|
|
122
|
+
"type_graph": False,
|
|
123
|
+
"expand_all": False
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
checkboxes = {}
|
|
127
|
+
|
|
128
|
+
opened = set()
|
|
129
|
+
|
|
130
|
+
def toggle_open(span_id):
|
|
131
|
+
global opened
|
|
132
|
+
if span_id in opened:
|
|
133
|
+
opened.remove(span_id)
|
|
134
|
+
else:
|
|
135
|
+
opened.add(span_id)
|
|
136
|
+
sidebar.refresh() # type: ignore[attr-defined]
|
|
137
|
+
details.refresh() # type: ignore[attr-defined]
|
|
138
|
+
|
|
139
|
+
def to_ms(t):
|
|
140
|
+
if not t:
|
|
141
|
+
return "..."
|
|
142
|
+
return f"{t*1000:,.0f}"
|
|
143
|
+
|
|
144
|
+
def format_time(t):
|
|
145
|
+
if not t:
|
|
146
|
+
return "..."
|
|
147
|
+
if t > 1:
|
|
148
|
+
return f"{t:.1f}s"
|
|
149
|
+
elif t > 0.001:
|
|
150
|
+
return f"{t*1000:.1f}ms"
|
|
151
|
+
elif t > 0.0005:
|
|
152
|
+
return f"{t*1000:.2f}ms"
|
|
153
|
+
else:
|
|
154
|
+
return f"{t*1000000:.0f}us"
|
|
155
|
+
|
|
156
|
+
def header(text):
|
|
157
|
+
return ui.label(text).style("font-size: 1.3em; font-weight: bold;")
|
|
158
|
+
|
|
159
|
+
def replace_long_brace_contents(code_str):
|
|
160
|
+
def replacement(match):
|
|
161
|
+
string = match.group(0)
|
|
162
|
+
if len(string) > 300:
|
|
163
|
+
# Extract the first 50 and last 30 characters from the string within brackets
|
|
164
|
+
return '{' + string[1:51] + '...' + string[-31:-1] + '}'
|
|
165
|
+
else:
|
|
166
|
+
# If the string is not longer than 300 characters, return it unchanged
|
|
167
|
+
return string
|
|
168
|
+
|
|
169
|
+
# This regex matches sequences of characters wrapped in { }
|
|
170
|
+
brace_content_regex = r'\{[\s\S]*?\}'
|
|
171
|
+
|
|
172
|
+
# Use the sub method to replace the matched strings with the result of the replacement function
|
|
173
|
+
return re.sub(brace_content_regex, replacement, code_str)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def code(c, language="python"):
|
|
177
|
+
# do not wrap text when outputting Rel
|
|
178
|
+
if language != "rel":
|
|
179
|
+
c = replace_long_brace_contents(c)
|
|
180
|
+
# use python's syntax highlighting for Rel as well
|
|
181
|
+
if language == "rel":
|
|
182
|
+
language = "ruby"
|
|
183
|
+
if language == "lqp":
|
|
184
|
+
language = "clojure"
|
|
185
|
+
c = re.sub(r"→", "->", c)
|
|
186
|
+
c = re.sub(r"⇑", "^", c)
|
|
187
|
+
c = re.sub(r"…", "...", c)
|
|
188
|
+
c = re.sub(r"\?", "<optional>", c)
|
|
189
|
+
return ui.code(c, language=language).style("border:none; margin:0; padding-right: 30px; ").classes("w-full")
|
|
190
|
+
|
|
191
|
+
def code_with_header(header, c, language="python"):
|
|
192
|
+
header = f"# --- {header} {'-' * (40 - len(header))}"
|
|
193
|
+
c = header + "\n\n" + c.strip()
|
|
194
|
+
return code(c, language=language)
|
|
195
|
+
|
|
196
|
+
def span_code(span:SpanNode):
|
|
197
|
+
if span.type == "rule":
|
|
198
|
+
if span.start_attrs.get("source"):
|
|
199
|
+
code(span.start_attrs["source"]).on("click", lambda: toggle_open(span.id)).style("cursor: pointer;")
|
|
200
|
+
if not toggles["expand_all"] and span.id not in opened:
|
|
201
|
+
return
|
|
202
|
+
with ui.column().style("gap:0px; padding-left: 30px; "):
|
|
203
|
+
if toggles["dsl"]:
|
|
204
|
+
code_with_header("dsl", span.start_attrs["dsl"])
|
|
205
|
+
if toggles["metamodel"] and "metamodel" in span.end_attrs:
|
|
206
|
+
code_with_header("metamodel", span.end_attrs["metamodel"])
|
|
207
|
+
elif span.type == "query":
|
|
208
|
+
if span.start_attrs.get("source"):
|
|
209
|
+
code(span.start_attrs["source"]).on("click", lambda: toggle_open(span.id)).style("cursor: pointer;")
|
|
210
|
+
if not toggles["expand_all"] and span.id not in opened:
|
|
211
|
+
return
|
|
212
|
+
with ui.column().style("gap:0px; padding-left: 30px; "):
|
|
213
|
+
if toggles["dsl"]:
|
|
214
|
+
code_with_header("dsl", span.start_attrs["dsl"])
|
|
215
|
+
for child in span.children:
|
|
216
|
+
span_code(child)
|
|
217
|
+
elif span.type == "compile" and span.end_attrs.get("compile_type") == "model":
|
|
218
|
+
if toggles["metamodel"]:
|
|
219
|
+
code_with_header("model metamodel", span.start_attrs["metamodel"])
|
|
220
|
+
for child in span.children:
|
|
221
|
+
span_code(child)
|
|
222
|
+
if toggles["emitter"]:
|
|
223
|
+
if "rel" in span.end_attrs:
|
|
224
|
+
code_with_header("rel model", span.end_attrs["rel"], language="rel")
|
|
225
|
+
elif "lqp" in span.end_attrs:
|
|
226
|
+
code_with_header("lqp model", span.end_attrs["lqp"], language="lqp")
|
|
227
|
+
# else:
|
|
228
|
+
# code_with_header("model", "# No code was emitted", language="python")
|
|
229
|
+
elif span.type == "compile" and span.end_attrs.get("compile_type") == "query":
|
|
230
|
+
if toggles["metamodel"]:
|
|
231
|
+
code_with_header("metamodel", span.start_attrs["metamodel"])
|
|
232
|
+
for child in span.children:
|
|
233
|
+
span_code(child)
|
|
234
|
+
if toggles["emitter"]:
|
|
235
|
+
if "rel" in span.end_attrs:
|
|
236
|
+
code_with_header("rel query", span.end_attrs["rel"], language="rel")
|
|
237
|
+
elif "lqp" in span.end_attrs:
|
|
238
|
+
code_with_header("lqp query", span.end_attrs["lqp"], language="lqp")
|
|
239
|
+
# else:
|
|
240
|
+
# code_with_header("query", "# No code was emitted", language="python")
|
|
241
|
+
elif span.type == "passes":
|
|
242
|
+
type_graph_shown = False
|
|
243
|
+
if toggles["passes"]:
|
|
244
|
+
previous_code = None
|
|
245
|
+
for child in span.children:
|
|
246
|
+
if toggles["pass_regex"] and not re.match(toggles["pass_regex"], child.type):
|
|
247
|
+
continue
|
|
248
|
+
current_code = ""
|
|
249
|
+
if "metamodel" in child.end_attrs:
|
|
250
|
+
current_code = child.end_attrs["metamodel"]
|
|
251
|
+
if current_code == previous_code:
|
|
252
|
+
current_code = ""
|
|
253
|
+
else:
|
|
254
|
+
previous_code = current_code
|
|
255
|
+
code_element = code_with_header(child.type + f' ({format_time(child.elapsed)})', current_code)
|
|
256
|
+
code_element.props(f'id="pass-{child.id}"')
|
|
257
|
+
code_element.style("scroll-margin-top: 40px;")
|
|
258
|
+
if child.type == "InferTypes" and toggles["type_graph"]:
|
|
259
|
+
# show the type graph inline with InferTypes
|
|
260
|
+
type_graph_shown = True
|
|
261
|
+
for child2 in child.children:
|
|
262
|
+
if child2.end_attrs.get("type_graph"):
|
|
263
|
+
ui.mermaid(child2.end_attrs['type_graph'], {"maxEdges":10000}).style("display:flex; padding:0px 0px 0px 15px; gap:30px; overflow: auto;")
|
|
264
|
+
# show the type graph if it was not shown inline above
|
|
265
|
+
if toggles["type_graph"] and not type_graph_shown:
|
|
266
|
+
for child in span.children:
|
|
267
|
+
if child.type == "InferTypes":
|
|
268
|
+
for child2 in child.children:
|
|
269
|
+
if child2.end_attrs.get("type_graph"):
|
|
270
|
+
ui.mermaid(child2.end_attrs['type_graph'], {"maxEdges":10000}).style("display:flex; padding:0px 0px 0px 15px; gap:30px; overflow: auto;")
|
|
271
|
+
elif span.type == "v1":
|
|
272
|
+
if toggles["v1"]:
|
|
273
|
+
previous_code = None
|
|
274
|
+
for child in span.children:
|
|
275
|
+
current_code = ""
|
|
276
|
+
if "metamodel" in child.end_attrs:
|
|
277
|
+
current_code = child.end_attrs["metamodel"]
|
|
278
|
+
if current_code == previous_code:
|
|
279
|
+
current_code = ""
|
|
280
|
+
else:
|
|
281
|
+
previous_code = current_code
|
|
282
|
+
code_element = code_with_header(child.type + f' ({format_time(child.elapsed)})', current_code)
|
|
283
|
+
code_element.props(f'id="pass-{child.id}"')
|
|
284
|
+
code_element.style("scroll-margin-top: 40px;")
|
|
285
|
+
else:
|
|
286
|
+
for child in span.children:
|
|
287
|
+
span_code(child)
|
|
288
|
+
|
|
289
|
+
@ui.refreshable
|
|
290
|
+
def details():
|
|
291
|
+
with ui.column().style("gap:2px;"):
|
|
292
|
+
for root in current_json_objects:
|
|
293
|
+
span_code(root)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def handle_attributes(attrs):
|
|
297
|
+
if attrs.get("file"):
|
|
298
|
+
ui.label(f"{attrs['file']}: {attrs['line']}")
|
|
299
|
+
if attrs.get("source"):
|
|
300
|
+
code(attrs["source"])
|
|
301
|
+
if attrs.get("txn_id"):
|
|
302
|
+
ui.label(f"txn_id: {attrs['txn_id']}")
|
|
303
|
+
if attrs.get("name"):
|
|
304
|
+
ui.label(f"{attrs['name']}")
|
|
305
|
+
if attrs.get("code"):
|
|
306
|
+
code(attrs["code"])
|
|
307
|
+
if attrs.get("dsl"):
|
|
308
|
+
code(attrs["dsl"])
|
|
309
|
+
if attrs.get("metamodel"):
|
|
310
|
+
code(attrs["metamodel"])
|
|
311
|
+
if attrs.get("rel"):
|
|
312
|
+
code(attrs["rel"], language="rel")
|
|
313
|
+
if attrs.get("sql"):
|
|
314
|
+
code(attrs["sql"], language="sql")
|
|
315
|
+
if attrs.get("results"):
|
|
316
|
+
vals = attrs['results']
|
|
317
|
+
if len(vals):
|
|
318
|
+
keys = [k for k in vals[0].keys()]
|
|
319
|
+
columns = [{'name': k, 'label': k, 'field': k, "align": "left"} for k in keys]
|
|
320
|
+
ui.table(columns=columns, rows=vals)
|
|
321
|
+
|
|
322
|
+
def handle_body(span: SpanNode):
|
|
323
|
+
if span.children and span.type not in ["rule_batch"]:
|
|
324
|
+
for child in span.children:
|
|
325
|
+
span_ui(child, True)
|
|
326
|
+
|
|
327
|
+
def span_ui(span: SpanNode, is_pass: bool=False):
|
|
328
|
+
def jump_to_pass():
|
|
329
|
+
global toggles
|
|
330
|
+
checkboxes["passes"].set_value(True)
|
|
331
|
+
toggles["passes"] = True
|
|
332
|
+
sidebar.refresh() # type: ignore[attr-defined]
|
|
333
|
+
details.refresh() # type: ignore[attr-defined]
|
|
334
|
+
ui.navigate.to(f"#pass-{span.id}")
|
|
335
|
+
|
|
336
|
+
with ui.column().classes("w-full").style("padding:0px 0px 0px 15px; gap:2px; "):
|
|
337
|
+
with ui.row().classes("w-full").style("padding-left:5px; flex-wrap:nowrap; margin:0; background: #2c2c2c") as me:
|
|
338
|
+
name = span.type
|
|
339
|
+
if name == "create_v2":
|
|
340
|
+
name += " ➚"
|
|
341
|
+
ui.label(name).style("padding:0; margin:0; color: #DDD;")
|
|
342
|
+
ui.space()
|
|
343
|
+
ui.label(f"{to_ms(span.elapsed)}").style("padding-right: 5px; color:#999;")
|
|
344
|
+
if span.type == "create_v2":
|
|
345
|
+
txn_id = span.end_attrs.get("txn_id")
|
|
346
|
+
me.on("click", lambda: ui.navigate.to(f"https://171608476159.observeinc.com/workspace/41759331/log-explorer?datasetId=41832558&filter-rai_transaction_id={txn_id}", new_tab=True))
|
|
347
|
+
me.style("cursor: pointer;")
|
|
348
|
+
elif is_pass:
|
|
349
|
+
me.on("click", lambda: jump_to_pass())
|
|
350
|
+
me.style("cursor: pointer;")
|
|
351
|
+
handle_body(span)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@ui.refreshable
|
|
355
|
+
def sidebar():
|
|
356
|
+
with ui.column().style("min-width: 270px; max-width: 270px; margin-top: 50px; height: calc(100vh - 60px); overflow-x: hidden; overflow-y: auto; position: fixed; top: 0; right: 0; z-index: 1000;"):
|
|
357
|
+
for root in current_json_objects:
|
|
358
|
+
span_ui(root)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def poll():
|
|
362
|
+
global last_mod_time, active_item
|
|
363
|
+
global current_json_objects
|
|
364
|
+
# Check the last modification time of the file
|
|
365
|
+
try:
|
|
366
|
+
mod_time = os.path.getmtime(DEBUG_LOG_FILE)
|
|
367
|
+
if last_mod_time is None or mod_time > last_mod_time:
|
|
368
|
+
last_mod_time = mod_time
|
|
369
|
+
# File has changed, read and parse the new content
|
|
370
|
+
with open(DEBUG_LOG_FILE, 'r') as file:
|
|
371
|
+
content = file.read()
|
|
372
|
+
if content:
|
|
373
|
+
new_tree = parse_jsonl_to_tree(content)
|
|
374
|
+
# Update the current JSON objects
|
|
375
|
+
current_json_objects = new_tree
|
|
376
|
+
|
|
377
|
+
if active_ix is not None and len(current_json_objects) > active_ix:
|
|
378
|
+
active_item = current_json_objects[active_ix]
|
|
379
|
+
# Refresh the UI with the new objects
|
|
380
|
+
sidebar.refresh() # type: ignore[attr-defined]
|
|
381
|
+
details.refresh() # type: ignore[attr-defined]
|
|
382
|
+
except FileNotFoundError:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
def toggle(key):
|
|
386
|
+
global toggles
|
|
387
|
+
toggles[key] = not toggles[key]
|
|
388
|
+
sidebar.refresh() # type: ignore[attr-defined]
|
|
389
|
+
details.refresh() # type: ignore[attr-defined]
|
|
390
|
+
|
|
391
|
+
def set_toggle_value(key, value):
|
|
392
|
+
global toggles
|
|
393
|
+
toggles[key] = value
|
|
394
|
+
sidebar.refresh() # type: ignore[attr-defined]
|
|
395
|
+
details.refresh() # type: ignore[attr-defined]
|
|
396
|
+
|
|
397
|
+
def main(host="0.0.0.0", port=8080):
|
|
398
|
+
global checkboxes
|
|
399
|
+
global toggles
|
|
400
|
+
ui.dark_mode(None)
|
|
401
|
+
with ui.column():
|
|
402
|
+
with ui.row().style("flex-wrap: nowrap; position: fixed; top: 0; left: 0; right: 0; z-index: 1000; background: #3c3c3c; color: #DDD;"):
|
|
403
|
+
ui.switch('expand all', value=toggles["expand_all"], on_change=lambda: toggle("expand_all"))
|
|
404
|
+
checkboxes["dsl"] = ui.checkbox('dsl', on_change=lambda: toggle("dsl"))
|
|
405
|
+
checkboxes["v1"] = ui.checkbox('v1 passes', on_change=lambda: toggle("v1"))
|
|
406
|
+
checkboxes["passes"] = ui.checkbox('v0 passes', on_change=lambda: toggle("passes")).props('id=passes_box')
|
|
407
|
+
checkboxes["emitter"] = ui.checkbox('emitter', on_change=lambda: toggle("emitter"))
|
|
408
|
+
ui.button("↓", on_click=lambda: ui.run_javascript("window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})")).style(
|
|
409
|
+
"margin: 2px;"
|
|
410
|
+
"border-radius: 20%; "
|
|
411
|
+
"border: 2px solid #666; "
|
|
412
|
+
).props("title='Scroll to bottom'")
|
|
413
|
+
ui.button("↑", on_click=lambda: ui.run_javascript("window.scrollTo({top: 0, behavior: 'smooth'})")).style(
|
|
414
|
+
"margin: 2px;"
|
|
415
|
+
"border-radius: 20%; "
|
|
416
|
+
"border: 2px solid #666; "
|
|
417
|
+
).props("title='Scroll to top'")
|
|
418
|
+
|
|
419
|
+
checkboxes["metamodel"] = ui.checkbox('v0 metamodel', on_change=lambda: toggle("metamodel"))
|
|
420
|
+
checkboxes["type_graph"] = ui.checkbox('v0 type graph', on_change=lambda: toggle("type_graph"))
|
|
421
|
+
ui.input(label='Filter passes by regex', on_change=lambda e: set_toggle_value("pass_regex", e.value)
|
|
422
|
+
).props('clearable input-style="color: white" input-class="font-mono" flat dense')
|
|
423
|
+
|
|
424
|
+
with ui.row().style("flex-wrap: nowrap; margin-top: 30px;"):
|
|
425
|
+
with ui.column() as c:
|
|
426
|
+
c.style("padding-left: 2em;")
|
|
427
|
+
details()
|
|
428
|
+
with ui.column() as c:
|
|
429
|
+
sidebar()
|
|
430
|
+
|
|
431
|
+
ui.timer(1, poll)
|
|
432
|
+
ui.run(reload=False, host=host, port=port)
|
|
433
|
+
|
|
434
|
+
if __name__ in {"__main__", "__mp_main__"}:
|
|
435
|
+
main()
|
|
@@ -2,7 +2,6 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
from nicegui import ui
|
|
4
4
|
import json
|
|
5
|
-
import dataclasses
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
#--------------------------------------------------
|
|
@@ -32,7 +31,7 @@ def poll():
|
|
|
32
31
|
content = file.read()
|
|
33
32
|
if content:
|
|
34
33
|
current_content = parse_file(content)
|
|
35
|
-
view.refresh()
|
|
34
|
+
view.refresh() # type: ignore[attr-defined]
|
|
36
35
|
except FileNotFoundError:
|
|
37
36
|
pass
|
|
38
37
|
|
relationalai/util/dataclasses.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from dataclasses import
|
|
2
|
-
from functools import lru_cache
|
|
3
|
-
from typing import Tuple, Any
|
|
1
|
+
from dataclasses import fields, is_dataclass
|
|
4
2
|
|
|
5
3
|
def is_dataclass_instance(obj) -> bool:
|
|
6
4
|
"""Checks if the object is a dataclass instance, excluding the class itself."""
|
|
@@ -27,11 +25,11 @@ def print_tree(node, field_name=None, indent=0, hide_fields=[], seen=set()):
|
|
|
27
25
|
elif isinstance(node, list) or isinstance(node, tuple):
|
|
28
26
|
# just ignore sequences that are empty
|
|
29
27
|
if node:
|
|
30
|
-
_indent_print(indent,
|
|
28
|
+
_indent_print(indent, "[", field_name)
|
|
31
29
|
next = indent + 2
|
|
32
30
|
for item in node:
|
|
33
31
|
print_tree(item, None, next, hide_fields)
|
|
34
|
-
_indent_print(indent,
|
|
32
|
+
_indent_print(indent, "]")
|
|
35
33
|
else:
|
|
36
34
|
_indent_print(indent, str(node), field_name)
|
|
37
35
|
|
relationalai/util/docutils.py
CHANGED
relationalai/util/error.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass
|
|
2
2
|
from io import StringIO
|
|
3
3
|
from typing import Any, NoReturn, Optional
|
|
4
4
|
import html
|
|
5
5
|
|
|
6
6
|
from .source import SourcePos
|
|
7
|
-
from .tracing import get_tracer
|
|
8
7
|
|
|
9
8
|
from rich import box
|
|
10
9
|
from rich.console import Console, Group
|
|
11
10
|
from rich.panel import Panel
|
|
12
|
-
from rich.rule import Rule
|
|
13
11
|
from rich.syntax import Syntax
|
|
14
12
|
from rich.table import Table as RichTable
|
|
15
13
|
from rich.text import Text
|
|
@@ -70,7 +68,6 @@ class RichEmitter:
|
|
|
70
68
|
# write to our buffer, not the real terminal
|
|
71
69
|
console = Console(file=buf, record=False, color_system="auto", soft_wrap=False, force_terminal=True)
|
|
72
70
|
sev_style = {"error": "bold red", "warning": "bold yellow", "info": "bold cyan"}.get(diag.severity, "bold")
|
|
73
|
-
sev_sub_style = {"error": "red", "warning": "yellow", "info": "cyan"}.get(diag.severity, "cyan")
|
|
74
71
|
|
|
75
72
|
# Title
|
|
76
73
|
title_text = Text(f"[{diag.name}] ", style=sev_style)
|
|
@@ -196,4 +193,4 @@ def source(obj:Any) -> Source:
|
|
|
196
193
|
return Source(source)
|
|
197
194
|
|
|
198
195
|
def table(headers: list[str] = [], rows: list[list[str]] = []) -> Table:
|
|
199
|
-
return Table(headers=headers, rows=rows)
|
|
196
|
+
return Table(headers=headers, rows=rows)
|
relationalai/util/python.py
CHANGED
|
@@ -3,11 +3,33 @@ from decimal import Decimal as PyDecimal
|
|
|
3
3
|
from types import NoneType
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pandas as pd
|
|
6
|
+
import datetime as dt
|
|
7
|
+
from pandas import DataFrame
|
|
6
8
|
|
|
7
9
|
#--------------------------------------------------
|
|
8
10
|
# Python types
|
|
9
11
|
#--------------------------------------------------
|
|
10
12
|
|
|
13
|
+
def column_to_concept_name(df:DataFrame, col) -> str:
|
|
14
|
+
""" Infer the type of the column in the dataframe and return the corresponding concept name. """
|
|
15
|
+
_type = df[col].dtype
|
|
16
|
+
if pd.api.types.is_datetime64_any_dtype(_type):
|
|
17
|
+
return "DateTime"
|
|
18
|
+
elif pd.api.types.is_object_dtype(_type) and is_date_column(df[col]):
|
|
19
|
+
return "Date"
|
|
20
|
+
elif pd.api.types.is_string_dtype(_type):
|
|
21
|
+
return "String"
|
|
22
|
+
else:
|
|
23
|
+
return pytype_to_concept_name.get(_type, "Any")
|
|
24
|
+
|
|
25
|
+
def is_date_column(col) -> bool:
|
|
26
|
+
""" Check whether the dataframe column contains date or datetime values. """
|
|
27
|
+
sample = col.dropna()
|
|
28
|
+
if sample.empty:
|
|
29
|
+
return False
|
|
30
|
+
sample_value = sample.iloc[0]
|
|
31
|
+
return isinstance(sample_value, dt.date) and not isinstance(sample_value, dt.datetime)
|
|
32
|
+
|
|
11
33
|
pytype_to_concept_name: dict[object, str] = {
|
|
12
34
|
NoneType: "Any",
|
|
13
35
|
int: "Integer", float: "Number(38,14)", str: "String", bool: "Boolean",
|
|
@@ -27,6 +49,7 @@ pytype_to_concept_name: dict[object, str] = {
|
|
|
27
49
|
**{t(): "Integer" for t in (pd.Int8Dtype, pd.Int16Dtype, pd.Int32Dtype, pd.Int64Dtype,
|
|
28
50
|
pd.UInt8Dtype, pd.UInt16Dtype, pd.UInt32Dtype, pd.UInt64Dtype)},
|
|
29
51
|
**{t(): "Number(38,14)" for t in (pd.Float32Dtype, pd.Float64Dtype)},
|
|
52
|
+
pd.StringDtype(na_value=np.nan): "String",
|
|
30
53
|
pd.StringDtype(): "String",
|
|
31
54
|
pd.BooleanDtype(): "Boolean",
|
|
32
55
|
"int": "Integer", "float": "Number(38,14)", "str": "String", "bool": "Boolean",
|
relationalai/util/runtime.py
CHANGED
|
@@ -4,7 +4,6 @@ import warnings
|
|
|
4
4
|
import more_itertools
|
|
5
5
|
from .tracing import get_tracer
|
|
6
6
|
from .error import Diagnostic, RAIWarning, RichEmitter, HTMLEmitter, RAIException
|
|
7
|
-
import traceback
|
|
8
7
|
|
|
9
8
|
#------------------------------------------------------
|
|
10
9
|
# Base Env
|
|
@@ -47,7 +46,7 @@ class EnvBase:
|
|
|
47
46
|
if self.prefers_html:
|
|
48
47
|
# In notebook-y envs, try to display inline
|
|
49
48
|
try:
|
|
50
|
-
from IPython.display import display, HTML as _HTML
|
|
49
|
+
from IPython.display import display, HTML as _HTML # type: ignore[import-not-found]
|
|
51
50
|
display(_HTML(rendered))
|
|
52
51
|
except Exception:
|
|
53
52
|
sys.stderr.write("[HTML diagnostic suppressed]\n")
|
relationalai/util/schema.py
CHANGED
|
@@ -6,8 +6,6 @@ import re
|
|
|
6
6
|
import textwrap
|
|
7
7
|
|
|
8
8
|
from relationalai.util.error import warn
|
|
9
|
-
from ..config import Config
|
|
10
|
-
from ..config.connections.snowflake import SnowflakeConnection
|
|
11
9
|
|
|
12
10
|
# config = Config()
|
|
13
11
|
# conn = config.get_connection(SnowflakeConnection, name="snowflake")
|
|
@@ -112,7 +110,7 @@ def fetch_snowflake(database:str, schema:str, table_names:list[str]) -> dict[str
|
|
|
112
110
|
# with debugging.span("fetch_schema", sql=query):
|
|
113
111
|
try:
|
|
114
112
|
columns = get_provider().sql(query)
|
|
115
|
-
except Exception
|
|
113
|
+
except Exception:
|
|
116
114
|
columns = []
|
|
117
115
|
assert isinstance(columns, list)
|
|
118
116
|
|
|
@@ -192,6 +190,6 @@ def sf_numeric_to_type_str(column_name: str, sf_type_info: dict) -> str:
|
|
|
192
190
|
# Integers (load_csv only supports these two (and not 8/16/32 bit ints)
|
|
193
191
|
bits = digits_to_bits(precision)
|
|
194
192
|
# return Int128 if bits == 128 else Int64
|
|
195
|
-
return
|
|
193
|
+
return "Decimal(38,0)" if bits == 128 else "Decimal(18,0)"
|
|
196
194
|
|
|
197
195
|
return f"Decimal({precision},{scale})"
|
relationalai/util/structures.py
CHANGED
|
@@ -91,8 +91,10 @@ class KeyedDict(Generic[K, V]):
|
|
|
91
91
|
del self._dict[self._key(k)]
|
|
92
92
|
|
|
93
93
|
def __contains__(self, k: object) -> bool:
|
|
94
|
-
try:
|
|
95
|
-
|
|
94
|
+
try:
|
|
95
|
+
return self._key(k) in self._dict # type: ignore[arg-type]
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
96
98
|
|
|
97
99
|
def __len__(self) -> int:
|
|
98
100
|
return len(self._dict)
|
relationalai/util/tracing.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# tracer.py — stdlib-only OTEL-shaped tracer with live span_start + 200ms write throttling
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
import os
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import secrets
|
|
7
|
+
import threading
|
|
8
|
+
import atexit
|
|
4
9
|
from contextlib import ContextDecorator, asynccontextmanager
|
|
5
10
|
from contextvars import ContextVar
|
|
6
11
|
from typing import Optional, Callable, Any, Dict
|
|
@@ -160,7 +165,8 @@ class Tracer(ContextDecorator):
|
|
|
160
165
|
|
|
161
166
|
def traceparent(self) -> str:
|
|
162
167
|
s = self.current_span()
|
|
163
|
-
if not s:
|
|
168
|
+
if not s:
|
|
169
|
+
return ""
|
|
164
170
|
# version(00)-trace_id-span_id-flags(01)
|
|
165
171
|
return f"00-{s.trace_id}-{s.span_id}-01"
|
|
166
172
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: relationalai
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a5
|
|
4
4
|
Summary: RelationalAI Library and CLI
|
|
5
5
|
Author-email: RelationalAI <support@relational.ai>
|
|
6
|
-
Requires-Python:
|
|
6
|
+
Requires-Python: <3.14,>=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: lqp
|
|
8
|
+
Requires-Dist: lqp
|
|
9
9
|
Requires-Dist: snowflake-connector-python[secure-local-storage]
|
|
10
10
|
Requires-Dist: snowflake-snowpark-python>=1.38.0
|
|
11
11
|
Requires-Dist: typing-extensions
|
|
@@ -15,10 +15,9 @@ Requires-Dist: numpy<2
|
|
|
15
15
|
Requires-Dist: pandas
|
|
16
16
|
Requires-Dist: colorama
|
|
17
17
|
Requires-Dist: inquirerpy
|
|
18
|
-
Requires-Dist: click
|
|
18
|
+
Requires-Dist: click
|
|
19
19
|
Requires-Dist: gravis
|
|
20
20
|
Requires-Dist: toml
|
|
21
|
-
Requires-Dist: tomlkit
|
|
22
21
|
Requires-Dist: requests
|
|
23
22
|
Requires-Dist: pyarrow
|
|
24
23
|
Requires-Dist: websockets
|
|
@@ -33,8 +32,12 @@ Requires-Dist: pyjwt
|
|
|
33
32
|
Requires-Dist: more-itertools
|
|
34
33
|
Requires-Dist: sqlglot
|
|
35
34
|
Requires-Dist: pyyaml
|
|
35
|
+
Requires-Dist: confocal
|
|
36
36
|
Requires-Dist: pydantic<2.12.0,>=2.0.0
|
|
37
37
|
Requires-Dist: pydantic-settings<2.11.0,>=2.0.0
|
|
38
|
+
Requires-Dist: tomlkit
|
|
39
|
+
Requires-Dist: fastapi
|
|
40
|
+
Requires-Dist: uvicorn[standard]
|
|
38
41
|
Provides-Extra: dev
|
|
39
42
|
Requires-Dist: pytest; extra == "dev"
|
|
40
43
|
Requires-Dist: pytest-cov; extra == "dev"
|