pytrilogy 0.0.2.10__py3-none-any.whl → 0.0.2.12__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 (30) hide show
  1. {pytrilogy-0.0.2.10.dist-info → pytrilogy-0.0.2.12.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.10.dist-info → pytrilogy-0.0.2.12.dist-info}/RECORD +30 -30
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/enums.py +0 -1
  5. trilogy/core/environment_helpers.py +44 -6
  6. trilogy/core/models.py +47 -26
  7. trilogy/core/optimization.py +31 -3
  8. trilogy/core/optimizations/__init__.py +2 -1
  9. trilogy/core/optimizations/predicate_pushdown.py +60 -42
  10. trilogy/core/processing/concept_strategies_v3.py +8 -4
  11. trilogy/core/processing/node_generators/basic_node.py +15 -9
  12. trilogy/core/processing/node_generators/filter_node.py +20 -3
  13. trilogy/core/processing/node_generators/group_node.py +2 -0
  14. trilogy/core/processing/node_generators/node_merge_node.py +28 -2
  15. trilogy/core/processing/node_generators/unnest_node.py +10 -3
  16. trilogy/core/processing/nodes/base_node.py +7 -2
  17. trilogy/core/processing/nodes/group_node.py +0 -1
  18. trilogy/core/processing/nodes/merge_node.py +11 -4
  19. trilogy/core/processing/nodes/unnest_node.py +13 -9
  20. trilogy/core/processing/utility.py +3 -1
  21. trilogy/core/query_processor.py +20 -5
  22. trilogy/dialect/base.py +96 -56
  23. trilogy/dialect/common.py +3 -3
  24. trilogy/parsing/common.py +58 -1
  25. trilogy/parsing/parse_engine.py +111 -136
  26. trilogy/parsing/trilogy.lark +5 -1
  27. {pytrilogy-0.0.2.10.dist-info → pytrilogy-0.0.2.12.dist-info}/LICENSE.md +0 -0
  28. {pytrilogy-0.0.2.10.dist-info → pytrilogy-0.0.2.12.dist-info}/WHEEL +0 -0
  29. {pytrilogy-0.0.2.10.dist-info → pytrilogy-0.0.2.12.dist-info}/entry_points.txt +0 -0
  30. {pytrilogy-0.0.2.10.dist-info → pytrilogy-0.0.2.12.dist-info}/top_level.txt +0 -0
trilogy/dialect/base.py CHANGED
@@ -9,7 +9,6 @@ from trilogy.core.enums import (
9
9
  FunctionType,
10
10
  WindowType,
11
11
  DatePart,
12
- ComparisonOperator,
13
12
  )
