pytrilogy 0.0.1.102__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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (77) hide show
  1. pytrilogy-0.0.1.102.dist-info/LICENSE.md +19 -0
  2. pytrilogy-0.0.1.102.dist-info/METADATA +277 -0
  3. pytrilogy-0.0.1.102.dist-info/RECORD +77 -0
  4. pytrilogy-0.0.1.102.dist-info/WHEEL +5 -0
  5. pytrilogy-0.0.1.102.dist-info/entry_points.txt +2 -0
  6. pytrilogy-0.0.1.102.dist-info/top_level.txt +1 -0
  7. trilogy/__init__.py +8 -0
  8. trilogy/compiler.py +0 -0
  9. trilogy/constants.py +30 -0
  10. trilogy/core/__init__.py +0 -0
  11. trilogy/core/constants.py +3 -0
  12. trilogy/core/enums.py +270 -0
  13. trilogy/core/env_processor.py +33 -0
  14. trilogy/core/environment_helpers.py +156 -0
  15. trilogy/core/ergonomics.py +187 -0
  16. trilogy/core/exceptions.py +23 -0
  17. trilogy/core/functions.py +320 -0
  18. trilogy/core/graph_models.py +55 -0
  19. trilogy/core/internal.py +37 -0
  20. trilogy/core/models.py +3145 -0
  21. trilogy/core/processing/__init__.py +0 -0
  22. trilogy/core/processing/concept_strategies_v3.py +603 -0
  23. trilogy/core/processing/graph_utils.py +44 -0
  24. trilogy/core/processing/node_generators/__init__.py +25 -0
  25. trilogy/core/processing/node_generators/basic_node.py +71 -0
  26. trilogy/core/processing/node_generators/common.py +239 -0
  27. trilogy/core/processing/node_generators/concept_merge.py +152 -0
  28. trilogy/core/processing/node_generators/filter_node.py +83 -0
  29. trilogy/core/processing/node_generators/group_node.py +92 -0
  30. trilogy/core/processing/node_generators/group_to_node.py +99 -0
  31. trilogy/core/processing/node_generators/merge_node.py +148 -0
  32. trilogy/core/processing/node_generators/multiselect_node.py +189 -0
  33. trilogy/core/processing/node_generators/rowset_node.py +130 -0
  34. trilogy/core/processing/node_generators/select_node.py +328 -0
  35. trilogy/core/processing/node_generators/unnest_node.py +37 -0
  36. trilogy/core/processing/node_generators/window_node.py +85 -0
  37. trilogy/core/processing/nodes/__init__.py +76 -0
  38. trilogy/core/processing/nodes/base_node.py +251 -0
  39. trilogy/core/processing/nodes/filter_node.py +49 -0
  40. trilogy/core/processing/nodes/group_node.py +110 -0
  41. trilogy/core/processing/nodes/merge_node.py +326 -0
  42. trilogy/core/processing/nodes/select_node_v2.py +198 -0
  43. trilogy/core/processing/nodes/unnest_node.py +54 -0
  44. trilogy/core/processing/nodes/window_node.py +34 -0
  45. trilogy/core/processing/utility.py +278 -0
  46. trilogy/core/query_processor.py +331 -0
  47. trilogy/dialect/__init__.py +0 -0
  48. trilogy/dialect/base.py +679 -0
  49. trilogy/dialect/bigquery.py +80 -0
  50. trilogy/dialect/common.py +43 -0
  51. trilogy/dialect/config.py +55 -0
  52. trilogy/dialect/duckdb.py +83 -0
  53. trilogy/dialect/enums.py +95 -0
  54. trilogy/dialect/postgres.py +86 -0
  55. trilogy/dialect/presto.py +82 -0
  56. trilogy/dialect/snowflake.py +82 -0
  57. trilogy/dialect/sql_server.py +89 -0
  58. trilogy/docs/__init__.py +0 -0
  59. trilogy/engine.py +48 -0
  60. trilogy/executor.py +242 -0
  61. trilogy/hooks/__init__.py +0 -0
  62. trilogy/hooks/base_hook.py +37 -0
  63. trilogy/hooks/graph_hook.py +24 -0
  64. trilogy/hooks/query_debugger.py +133 -0
  65. trilogy/metadata/__init__.py +0 -0
  66. trilogy/parser.py +10 -0
  67. trilogy/parsing/__init__.py +0 -0
  68. trilogy/parsing/common.py +176 -0
  69. trilogy/parsing/config.py +5 -0
  70. trilogy/parsing/exceptions.py +2 -0
  71. trilogy/parsing/helpers.py +1 -0
  72. trilogy/parsing/parse_engine.py +1951 -0
  73. trilogy/parsing/render.py +483 -0
  74. trilogy/py.typed +0 -0
  75. trilogy/scripts/__init__.py +0 -0
  76. trilogy/scripts/trilogy.py +127 -0
  77. trilogy/utility.py +31 -0
