pytrilogy 0.0.3.95__py3-none-any.whl → 0.0.3.97__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.

@@ -0,0 +1,233 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, List, Optional
3
+
4
+ from trilogy.core.models.author import ConceptRef
5
+ from trilogy.core.models.datasource import Datasource
6
+ from trilogy.core.models.environment import Environment
7
+ from trilogy.core.statements.author import (
8
+ ConceptDeclarationStatement,
9
+ ImportStatement,
10
+ MergeStatementV2,
11
+ )
12
+ from trilogy.core.statements.execute import (
13
+ ProcessedShowStatement,
14
+ ProcessedStaticValueOutput,
15
+ ProcessedValidateStatement,
16
+ )
17
+ from trilogy.core.validation.common import ValidationTest
18
+ from trilogy.dialect.base import BaseDialect
19
+ from trilogy.engine import ResultProtocol
20
+
21
+
22
+ @dataclass
23
+ class MockResult(ResultProtocol):
24
+ values: list["MockResultRow"]
25
+ columns: list[str]
26
+
27
+ def __init__(self, values: list[Any], columns: list[str]):
28
+ processed: list[MockResultRow] = []
29
+ for x in values:
30
+ if isinstance(x, dict):
31
+ processed.append(MockResultRow(x))
32
+ elif isinstance(x, MockResultRow):
33
+ processed.append(x)
34
+ else:
35
+ raise ValueError(
36
+ f"Cannot process value of type {type(x)} in MockResult"
37
+ )
38
+ self.columns = columns
39
+ self.values = processed
40
+
41
+ def __iter__(self):
42
+ while self.values:
43
+ yield self.values.pop(0)
44
+
45
+ def fetchall(self):
46
+ return self.values
47
+
48
+ def fetchone(self):
49
+ if self.values:
50
+ return self.values.pop(0)
51
+ return None
52
+
53
+ def fetchmany(self, size: int):
54
+ rval = self.values[:size]
55
+ self.values = self.values[size:]
56
+ return rval
57
+
58
+ def keys(self):
59
+ return self.columns
60
+
61
+ def as_dict(self):
62
+ return [x.as_dict() if isinstance(x, MockResultRow) else x for x in self.values]
63
+
64
+
65
+ @dataclass
66
+ class MockResultRow:
67
+ _values: dict[str, Any]
68
+
69
+ def as_dict(self):
70
+ return self._values
71
+
72
+ def __str__(self) -> str:
73
+ return str(self._values)
74
+
75
+ def __repr__(self) -> str:
76
+ return repr(self._values)
77
+
78
+ def __getattr__(self, name: str) -> Any:
79
+ if name in self._values:
80
+ return self._values[name]
81
+ return super().__getattribute__(name)
82
+
83
+ def __getitem__(self, key: str) -> Any:
84
+ return self._values[key]
85
+
86
+ def values(self):
87
+ return self._values.values()
88
+
89
+ def keys(self):
90
+ return self._values.keys()
91
+
92
+
93
+ def generate_result_set(
94
+ columns: List[ConceptRef], output_data: list[Any]
95
+ ) -> MockResult:
96
+ """Generate a mock result set from columns and output data."""
97
+ names = [x.address.replace(".", "_") for x in columns]
98
+ return MockResult(
99
+ values=[dict(zip(names, [row])) for row in output_data], columns=names
100
+ )
101
+
102
+
103
+ def handle_concept_declaration(query: ConceptDeclarationStatement) -> MockResult:
104
+ """Handle concept declaration statements without execution."""
105
+ concept = query.concept
106
+ return MockResult(
107
+ [
108
+ {
109
+ "address": concept.address,
110
+ "type": concept.datatype.value,
111
+ "purpose": concept.purpose.value,
112
+ "derivation": concept.derivation.value,
113
+ }
114
+ ],
115
+ ["address", "type", "purpose", "derivation"],
116
+ )
117
+
118
+
119
+ def handle_datasource(query: Datasource) -> MockResult:
120
+ """Handle datasource queries without execution."""
121
+ return MockResult(
122
+ [
123
+ {
124
+ "name": query.name,
125
+ }
126
+ ],
127
+ ["name"],
128
+ )
129
+
130
+
131
+ def handle_import_statement(query: ImportStatement) -> MockResult:
132
+ """Handle import statements without execution."""
133
+ return MockResult(
134
+ [
135
+ {
136
+ "path": query.path,
137
+ "alias": query.alias,
138
+ }
139
+ ],
140
+ ["path", "alias"],
141
+ )
142
+
143
+
144
+ def handle_merge_statement(
145
+ query: MergeStatementV2, environment: Environment
146
+ ) -> MockResult:
147
+ """Handle merge statements by updating environment and returning result."""
148
+ for concept in query.sources:
149
+ environment.merge_concept(
150
+ concept, query.targets[concept.address], modifiers=query.modifiers
151
+ )
152
+
153
+ return MockResult(
154
+ [
155
+ {
156
+ "sources": ",".join([x.address for x in query.sources]),
157
+ "targets": ",".join([x.address for _, x in query.targets.items()]),
158
+ }
159
+ ],
160
+ ["source", "target"],
161
+ )
162
+
163
+
164
+ def handle_processed_show_statement(
165
+ query: ProcessedShowStatement, compiled_statements: list[str]
166
+ ) -> MockResult:
167
+ """Handle processed show statements without execution."""
168
+
169
+ return generate_result_set(query.output_columns, compiled_statements)
170
+
171
+
172
+ def raw_validation_to_result(
173
+ raw: list[ValidationTest], generator: Optional[BaseDialect] = None
174
+ ) -> Optional[MockResult]:
175
+ """Convert raw validation tests to mock result."""
176
+ if not raw:
177
+ return None
178
+ output = []
179
+ for row in raw:
180
+ if row.raw_query and generator and not row.generated_query:
181
+ try:
182
+ row.generated_query = generator.compile_statement(row.raw_query)
183
+ except Exception as e:
184
+ row.generated_query = f"Error generating query: {e}"
185
+ output.append(
186
+ {
187
+ "check_type": row.check_type.value,
188
+ "expected": row.expected,
189
+ "result": str(row.result) if row.result else None,
190
+ "ran": row.ran,
191
+ "query": row.generated_query if row.generated_query else "",
192
+ }
193
+ )
194
+ return MockResult(output, ["check_type", "expected", "result", "ran", "query"])
195
+
196
+
197
+ def handle_processed_validate_statement(
198
+ query: ProcessedValidateStatement, dialect: BaseDialect, validate_environment_func
199
+ ) -> Optional[MockResult]:
200
+ """Handle processed validate statements."""
201
+ results = validate_environment_func(query.scope, query.targets)
202
+ return raw_validation_to_result(results, dialect)
203
+
204
+
205
+ def handle_show_statement_outputs(
206
+ statement: ProcessedShowStatement,
207
+ compiled_statements: list[str],
208
+ environment: Environment,
209
+ dialect: BaseDialect,
210
+ ) -> list[MockResult]:
211
+ """Handle show statement outputs without execution."""
212
+ output = []
213
+ for x in statement.output_values:
214
+ if isinstance(x, ProcessedStaticValueOutput):
215
+ output.append(generate_result_set(statement.output_columns, x.values))
216
+ elif compiled_statements:
217
+
218
+ output.append(
219
+ generate_result_set(
220
+ statement.output_columns,
221
+ compiled_statements,
222
+ )
223
+ )
224
+ elif isinstance(x, ProcessedValidateStatement):
225
+ from trilogy.core.validation.environment import validate_environment
226
+
227
+ raw = validate_environment(environment, x.scope, x.targets)
228
+ results = raw_validation_to_result(raw, dialect)
229
+ if results:
230
+ output.append(results)
231
+ else:
232
+ raise NotImplementedError(f"Cannot show type {type(x)} in show statement")
233
+ return output
trilogy/executor.py CHANGED
@@ -1,4 +1,3 @@
1
- from dataclasses import dataclass
2
1
  from functools import singledispatchmethod