14
13
  from trilogy.core.models import (
15
14
  ListType,
@@ -227,13 +226,7 @@ def safe_get_cte_value(coalesce, cte: CTE, c: Concept, quote_char: str):
227
226
  raw = cte.source_map.get(address, None)
228
227
 
229
228
  if not raw:
230
- for k, v in c.pseudonyms.items():
231
- if cte.source_map.get(k):
232
- c = v
233
- raw = cte.source_map[k]
234
- break
235
- if not raw:
236
- return INVALID_REFERENCE_STRING("Missing source reference")
229
+ return None
237
230
  if isinstance(raw, str):
238
231
  rendered = cte.get_alias(c, raw)
239
232
  return f"{raw}.{safe_quote(rendered, quote_char)}"
@@ -260,38 +253,64 @@ class BaseDialect:
260
253
 
261
254
  return f"{self.render_concept_sql(order_item.expr, cte=cte, alias=False)} {order_item.order.value}"
262
255
 
263
- def render_concept_sql(self, c: Concept, cte: CTE, alias: bool = True) -> str:
256
+ def render_concept_sql(
257
+ self, c: Concept, cte: CTE, alias: bool = True, raise_invalid: bool = False
258
+ ) -> str:
259
+ result = None
260
+ if c.pseudonyms:
261
+ for candidate in [c] + list(c.pseudonyms.values()):
262
+ try:
263
+ logger.debug(
264
+ f"{LOGGER_PREFIX} [{c.address}] Attempting rendering w/ candidate {candidate.address}"
265
+ )
266
+ result = self._render_concept_sql(
267
+ candidate, cte, raise_invalid=True
268
+ )
269
+ if result:
270
+ break
271
+ except ValueError:
272
+ continue
273
+ if not result:
274
+ result = self._render_concept_sql(c, cte, raise_invalid=raise_invalid)
275
+ if alias:
276
+ return f"{result} as {self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
277
+ return result
278
+
279
+ def _render_concept_sql(
280
+ self, c: Concept, cte: CTE, raise_invalid: bool = False
281
+ ) -> str:
264
282
  # only recurse while it's in sources of the current cte
265
283
  logger.debug(
266
284
  f"{LOGGER_PREFIX} [{c.address}] Starting rendering loop on cte: {cte.name}"
267
285
  )
268
286
 
287
+ # check if it's not inherited AND no pseudonyms are inherited
269
288
  if c.lineage and cte.source_map.get(c.address, []) == []:
270
289
  logger.debug(
271
- f"{LOGGER_PREFIX} [{c.address}] rendering concept with lineage that is not already existing"
290
+ f"{LOGGER_PREFIX} [{c.address}] rendering concept with lineage that is not already existing, have {cte.source_map}"
272
291
  )
273
292
  if isinstance(c.lineage, WindowItem):
274
293
  rendered_order_components = [
275
- f"{self.render_concept_sql(x.expr, cte, alias=False)} {x.order.value}"
294
+ f"{self.render_concept_sql(x.expr, cte, alias=False, raise_invalid=raise_invalid)} {x.order.value}"
276
295
  for x in c.lineage.order_by
277
296
  ]
278
297
  rendered_over_components = [
279
- self.render_concept_sql(x, cte, alias=False) for x in c.lineage.over
298
+ self.render_concept_sql(
299
+ x, cte, alias=False, raise_invalid=raise_invalid
300
+ )
301
+ for x in c.lineage.over
280
302
  ]
281
- rval = f"{self.WINDOW_FUNCTION_MAP[c.lineage.type](concept = self.render_concept_sql(c.lineage.content, cte=cte, alias=False), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
303
+ rval = f"{self.WINDOW_FUNCTION_MAP[c.lineage.type](concept = self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
282
304
  elif isinstance(c.lineage, FilterItem):
283
305
  # for cases when we've optimized this
284
- if (
285
- len(cte.output_columns) == 1
286
- and cte.condition == c.lineage.where.conditional
287
- ):
306
+ if cte.condition == c.lineage.where.conditional:
288
307
  rval = self.render_expr(c.lineage.content, cte=cte)
289
308
  else:
290
- rval = f"CASE WHEN {self.render_expr(c.lineage.where.conditional, cte=cte)} THEN {self.render_concept_sql(c.lineage.content, cte=cte, alias=False)} ELSE NULL END"
309
+ rval = f"CASE WHEN {self.render_expr(c.lineage.where.conditional, cte=cte)} THEN {self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid)} ELSE NULL END"
291
310
  elif isinstance(c.lineage, RowsetItem):
292
- rval = f"{self.render_concept_sql(c.lineage.content, cte=cte, alias=False)}"
311
+ rval = f"{self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid)}"
293
312
  elif isinstance(c.lineage, MultiSelectStatement):
294
- rval = f"{self.render_concept_sql(c.lineage.find_source(c, cte), cte=cte, alias=False)}"
313
+ rval = f"{self.render_concept_sql(c.lineage.find_source(c, cte), cte=cte, alias=False, raise_invalid=raise_invalid)}"
295
314
  elif isinstance(c.lineage, AggregateWrapper):
296
315
  args = [
297
316
  self.render_expr(v, cte) # , alias=False)
@@ -307,16 +326,15 @@ class BaseDialect:
307
326
  rval = f"{self.FUNCTION_GRAIN_MATCH_MAP[c.lineage.function.operator](args)}"
308
327
  else:
309
328
  args = [
310
- self.render_expr(v, cte) # , alias=False)
329
+ self.render_expr(
330
+ v, cte=cte, raise_invalid=raise_invalid
331
+ ) # , alias=False)
311
332
  for v in c.lineage.arguments
312
333
  ]
313
334
 
314
335
  if cte.group_to_grain:
315
336
  rval = f"{self.FUNCTION_MAP[c.lineage.operator](args)}"
316
337
  else:
317
- logger.debug(
318
- f"{LOGGER_PREFIX} [{c.address}] ignoring optimazable aggregate function, at grain so optimizing"
319
- )
320
338
  rval = f"{self.FUNCTION_GRAIN_MATCH_MAP[c.lineage.operator](args)}"
321
339
  else:
322
340
  logger.debug(
@@ -327,14 +345,24 @@ class BaseDialect:
327
345
  if isinstance(raw_content, RawColumnExpr):
328
346
  rval = raw_content.text
329
347
  elif isinstance(raw_content, Function):
330
- rval = self.render_expr(raw_content, cte=cte)
348
+ rval = self.render_expr(
349
+ raw_content, cte=cte, raise_invalid=raise_invalid
350
+ )
331
351
  else:
332
- rval = f"{safe_get_cte_value(self.FUNCTION_MAP[FunctionType.COALESCE], cte, c, self.QUOTE_CHARACTER)}"
333
- if alias:
334
- return (
335
- f"{rval} as"
336
- f" {self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
337
- )
352
+ rval = safe_get_cte_value(
353
+ self.FUNCTION_MAP[FunctionType.COALESCE],
354
+ cte,
355
+ c,
356
+ self.QUOTE_CHARACTER,
357
+ )
358
+ if not rval:
359
+ if raise_invalid:
360
+ raise ValueError(
361
+ f"Invalid reference string found in query: {rval}, this should never occur. Please report this issue."
362
+ )
363
+ rval = INVALID_REFERENCE_STRING(
364
+ f"Missing source reference to {c.address}"
365
+ )
338
366
  return rval
339
367
 
340
368
  def render_expr(
@@ -370,6 +398,7 @@ class BaseDialect:
370
398
  ],
371
399
  cte: Optional[CTE] = None,
372
400
  cte_map: Optional[Dict[str, CTE]] = None,
401
+ raise_invalid: bool = False,
373
402
  ) -> str:
374
403
 
375
404
  if isinstance(e, SubselectComparison):
@@ -398,9 +427,9 @@ class BaseDialect:
398
427
  target = INVALID_REFERENCE_STRING(
399
428
  f"Missing source CTE for {e.right.address}"
400
429
  )
401
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
430
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
402
431
  elif isinstance(e.right, (ListWrapper, Parenthetical, list)):
403
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map)}"
432
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
404
433
 