@@ -0,0 +1,483 @@
1
+ from functools import singledispatchmethod
2
+
3
+ from jinja2 import Template
4
+
5
+ from trilogy.constants import DEFAULT_NAMESPACE, MagicConstants, VIRTUAL_CONCEPT_PREFIX
6
+ from trilogy.core.enums import Purpose, ConceptSource, DatePart, FunctionType
7
+ from trilogy.core.models import (
8
+ DataType,
9
+ Address,
10
+ Query,
11
+ Concept,
12
+ ConceptTransform,
13
+ Function,
14
+ Grain,
15
+ OrderItem,
16
+ SelectStatement,
17
+ SelectItem,
18
+ WhereClause,
19
+ Conditional,
20
+ Comparison,
21
+ Environment,
22
+ ConceptDeclarationStatement,
23
+ ConceptDerivation,
24
+ Datasource,
25
+ WindowItem,
26
+ FilterItem,
27
+ ColumnAssignment,
28
+ CaseElse,
29
+ CaseWhen,
30
+ ImportStatement,
31
+ Parenthetical,
32
+ AggregateWrapper,
33
+ PersistStatement,
34
+ ListWrapper,
35
+ RowsetDerivationStatement,
36
+ MergeStatement,
37
+ MultiSelectStatement,
38
+ OrderBy,
39
+ AlignClause,
40
+ AlignItem,
41
+ )
42
+ from trilogy.core.enums import Modifier
43
+
44
+ from collections import defaultdict
45
+
46
+
47
+ QUERY_TEMPLATE = Template(
48
+ """SELECT{%- for select in select_columns %}
49
+ {{ select }},{% endfor %}{% if where %}
50
+ WHERE
51
+ {{ where }}{% endif %}{%- if order_by %}
52
+ ORDER BY{% for order in order_by %}
53
+ {{ order }}{% if not loop.last %},{% endif %}
54
+ {% endfor %}{% endif %}{%- if limit is not none %}
55
+ LIMIT {{ limit }}{% endif %};"""
56
+ )
57
+
58
+
59
+ class Renderer:
60
+ @singledispatchmethod
61
+ def to_string(self, arg):
62
+ raise NotImplementedError("Cannot render type {}".format(type(arg)))
63
+
64
+ @to_string.register
65
+ def _(self, arg: Environment):
66
+ output_concepts = []
67
+ constants: list[Concept] = []
68
+ keys: list[Concept] = []
69
+ properties = defaultdict(list)
70
+ metrics = []
71
+ # first, keys
72
+ for concept in arg.concepts.values():
73
+ # don't render anything that came from an import
74
+ if concept.namespace in arg.imports:
75
+ continue
76
+ if (
77
+ concept.metadata
78
+ and concept.metadata.concept_source == ConceptSource.AUTO_DERIVED
79
+ ):
80
+ continue
81
+ elif not concept.lineage and concept.purpose == Purpose.CONSTANT:
82
+ constants.append(concept)
83
+ elif not concept.lineage and concept.purpose == Purpose.KEY:
84
+ keys.append(concept)
85
+
86
+ elif not concept.lineage and concept.purpose == Purpose.PROPERTY:
87
+ if concept.keys:
88
+ # avoid duplicate declarations
89
+ # but we need better composite key support
90
+ for key in concept.keys[:1]:
91
+ properties[key.name].append(concept)
92
+ else:
93
+ keys.append(concept)
94
+ else:
95
+ metrics.append(concept)
96
+
97
+ output_concepts = constants
98
+ for key in keys:
99
+ output_concepts += [key]
100
+ output_concepts += properties.get(key.name, [])
101
+ output_concepts += metrics
102
+
103
+ rendered_concepts = [
104
+ self.to_string(ConceptDeclarationStatement(concept=concept))
105
+ for concept in output_concepts
106
+ ]
107
+
108
+ rendered_datasources = [
109
+ # extra padding between datasources
110
+ # todo: make this more generic
111
+ self.to_string(datasource) + "\n"
112
+ for datasource in arg.datasources.values()
113
+ if datasource.namespace == DEFAULT_NAMESPACE
114
+ ]
115
+ rendered_imports = [
116
+ self.to_string(import_statement)
117
+ for import_statement in arg.imports.values()
118
+ ]
119
+ components = []
120
+ if rendered_imports:
121
+ components.append(rendered_imports)
122
+ if rendered_concepts:
123
+ components.append(rendered_concepts)
124
+ if rendered_datasources:
125
+ components.append(rendered_datasources)
126
+ final = "\n\n".join("\n".join(x) for x in components)
127
+ return final
128
+
129
+ @to_string.register
130
+ def _(self, arg: Datasource):
131
+ assignments = ",\n\t".join([self.to_string(x) for x in arg.columns])
132
+ return f"""datasource {arg.name} (
133
+ {assignments}
134
+ )
135
+ {self.to_string(arg.grain)}
136
+ {self.to_string(arg.address)};"""
137
+
138
+ @to_string.register
139
+ def _(self, arg: "Grain"):
140
+ components = ",".join(self.to_string(x) for x in arg.components)
141
+ return f"grain ({components})"
142
+
143
+ @to_string.register
144
+ def _(self, arg: MergeStatement):
145
+ components = ", ".join(self.to_string(x) for x in arg.concepts)
146
+ return f"merge {components};"
147
+
148
+ @to_string.register
149
+ def _(self, arg: "Query"):
150
+ return f"""query {arg.text}"""
151
+
152
+ @to_string.register
153
+ def _(self, arg: RowsetDerivationStatement):
154
+ return f"""rowset {arg.name} <- {self.to_string(arg.select)}"""
155
+
156
+ @to_string.register
157
+ def _(self, arg: "CaseWhen"):
158
+ return (
159
+ f"""WHEN {self.to_string(arg.comparison)} THEN {self.to_string(arg.expr)}"""
160
+ )
161
+
162
+ @to_string.register
163
+ def _(self, arg: "CaseElse"):
164
+ return f"""ELSE {self.to_string(arg.expr)}"""
165
+
166
+ @to_string.register
167
+ def _(self, arg: "Parenthetical"):
168
+ return f"""({self.to_string(arg.content)})"""
169
+
170
+ @to_string.register
171
+ def _(self, arg: DataType):
172
+ return arg.value
173
+
174
+ @to_string.register
175
+ def _(self, arg: ListWrapper):
176
+ return "[" + ", ".join([self.to_string(x) for x in arg]) + "]"
177
+
178
+ @to_string.register
179
+ def _(self, arg: DatePart):
180
+ return arg.value
181
+
182
+ @to_string.register
183
+ def _(self, arg: "Address"):
184
+ return f"address {arg.location}"
185
+
186
+ @to_string.register
187
+ def _(self, arg: "MagicConstants"):
188
+ if arg == MagicConstants.NULL:
189
+ return "null"
190
+ return arg.value
191
+
192
+ @to_string.register
193
+ def _(self, arg: "ColumnAssignment"):
194
+ return f"{arg.alias}:{self.to_string(arg.concept)}"
195
+
196
+ @to_string.register
197
+ def _(self, arg: "ConceptDeclarationStatement"):
198
+ concept = arg.concept
199
+ if concept.metadata and concept.metadata.description:
200
+ base_description = concept.metadata.description
201
+ else:
202
+ base_description = None
203
+ if concept.namespace:
204
+ namespace = f"{concept.namespace}."
205
+ else:
206
+ namespace = ""
207
+ if not concept.lineage:
208
+ if concept.purpose == Purpose.PROPERTY and concept.keys:
209
+ output = f"{concept.purpose.value} {namespace}{concept.keys[0].name}.{concept.name} {concept.datatype.value};"
210
+ else:
211
+ output = f"{concept.purpose.value} {namespace}{concept.name} {concept.datatype.value};"
212
+ else:
213
+ output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
214
+ if base_description:
215
+ output += f" # {base_description}"
216
+ return output
217
+
218
+ @to_string.register
219
+ def _(self, arg: ConceptDerivation):
220
+ # this is identical rendering;
221
+ return self.to_string(ConceptDeclarationStatement(concept=arg.concept))
222
+
223
+ @to_string.register
224
+ def _(self, arg: PersistStatement):
225
+ return f"PERSIST {arg.identifier} INTO {arg.address.location} FROM {self.to_string(arg.select)}"
226
+
227
+ @to_string.register
228
+ def _(self, arg: SelectItem):
229
+ prefixes = []
230
+ if Modifier.HIDDEN in arg.modifiers:
231
+ prefixes.append("--")
232
+ if Modifier.PARTIAL in arg.modifiers:
233
+ prefixes.append("~")
234
+ final = "".join(prefixes)
235
+ return f"{final}{self.to_string(arg.content)}"
236
+
237
+ @to_string.register
238
+ def _(self, arg: SelectStatement):
239
+ return QUERY_TEMPLATE.render(
240
+ select_columns=[self.to_string(c) for c in arg.selection],
241
+ where=self.to_string(arg.where_clause) if arg.where_clause else None,
242
+ order_by=(
243
+ [self.to_string(c) for c in arg.order_by.items]
244
+ if arg.order_by
245
+ else None
246
+ ),
247
+ limit=arg.limit,
248
+ )
249
+
250
+ @to_string.register
251
+ def _(self, arg: MultiSelectStatement):
252
+ base = "\nMERGE\n".join([self.to_string(select)[:-1] for select in arg.selects])
253
+ base += self.to_string(arg.align)
254
+ if arg.where_clause:
255
+ base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
256
+ if arg.order_by:
257
+ base += f"\nORDER BY\n\t{self.to_string(arg.order_by)}"
258
+ if arg.limit:
259
+ base += f"\nLIMIT {arg.limit}"
260
+ base += "\n;"
261
+ return base
262
+
263
+ @to_string.register
264
+ def _(self, arg: AlignClause):
265
+ return "\nALIGN\n\t" + ",\n\t".join([self.to_string(c) for c in arg.items])
266
+
267
+ @to_string.register
268
+ def _(self, arg: AlignItem):
269
+ return f"{arg.alias}:{','.join([self.to_string(c) for c in arg.concepts])}"
270
+
271
+ @to_string.register
272
+ def _(self, arg: OrderBy):
273
+ return ",\t".join([self.to_string(c) for c in arg.items])
274
+
275
+ @to_string.register
276
+ def _(self, arg: "WhereClause"):
277
+ return f"{self.to_string(arg.conditional)}"
278
+
279
+ @to_string.register
280
+ def _(self, arg: "Conditional"):
281
+ return f"({self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)})"
282
+
283
+ @to_string.register
284
+ def _(self, arg: "Comparison"):
285
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
286
+
287
+ @to_string.register
288
+ def _(self, arg: "WindowItem"):
289
+ over = ",".join(self.to_string(c) for c in arg.over)
290
+ order = ",".join(self.to_string(c) for c in arg.order_by)
291
+ if over:
292
+ return (
293
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
294
+ )
295
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
296
+
297
+ @to_string.register
298
+ def _(self, arg: "FilterItem"):
299
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
300
+
301
+ @to_string.register
302
+ def _(self, arg: "WindowItem"):
303
+ over = ",".join(self.to_string(c) for c in arg.over)
304
+ order = ",".join(self.to_string(c) for c in arg.order_by)
305
+ if over:
306
+ return (
307
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
308
+ )
309
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
310
+
311
+ @to_string.register
312
+ def _(self, arg: "FilterItem"):
313
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
314
+
315
+ @to_string.register
316
+ def _(self, arg: "ImportStatement"):
317
+ return f"import {arg.path} as {arg.alias};"
318
+
319
+ @to_string.register
320
+ def _(self, arg: "WindowItem"):
321
+ over = ",".join(self.to_string(c) for c in arg.over)
322
+ order = ",".join(self.to_string(c) for c in arg.order_by)
323
+ if over:
324
+ return (
325
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
326
+ )
327
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
328
+
329
+ @to_string.register
330
+ def _(self, arg: "FilterItem"):
331
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
332
+
333
+ @to_string.register
334
+ def _(self, arg: "ImportStatement"):
335
+ return f"import {arg.path} as {arg.alias};"
336
+
337
+ @to_string.register
338
+ def _(self, arg: "WindowItem"):
339
+ over = ",".join(self.to_string(c) for c in arg.over)
340
+ order = ",".join(self.to_string(c) for c in arg.order_by)
341
+ if over:
342
+ return (
343
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
344
+ )
345
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
346
+
347
+ @to_string.register
348
+ def _(self, arg: "FilterItem"):
349
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
350
+
351
+ @to_string.register
352
+ def _(self, arg: "ImportStatement"):
353
+ return f"import {arg.path} as {arg.alias};"
354
+
355
+ @to_string.register
356
+ def _(self, arg: "WindowItem"):
357
+ over = ",".join(self.to_string(c) for c in arg.over)
358
+ order = ",".join(self.to_string(c) for c in arg.order_by)
359
+ if over:
360
+ return (
361
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
362
+ )
363
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
364
+
365
+ @to_string.register
366
+ def _(self, arg: "FilterItem"):
367
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
368
+
369
+ @to_string.register
370
+ def _(self, arg: "ImportStatement"):
371
+ return f"import {arg.path} as {arg.alias};"
372
+
373
+ @to_string.register
374
+ def _(self, arg: "WindowItem"):
375
+ over = ",".join(self.to_string(c) for c in arg.over)
376
+ order = ",".join(self.to_string(c) for c in arg.order_by)
377
+ if over:
378
+ return (
379
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
380
+ )
381
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
382
+
383
+ @to_string.register
384
+ def _(self, arg: "FilterItem"):
385
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
386
+
387
+ @to_string.register
388
+ def _(self, arg: "ImportStatement"):
389
+ return f"import {arg.path} as {arg.alias};"
390
+
391
+ @to_string.register
392
+ def _(self, arg: "WindowItem"):
393
+ over = ",".join(self.to_string(c) for c in arg.over)
394
+ order = ",".join(self.to_string(c) for c in arg.order_by)
395
+ if over:
396
+ return (
397
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
398
+ )
399
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
400
+
401
+ @to_string.register
402
+ def _(self, arg: "FilterItem"):
403
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
404
+
405
+ @to_string.register
406
+ def _(self, arg: "ImportStatement"):
407
+ return f"import {arg.path} as {arg.alias};"
408
+
409
+ @to_string.register
410
+ def _(self, arg: "WindowItem"):
411
+ over = ",".join(self.to_string(c) for c in arg.over)
412
+ order = ",".join(self.to_string(c) for c in arg.order_by)
413
+ if over:
414
+ return (
415
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} OVER {over}"
416
+ )
417
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
418
+
419
+ @to_string.register
420
+ def _(self, arg: "FilterItem"):
421
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
422
+
423
+ @to_string.register
424
+ def _(self, arg: "ImportStatement"):
425
+ return f"import {arg.path} as {arg.alias};"
426
+
427
+ @to_string.register
428
+ def _(self, arg: "Concept"):
429
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX):
430
+ return self.to_string(arg.lineage)
431
+ if arg.namespace == DEFAULT_NAMESPACE:
432
+ return arg.name
433
+ return arg.address
434
+
435
+ @to_string.register
436
+ def _(self, arg: "ConceptTransform"):
437
+ return f"{self.to_string(arg.function)}->{arg.output.name}"
438
+
439
+ @to_string.register
440
+ def _(self, arg: "Function"):
441
+ inputs = ",".join(self.to_string(c) for c in arg.arguments)
442
+ if arg.operator == FunctionType.CONSTANT:
443
+ return f"{inputs}"
444
+ return f"{arg.operator.value}({inputs})"
445
+
446
+ @to_string.register
447
+ def _(self, arg: "OrderItem"):
448
+ return f"{self.to_string(arg.expr)} {arg.order.value}"
449
+
450
+ @to_string.register
451
+ def _(self, arg: AggregateWrapper):
452
+ if arg.by:
453
+ return f"{self.to_string(arg.function)} by {self.to_string(arg.by)}"
454
+ return f"{self.to_string(arg.function)}"
455
+
456
+ @to_string.register
457
+ def _(self, arg: int):
458
+ return f"{arg}"
459
+
460
+ @to_string.register
461
+ def _(self, arg: str):
462
+ return f"'{arg}'"
463
+
464
+ @to_string.register
465
+ def _(self, arg: float):
466
+ return f"{arg}"
467
+
468
+ @to_string.register
469
+ def _(self, arg: bool):
470
+ return f"{arg}"
471
+
472
+ @to_string.register
473
+ def _(self, arg: list):
474
+ base = ", ".join([self.to_string(x) for x in arg])
475
+ return f"[{base}]"
476
+
477
+
478
+ def render_query(query: "SelectStatement") -> str:
479
+ return Renderer().to_string(query)
480
+
481
+
482
+ def render_environment(environment: "Environment") -> str:
483
+ return Renderer().to_string(environment)
trilogy/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,127 @@
1
+ from click import Path, argument, option, group, pass_context, UNPROCESSED
2
+ from trilogy import Executor, Environment, parse
3
+ from trilogy.dialect.enums import Dialects
4
+ from datetime import datetime
5
+ from pathlib import Path as PathlibPath
6
+ from trilogy.hooks.query_debugger import DebuggingHook
7
+ from trilogy.parsing.render import Renderer
8
+ from trilogy.constants import DEFAULT_NAMESPACE
9
+
10
+
11
+ def print_tabulate(q, tabulate):
12
+ result = q.fetchall()
13
+ print(tabulate(result, headers=q.keys(), tablefmt="psql"))
14
+
15
+
16
+ def pairwise(t):
17
+ it = iter(t)
18
+ return zip(it, it)
19
+
20
+
21
+ def extra_to_kwargs(arg_list: list[str]) -> dict[str, str | int]:
22
+ pairs = pairwise(arg_list)
23
+ final = {}
24
+ for k, v in pairs:
25
+ k = k.lstrip("--")
26
+ final[k] = v
27
+ return final
28
+
29
+
30
+ @group()
31
+ @option("--debug", default=False)
32
+ @pass_context
33
+ def cli(ctx, debug: bool):
34
+ ctx.ensure_object(dict)
35
+ ctx.obj["DEBUG"] = debug
36
+
37
+
38
+ @cli.command("fmt")
39
+ @argument("input", type=Path(exists=True))
40
+ @pass_context
41
+ def fmt(ctx, input):
42
+ start = datetime.now()
43
+ with open(input, "r") as f:
44
+ script = f.read()
45
+
46
+ _, queries = parse(script)
47
+ r = Renderer()
48
+ with open(input, "w") as f:
49
+ f.write("\n".join([r.to_string(x) for x in queries]))
50
+ print(f"Completed all in {(datetime.now()-start)}")
51
+
52
+
53
+ @cli.command(
54
+ "run",
55
+ context_settings=dict(
56
+ ignore_unknown_options=True,
57
+ ),
58
+ )
59
+ @argument("input", type=Path())
60
+ @argument("dialect", type=str)
61
+ @argument("conn_args", nargs=-1, type=UNPROCESSED)
62
+ @pass_context
63
+ def run(ctx, input, dialect: str, conn_args):
64
+ if PathlibPath(input).exists():
65
+ inputp = PathlibPath(input)
66
+ with open(input, "r") as f:
67
+ script = f.read()
68
+ namespace = DEFAULT_NAMESPACE
69
+ directory = inputp.parent
70
+ else:
71
+ script = input
72
+ namespace = DEFAULT_NAMESPACE
73
+ directory = PathlibPath.cwd()
74
+ edialect = Dialects(dialect)
75
+
76
+ debug = ctx.obj["DEBUG"]
77
+ conn_dict = extra_to_kwargs((conn_args))
78
+ if edialect == Dialects.DUCK_DB:
79
+ from trilogy.dialect.config import DuckDBConfig
80
+
81
+ conf = DuckDBConfig(**conn_dict) # type: ignore
82
+ elif edialect == Dialects.SNOWFLAKE:
83
+ from trilogy.dialect.config import SnowflakeConfig
84
+
85
+ conf = SnowflakeConfig(**conn_dict) # type: ignore
86
+ elif edialect == Dialects.SQL_SERVER:
87
+ from trilogy.dialect.config import SQLServerConfig
88
+
89
+ conf = SQLServerConfig(**conn_dict) # type: ignore
90
+ elif edialect == Dialects.POSTGRES:
91
+ from trilogy.dialect.config import PostgresConfig
92
+
93
+ conf = PostgresConfig(**conn_dict) # type: ignore
94
+ else:
95
+ conf = None
96
+ exec = Executor(
97
+ dialect=edialect,
98
+ engine=edialect.default_engine(conf=conf),
99
+ environment=Environment(working_path=str(directory), namespace=namespace),
100
+ hooks=[DebuggingHook()] if debug else [],
101
+ )
102
+
103
+ queries = exec.parse_text(script)
104
+ start = datetime.now()
105
+ print(f"Executing {len(queries)} statements...")
106
+ for idx, query in enumerate(queries):
107
+ lstart = datetime.now()
108
+ results = exec.execute_statement(query)
109
+ end = datetime.now()
110
+ print(f"Statement {idx+1} of {len(queries)} done, duration: {end-lstart}.")
111
+ if not results:
112
+ continue
113
+ try:
114
+ import tabulate
115
+
116
+ print_tabulate(results, tabulate.tabulate)
117
+ except ImportError:
118
+ print('Install tabulate (pip install tabulate) for a prettier output')
119
+ print(", ".join(results.keys()))
120
+ for row in results:
121
+ print(row)
122
+ print("---")
123
+ print(f"Completed all in {(datetime.now()-start)}")
124
+
125
+
126
+ if __name__ == "__main__":
127
+ cli()
trilogy/utility.py ADDED
@@ -0,0 +1,31 @@
1
+ import hashlib
2
+ from typing import List, Any, Union, Callable
3
+
4
+ from trilogy.constants import DEFAULT_NAMESPACE
5
+
6
+ INT_HASH_SIZE = 16
7
+
8
+
9
+ def string_to_hash(input: str) -> int:
10
+ return (
11
+ int(hashlib.sha256(input.encode("utf-8")).hexdigest(), 16) % 10**INT_HASH_SIZE
12
+ )
13
+
14
+
15
+ def unique(inputs: List, property: Union[str, Callable]) -> List[Any]:
16
+ final = []
17
+ dedupe = set()
18
+ if isinstance(property, str):
19
+
20
+ def getter(x):
21
+ return getattr(x, property, DEFAULT_NAMESPACE)
22
+
23
+ else:
24
+ getter = property
25
+ for input in inputs:
26
+ key = getter(input)
27
+ if key in dedupe:
28
+ continue
29
+ dedupe.add(key)
30
+ final.append(input)
31
+ return final