3
2
  from pathlib import Path
4
3
  from typing import Any, Generator, List, Optional
@@ -7,7 +6,7 @@ from sqlalchemy import text
7
6
 
8
7
  from trilogy.constants import MagicConstants, Rendering, logger
9
8
  from trilogy.core.enums import FunctionType, Granularity, IOType, ValidationScope
10
- from trilogy.core.models.author import Concept, ConceptRef, Function
9
+ from trilogy.core.models.author import Concept, Function
11
10
  from trilogy.core.models.build import BuildFunction
12
11
  from trilogy.core.models.core import ListWrapper, MapWrapper
13
12
  from trilogy.core.models.datasource import Datasource
@@ -31,7 +30,6 @@ from trilogy.core.statements.execute import (
31
30
  ProcessedQueryPersist,
32
31
  ProcessedRawSQLStatement,
33
32
  ProcessedShowStatement,
34
- ProcessedStaticValueOutput,
35
33
  ProcessedValidateStatement,
36
34
  )
37
35
  from trilogy.core.validation.common import (
@@ -39,82 +37,22 @@ from trilogy.core.validation.common import (
39
37
  )
40
38
  from trilogy.dialect.base import BaseDialect
41
39
  from trilogy.dialect.enums import Dialects
40
+ from trilogy.dialect.metadata import (
41
+ generate_result_set,
42
+ handle_concept_declaration,
43
+ handle_datasource,
44
+ handle_import_statement,
45
+ handle_merge_statement,
46
+ handle_processed_show_statement,
47
+ handle_processed_validate_statement,
48
+ handle_show_statement_outputs,
49
+ )
42
50
  from trilogy.engine import ExecutionEngine, ResultProtocol
43
51
  from trilogy.hooks.base_hook import BaseHook
44
52
  from trilogy.parser import parse_text
45
53
  from trilogy.render import get_dialect_generator
46
54
 
47
55
 
48
- @dataclass
49
- class MockResult(ResultProtocol):
50
- values: list[Any]
51
- columns: list[str]
52
-
53
- def __init__(self, values: list[Any], columns: list[str]):
54
- processed = []
55
- for x in values:
56
- if isinstance(x, dict):
57
- processed.append(MockResultRow(x))
58
- else:
59
- processed.append(x)
60
- self.columns = columns
61
- self.values = processed
62
-
63
- def __iter__(self):
64
- while self.values:
65
- yield self.values.pop(0)
66
-
67
- def fetchall(self):
68
- return self.values
69
-
70
- def fetchone(self):
71
- if self.values:
72
- return self.values.pop(0)
73
- return None
74
-
75
- def fetchmany(self, size: int):
76
- rval = self.values[:size]
77
- self.values = self.values[size:]
78
- return rval
79
-
80
- def keys(self):
81
- return self.columns
82
-
83
-
84
- @dataclass
85
- class MockResultRow:
86
- _values: dict[str, Any]
87
-
88
- def __str__(self) -> str:
89
- return str(self._values)
90
-
91
- def __repr__(self) -> str:
92
- return repr(self._values)
93
-
94
- def __getattr__(self, name: str) -> Any:
95
- if name in self._values:
96
- return self._values[name]
97
- return super().__getattribute__(name)
98
-
99
- def __getitem__(self, key: str) -> Any:
100
- return self._values[key]
101
-
102
- def values(self):
103
- return self._values.values()
104
-
105
- def keys(self):
106
- return self._values.keys()
107
-
108
-
109
- def generate_result_set(
110
- columns: List[ConceptRef], output_data: list[Any]
111
- ) -> MockResult:
112
- names = [x.address.replace(".", "_") for x in columns]
113
- return MockResult(
114
- values=[dict(zip(names, [row])) for row in output_data], columns=names
115
- )
116
-
117
-
118
56
  class Executor(object):
119
57
  def __init__(
120
58
  self,
@@ -150,29 +88,11 @@ class Executor(object):
150
88
 
151
89
  @execute_query.register
152
90
  def _(self, query: ConceptDeclarationStatement) -> ResultProtocol | None:
153
- concept = query.concept
154
- return MockResult(
155
- [
156
- {
157
- "address": concept.address,
158
- "type": concept.datatype.value,
159
- "purpose": concept.purpose.value,
160
- "derivation": concept.derivation.value,
161
- }
162
- ],
163
- ["address", "type", "purpose", "derivation"],
164
- )
91
+ return handle_concept_declaration(query)
165
92
 
166
93
  @execute_query.register
167
94
  def _(self, query: Datasource) -> ResultProtocol | None:
168
- return MockResult(
169
- [
170
- {
171
- "name": query.name,
172
- }
173
- ],
174
- ["name"],
175
- )
95
+ return handle_datasource(query)
176
96
 
177
97
  @execute_query.register
178
98
  def _(self, query: str) -> ResultProtocol | None:
@@ -208,66 +128,28 @@ class Executor(object):
208
128
 
209
129
  @execute_query.register
210
130
  def _(self, query: ProcessedShowStatement) -> ResultProtocol | None:
211
- return generate_result_set(
212
- query.output_columns,
131
+ return handle_processed_show_statement(
132
+ query,
213
133
  [
214
134
  self.generator.compile_statement(x)
215
135
  for x in query.output_values
216
- if isinstance(x, ProcessedQuery)
136
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
217
137
  ],
218
138
  )
219
139
 
220
- def _raw_validation_to_result(
221
- self, raw: list[ValidationTest]
222
- ) -> Optional[ResultProtocol]:
223
- if not raw:
224
- return None
225
- output = []
226
- for row in raw:
227
- output.append(
228
- {
229
- "check_type": row.check_type.value,
230
- "expected": row.expected,
231
- "result": str(row.result) if row.result else None,
232
- "ran": row.ran,
233
- "query": row.query if row.query else "",
234
- }
235
- )
236
- return MockResult(output, ["check_type", "expected", "result", "ran", "query"])
237
-
238
140
  @execute_query.register
239
141
  def _(self, query: ProcessedValidateStatement) -> ResultProtocol | None:
240
- results = self.validate_environment(query.scope, query.targets)
241
- return self._raw_validation_to_result(results)
142
+ return handle_processed_validate_statement(
143
+ query, self.generator, self.validate_environment
144
+ )
242
145
 
243
146
  @execute_query.register
244
147
  def _(self, query: ImportStatement) -> ResultProtocol | None:
245
- return MockResult(
246
- [
247
- {
248
- "path": query.path,
249
- "alias": query.alias,
250
- }
251
- ],
252
- ["path", "alias"],
253
- )
148
+ return handle_import_statement(query)
254
149
 
255
150
  @execute_query.register
256
151
  def _(self, query: MergeStatementV2) -> ResultProtocol | None:
257
- for concept in query.sources:
258
- self.environment.merge_concept(
259
- concept, query.targets[concept.address], modifiers=query.modifiers
260
- )
261
-
262
- return MockResult(
263
- [
264
- {
265
- "sources": ",".join([x.address for x in query.sources]),
266
- "targets": ",".join([x.address for _, x in query.targets.items()]),
267
- }
268
- ],
269
- ["source", "target"],
270
- )
152
+ return handle_merge_statement(query, self.environment)
271
153
 
272
154
  @execute_query.register
273
155
  def _(self, query: ProcessedRawSQLStatement) -> ResultProtocol | None:
@@ -516,29 +398,17 @@ class Executor(object):
516
398
  # connection = self.engine.connect()
517
399
  for statement in self.parse_text_generator(command):
518
400
  if isinstance(statement, ProcessedShowStatement):
519
- for x in statement.output_values:
520
- if isinstance(x, ProcessedStaticValueOutput):
521
- output.append(
522
- generate_result_set(statement.output_columns, x.values)
523
- )
524
- elif isinstance(x, ProcessedQuery):
525
- output.append(
526
- generate_result_set(
527
- statement.output_columns,
528
- [self.generator.compile_statement(x)],
529
- )
530
- )
531
- elif isinstance(x, ProcessedValidateStatement):
532
- raw = self.validate_environment(
533
- x.scope, x.targets, generate_only=True
534
- )
535
- results = self._raw_validation_to_result(raw)
536
- if results:
537
- output.append(results)
538
- else:
539
- raise NotImplementedError(
540
- f"Cannot show type {type(x)} in show statement"
541
- )
401
+ results = handle_show_statement_outputs(
402
+ statement,
403
+ [
404
+ self.generator.compile_statement(x)
405
+ for x in statement.output_values
406
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
407
+ ],
408
+ self.environment,
409
+ self.generator,
410
+ )
411
+ output.extend(results)
542
412
  continue
543
413
  if non_interactive:
544
414
  if not isinstance(
@@ -567,5 +437,5 @@ class Executor(object):
567
437
  from trilogy.core.validation.environment import validate_environment
568
438
 
569
439
  return validate_environment(
570
- self.environment, self, scope, targets, generate_only
440
+ self.environment, scope, targets, exec=None if generate_only else self
571
441
  )
@@ -379,14 +379,16 @@ class ParseToObjects(Transformer):
379
379
  def start(self, args):
380
380
  return args
381
381
 
382
+ def LINE_SEPARATOR(self, args):
383
+ return MagicConstants.LINE_SEPARATOR
384
+
382
385
  def block(self, args):
383
386
  output = args[0]
384
387
  if isinstance(output, ConceptDeclarationStatement):
385
- if len(args) > 1 and isinstance(args[1], Comment):
386
- output.concept.metadata.description = (
387
- output.concept.metadata.description
388
- or args[1].text.split("#")[1].strip()
389
- )
388
+ if len(args) > 1 and args[1] != MagicConstants.LINE_SEPARATOR:
389
+ comments = [x for x in args[1:] if isinstance(x, Comment)]
390
+ merged = "\n".join([x.text.split("#")[1].rstrip() for x in comments])
391
+ output.concept.metadata.description = merged
390
392
  # this is a bad plan for now;
391
393
  # because a comment after an import statement is very common
392
394
  # and it's not intuitive that it modifies the import description
@@ -913,7 +915,7 @@ class ParseToObjects(Transformer):
913
915
  return Comment(text=args[0].value)
914
916
 
915
917
  def PARSE_COMMENT(self, args):
916
- return Comment(text=args.value)
918
+ return Comment(text=args.value.rstrip())
917
919
 
918
920
  @v_args(meta=True)
919
921
  def select_transform(self, meta: Meta, args) -> ConceptTransform:
trilogy/parsing/render.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from collections import defaultdict
2
2
  from datetime import date, datetime
3
3
  from functools import singledispatchmethod
4
+ from typing import Any
4
5
 
5
6
  from jinja2 import Template
6
7
 
@@ -12,6 +13,7 @@ from trilogy.core.models.author import (
12
13
  AlignItem,
13
14
  CaseElse,
14
15
  CaseWhen,
16
+ Comment,
15
17
  Comparison,
16
18
  Concept,
17
19
  ConceptRef,
@@ -83,6 +85,23 @@ class Renderer:
83
85
  def __init__(self, environment: Environment | None = None):
84
86
  self.environment = environment
85
87
 
88
+ def render_statement_string(self, list_of_statements: list[Any]) -> str:
89
+ new = []
90
+ last_statement_type = None
91
+ for stmt in list_of_statements:
92
+ stmt_type = type(stmt)
93
+ if last_statement_type is None:
94
+ pass
95
+ elif last_statement_type == Comment:
96
+ new.append("\n")
97
+ elif stmt_type != last_statement_type:
98
+ new.append("\n\n")
99
+ else:
100
+ new.append("\n")
101
+ new.append(Renderer().to_string(stmt))
102
+ last_statement_type = stmt_type
103
+ return "".join(new)
104
+
86
105
  @singledispatchmethod
87
106
  def to_string(self, arg):
88
107
  raise NotImplementedError("Cannot render type {}".format(type(arg)))
@@ -269,6 +288,8 @@ class Renderer:
269
288
  @to_string.register
270
289
  def _(self, arg: "Address"):
271
290
  if arg.is_query:
291
+ if arg.location.startswith("("):
292
+ return f"query '''{arg.location[1:-1]}'''"
272
293
  return f"query '''{arg.location}'''"
273
294
  return f"address {arg.location}"
274
295
 
@@ -286,7 +307,7 @@ class Renderer:
286
307
  def _(self, arg: "ColumnAssignment"):
287
308
  if arg.modifiers:
288
309
  modifiers = "".join(
289
- [self.to_string(modifier) for modifier in arg.modifiers]
310
+ [self.to_string(modifier) for modifier in sorted(arg.modifiers)]
290
311
  )
291
312
  else:
292
313
  modifiers = ""
@@ -328,7 +349,7 @@ class Renderer:
328
349
  else:
329
350
  output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
330
351
  if base_description:
331
- output += f" # {base_description}"
352
+ output += f" #{base_description}"
332
353
  return output
333
354
 
334
355
  @to_string.register
@@ -428,6 +449,10 @@ class Renderer:
428
449
  def _(self, arg: "Comparison"):
429
450
  return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
430
451
 
452
+ @to_string.register
453
+ def _(self, arg: "Comment"):
454
+ return f"{arg.text}"
455
+
431
456
  @to_string.register
432
457
  def _(self, arg: "WindowItem"):
433
458
  over = ",".join(self.to_string(c) for c in arg.over)
@@ -551,8 +576,10 @@ class Renderer:
551
576
  def _(self, arg: Modifier):
552
577
  if arg == Modifier.PARTIAL:
553
578
  return "~"
554
- if arg == Modifier.HIDDEN:
579
+ elif arg == Modifier.HIDDEN:
555
580
  return "--"
581
+ elif arg == Modifier.NULLABLE:
582
+ return "?"
556
583
  return arg.value
557
584
 
558
585
  @to_string.register
@@ -1,5 +1,5 @@
1
- !start: ( block | show_statement )*
2
- block: statement _TERMINATOR PARSE_COMMENT?
1
+ !start: ( block | show_statement | PARSE_COMMENT )*
2
+ block: statement _TERMINATOR LINE_SEPARATOR? PARSE_COMMENT*
3
3
  ?statement: concept
4
4
  | datasource
5
5
  | function
@@ -14,9 +14,12 @@
14
14
  | rawsql_statement
15
15
  | validate_statement
16
16
 
17
- _TERMINATOR: ";"i /\s*/
17
+ _TERMINATOR: ";"i
18
18
 
19
- PARSE_COMMENT.1: /#.*(\n|$)/ | /\/\/.*\n/
19
+ PARSE_COMMENT.1: /#.*(\n|$)/ | /\/\/.*(\n|$)/
20
+
21
+ // when whitespace matters - comment placement
22
+ LINE_SEPARATOR.1: /[ \t\r\f\v]*\n+/
20
23
 
21
24
  // property display_name string
22
25
  concept_declaration: PURPOSE IDENTIFIER data_type concept_nullable_modifier? metadata?
trilogy/compiler.py DELETED
File without changes