405
434
  elif isinstance(
406
435
  e.right,
@@ -411,53 +440,64 @@ class BaseDialect:
411
440
  float,
412
441
  ),
413
442
  ):
414
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} ({self.render_expr(e.right, cte=cte, cte_map=cte_map)})"
443
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} ({self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)})"
415
444
  else:
416
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map)}"
445
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
417
446
  elif isinstance(e, Comparison):
418
- if e.operator == ComparisonOperator.BETWEEN:
419
- right_comp = e.right
420
- assert isinstance(right_comp, Conditional)
421
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(right_comp.left, cte=cte, cte_map=cte_map) and self.render_expr(right_comp.right, cte=cte, cte_map=cte_map)}"
422
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map)}"
447
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
423
448
  elif isinstance(e, Conditional):
424
449
  # conditions need to be nested in parentheses
425
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map)}"
450
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
426
451
  elif isinstance(e, WindowItem):
427
452
  rendered_order_components = [
428
- f"{self.render_expr(x.expr, cte, cte_map=cte_map)} {x.order.value}"
453
+ f"{self.render_expr(x.expr, cte, cte_map=cte_map, raise_invalid=raise_invalid)} {x.order.value}"
429
454
  for x in e.order_by
430
455
  ]
431
456
  rendered_over_components = [
432
- self.render_expr(x, cte, cte_map=cte_map) for x in e.over
457
+ self.render_expr(x, cte, cte_map=cte_map, raise_invalid=raise_invalid)
458
+ for x in e.over
433
459
  ]
434
- return f"{self.WINDOW_FUNCTION_MAP[e.type](concept = self.render_expr(e.content, cte=cte, cte_map=cte_map), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
460
+ return f"{self.WINDOW_FUNCTION_MAP[e.type](concept = self.render_expr(e.content, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
435
461
  elif isinstance(e, Parenthetical):
436
462
  # conditions need to be nested in parentheses
437
463
  if isinstance(e.content, list):
438
- return f"( {','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e.content])} )"
439
- return f"( {self.render_expr(e.content, cte=cte, cte_map=cte_map)} )"
464
+ return f"( {','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e.content])} )"
465
+ return f"( {self.render_expr(e.content, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} )"
440
466
  elif isinstance(e, CaseWhen):
441
- return f"WHEN {self.render_expr(e.comparison, cte=cte, cte_map=cte_map) } THEN {self.render_expr(e.expr, cte=cte, cte_map=cte_map) }"
467
+ return f"WHEN {self.render_expr(e.comparison, cte=cte, cte_map=cte_map) } THEN {self.render_expr(e.expr, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) }"
442
468
  elif isinstance(e, CaseElse):
443
- return f"ELSE {self.render_expr(e.expr, cte=cte, cte_map=cte_map) }"
469
+ return f"ELSE {self.render_expr(e.expr, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) }"
444
470
  elif isinstance(e, Function):
445
471
 
446
472
  if cte and cte.group_to_grain:
447
473
  return self.FUNCTION_MAP[e.operator](
448
- [self.render_expr(z, cte=cte, cte_map=cte_map) for z in e.arguments]
474
+ [
475
+ self.render_expr(
476
+ z, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid
477
+ )
478
+ for z in e.arguments
479
+ ]
449
480
  )
450
481
 
451
482
  return self.FUNCTION_GRAIN_MATCH_MAP[e.operator](
452
- [self.render_expr(z, cte=cte, cte_map=cte_map) for z in e.arguments]
483
+ [
484
+ self.render_expr(
485
+ z, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid
486
+ )
487
+ for z in e.arguments
488
+ ]
453
489
  )
454
490
  elif isinstance(e, AggregateWrapper):
455
- return self.render_expr(e.function, cte, cte_map=cte_map)
491
+ return self.render_expr(
492
+ e.function, cte, cte_map=cte_map, raise_invalid=raise_invalid
493
+ )
456
494
  elif isinstance(e, FilterItem):
457
- return f"CASE WHEN {self.render_expr(e.where.conditional,cte=cte, cte_map=cte_map)} THEN {self.render_expr(e.content, cte, cte_map=cte_map)} ELSE NULL END"
495
+ return f"CASE WHEN {self.render_expr(e.where.conditional,cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} THEN {self.render_expr(e.content, cte, cte_map=cte_map, raise_invalid=raise_invalid)} ELSE NULL END"
458
496
  elif isinstance(e, Concept):
459
497
  if cte:
460
- return self.render_concept_sql(e, cte, alias=False)
498
+ return self.render_concept_sql(
499
+ e, cte, alias=False, raise_invalid=raise_invalid
500
+ )
461
501
  elif cte_map:
462
502
  return f"{cte_map[e.address].name}.{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
463
503
  return f"{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
@@ -468,11 +508,11 @@ class BaseDialect:
468
508
  elif isinstance(e, (int, float)):
469
509
  return str(e)
470
510
  elif isinstance(e, ListWrapper):
471
- return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}]"
511
+ return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}]"
472
512
  elif isinstance(e, MapWrapper):
473
- return f"MAP {{{','.join([f'{self.render_expr(k, cte=cte, cte_map=cte_map)}:{self.render_expr(v, cte=cte, cte_map=cte_map)}' for k, v in e.items()])}}}"
513
+ return f"MAP {{{','.join([f'{self.render_expr(k, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}:{self.render_expr(v, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}' for k, v in e.items()])}}}"
474
514
  elif isinstance(e, list):
475
- return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}"
515
+ return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}"
476
516
  elif isinstance(e, DataType):
477
517
  return str(e.value)
478
518
  elif isinstance(e, DatePart):
trilogy/dialect/common.py CHANGED
@@ -37,10 +37,10 @@ def render_join(
37
37
  if not cte:
38
38
  raise ValueError("must provide a cte to build an unnest joins")
39
39
  if unnest_mode == UnnestMode.CROSS_JOIN:
40
- return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept, render_func, cte)}"
40
+ return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
41
41
  if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
42
- return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept, render_func, cte)}"
43
- return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept, render_func, cte)}"
42
+ return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
43
+ return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
44
44
  left_name = join.left_name
45
45
  right_name = join.right_name
46
46
  right_base = join.right_ref
trilogy/parsing/common.py CHANGED
@@ -9,6 +9,10 @@ from trilogy.core.models import (
9
9
  ListWrapper,
10
10
  MapWrapper,
11
11
  WindowItem,
12
+ Meta,
13
+ Parenthetical,
14
+ FunctionClass,
15
+ Environment,
12
16
  )
13
17
  from typing import List, Tuple
14
18
  from trilogy.core.functions import (
@@ -16,8 +20,61 @@ from trilogy.core.functions import (
16
20
  FunctionType,
17
21
  arg_to_datatype,
18
22
  )
19
- from trilogy.utility import unique
23
+ from trilogy.utility import unique, string_to_hash
20
24
  from trilogy.core.enums import PurposeLineage
25
+ from trilogy.constants import (
26
+ VIRTUAL_CONCEPT_PREFIX,
27
+ )
28
+
29
+
30
+ def process_function_args(
31
+ args,
32
+ meta: Meta | None,
33
+ environment: Environment,
34
+ ):
35
+ final: List[Concept | Function] = []
36
+ for arg in args:
37
+ # if a function has an anonymous function argument
38
+ # create an implicit concept
39
+ while isinstance(arg, Parenthetical):
40
+ arg = arg.content
41
+ if isinstance(arg, Function):
42
+ # if it's not an aggregate function, we can skip the virtual concepts
43
+ # to simplify anonymous function handling
44
+ if (
45
+ arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
46
+ and arg.operator != FunctionType.UNNEST
47
+ ):
48
+ final.append(arg)
49
+ continue
50
+ id_hash = string_to_hash(str(arg))
51
+ concept = function_to_concept(
52
+ arg,
53
+ name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
54
+ namespace=environment.namespace,
55
+ )
56
+ # to satisfy mypy, concept will always have metadata
57
+ if concept.metadata and meta:
58
+ concept.metadata.line_number = meta.line
59
+ environment.add_concept(concept, meta=meta)
60
+ final.append(concept)
61
+ elif isinstance(
62
+ arg, (FilterItem, WindowItem, AggregateWrapper, ListWrapper, MapWrapper)
63
+ ):
64
+ id_hash = string_to_hash(str(arg))
65
+ concept = arbitrary_to_concept(
66
+ arg,
67
+ name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
68
+ namespace=environment.namespace,
69
+ )
70
+ if concept.metadata and meta:
71
+ concept.metadata.line_number = meta.line
72
+ environment.add_concept(concept, meta=meta)
73
+ final.append(concept)
74
+
75
+ else:
76
+ final.append(arg)
77
+ return final
21
78
 
22
79
 
23
80
  def get_purpose_and_keys(