pytrilogy 0.0.1.103__tar.gz → 0.0.1.104__tar.gz
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.
- {pytrilogy-0.0.1.103/pytrilogy.egg-info → pytrilogy-0.0.1.104}/PKG-INFO +1 -1
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104/pytrilogy.egg-info}/PKG-INFO +1 -1
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/models.py +77 -26
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/concept_strategies_v3.py +5 -3
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/filter_node.py +2 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/select_node.py +275 -53
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/__init__.py +54 -1
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/parse_engine.py +23 -12
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/LICENSE.md +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/README.md +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pyproject.toml +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/SOURCES.txt +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/setup.cfg +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/setup.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_models.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_parsing.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_select.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/compiler.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/constants.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/enums.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/environment_helpers.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/exceptions.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/functions.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/graph_models.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/common.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/concept_merge.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/merge_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/base_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/merge_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/utility.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/query_processor.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/base.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/common.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/duckdb.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/presto.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/docs/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/executor.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/common.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/render.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/utility.py +0 -0
|
@@ -355,7 +355,7 @@ class Concept(Namespaced, SelectGrain, BaseModel):
|
|
|
355
355
|
grain = ",".join([str(c.address) for c in self.grain.components])
|
|
356
356
|
return f"{self.namespace}.{self.name}<{grain}>"
|
|
357
357
|
|
|
358
|
-
@
|
|
358
|
+
@cached_property
|
|
359
359
|
def address(self) -> str:
|
|
360
360
|
return f"{self.namespace}.{self.name}"
|
|
361
361
|
|
|
@@ -436,7 +436,8 @@ class Concept(Namespaced, SelectGrain, BaseModel):
|
|
|
436
436
|
modifiers=self.modifiers,
|
|
437
437
|
)
|
|
438
438
|
|
|
439
|
-
|
|
439
|
+
@cached_property
|
|
440
|
+
def _with_default_grain(self) -> "Concept":
|
|
440
441
|
if self.purpose == Purpose.KEY:
|
|
441
442
|
# we need to make this abstract
|
|
442
443
|
grain = Grain(components=[self.with_grain(Grain())], nested=True)
|
|
@@ -473,6 +474,9 @@ class Concept(Namespaced, SelectGrain, BaseModel):
|
|
|
473
474
|
modifiers=self.modifiers,
|
|
474
475
|
)
|
|
475
476
|
|
|
477
|
+
def with_default_grain(self) -> "Concept":
|
|
478
|
+
return self._with_default_grain
|
|
479
|
+
|
|
476
480
|
@property
|
|
477
481
|
def sources(self) -> List["Concept"]:
|
|
478
482
|
if self.lineage:
|
|
@@ -610,7 +614,7 @@ class Grain(BaseModel):
|
|
|
610
614
|
[c.name == ALL_ROWS_CONCEPT for c in self.components]
|
|
611
615
|
)
|
|
612
616
|
|
|
613
|
-
@
|
|
617
|
+
@cached_property
|
|
614
618
|
def set(self):
|
|
615
619
|
return set([c.address for c in self.components_copy])
|
|
616
620
|
|
|
@@ -1585,7 +1589,7 @@ class Datasource(Namespaced, BaseModel):
|
|
|
1585
1589
|
columns=[c.with_namespace(namespace) for c in self.columns],
|
|
1586
1590
|
)
|
|
1587
1591
|
|
|
1588
|
-
@
|
|
1592
|
+
@cached_property
|
|
1589
1593
|
def concepts(self) -> List[Concept]:
|
|
1590
1594
|
return [c.concept for c in self.columns]
|
|
1591
1595
|
|
|
@@ -1780,7 +1784,7 @@ class QueryDatasource(BaseModel):
|
|
|
1780
1784
|
|
|
1781
1785
|
@field_validator("source_map")
|
|
1782
1786
|
@classmethod
|
|
1783
|
-
def validate_source_map(cls, v, info
|
|
1787
|
+
def validate_source_map(cls, v, info: ValidationInfo):
|
|
1784
1788
|
values = info.data
|
|
1785
1789
|
expected = {c.address for c in values["output_concepts"]}.union(
|
|
1786
1790
|
c.address for c in values["input_concepts"]
|
|
@@ -2288,8 +2292,8 @@ class EnvironmentConceptDict(dict):
|
|
|
2288
2292
|
|
|
2289
2293
|
class ImportStatement(BaseModel):
|
|
2290
2294
|
alias: str
|
|
2291
|
-
path:
|
|
2292
|
-
|
|
2295
|
+
path: Path
|
|
2296
|
+
environment: Union["Environment", None] = None
|
|
2293
2297
|
# TODO: this might result in a lot of duplication
|
|
2294
2298
|
# environment:"Environment"
|
|
2295
2299
|
|
|
@@ -2324,6 +2328,9 @@ class Environment(BaseModel):
|
|
|
2324
2328
|
version: str = Field(default_factory=get_version)
|
|
2325
2329
|
cte_name_map: Dict[str, str] = Field(default_factory=dict)
|
|
2326
2330
|
|
|
2331
|
+
materialized_concepts: List[Concept] = Field(default_factory=list)
|
|
2332
|
+
_parse_count: int = 0
|
|
2333
|
+
|
|
2327
2334
|
@classmethod
|
|
2328
2335
|
def from_file(cls, path: str | Path) -> "Environment":
|
|
2329
2336
|
with open(path, "r") as f:
|
|
@@ -2349,20 +2356,14 @@ class Environment(BaseModel):
|
|
|
2349
2356
|
f.write(self.model_dump_json())
|
|
2350
2357
|
return ppath
|
|
2351
2358
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
if concept.address in [x.address for x in datasource.output_concepts]:
|
|
2361
|
-
found = True
|
|
2362
|
-
break
|
|
2363
|
-
if found:
|
|
2364
|
-
output.append(concept)
|
|
2365
|
-
return output
|
|
2359
|
+
def gen_materialized_concepts(self) -> None:
|
|
2360
|
+
concrete_addresses = set()
|
|
2361
|
+
for datasource in self.datasources.values():
|
|
2362
|
+
for concept in datasource.output_concepts:
|
|
2363
|
+
concrete_addresses.add(concept.address)
|
|
2364
|
+
self.materialized_concepts = [
|
|
2365
|
+
c for c in self.concepts.values() if c.address in concrete_addresses
|
|
2366
|
+
]
|
|
2366
2367
|
|
|
2367
2368
|
def validate_concept(self, lookup: str, meta: Meta | None = None):
|
|
2368
2369
|
existing: Concept = self.concepts.get(lookup) # type: ignore
|
|
@@ -2392,12 +2393,61 @@ class Environment(BaseModel):
|
|
|
2392
2393
|
|
|
2393
2394
|
def add_import(self, alias: str, environment: Environment):
|
|
2394
2395
|
self.imports[alias] = ImportStatement(
|
|
2395
|
-
alias=alias, path=
|
|
2396
|
+
alias=alias, path=Path(environment.working_path)
|
|
2396
2397
|
)
|
|
2397
2398
|
for key, concept in environment.concepts.items():
|
|
2398
2399
|
self.concepts[f"{alias}.{key}"] = concept.with_namespace(alias)
|
|
2399
2400
|
for key, datasource in environment.datasources.items():
|
|
2400
2401
|
self.datasources[f"{alias}.{key}"] = datasource.with_namespace(alias)
|
|
2402
|
+
self.gen_materialized_concepts()
|
|
2403
|
+
return self
|
|
2404
|
+
|
|
2405
|
+
def add_file_import(self, path: str, alias: str, env: Environment | None = None):
|
|
2406
|
+
from trilogy.parsing.parse_engine import ParseToObjects, PARSER
|
|
2407
|
+
|
|
2408
|
+
apath = path.split(".")
|
|
2409
|
+
apath[-1] = apath[-1] + ".preql"
|
|
2410
|
+
|
|
2411
|
+
target: Path = Path(self.working_path, *apath)
|
|
2412
|
+
if env:
|
|
2413
|
+
self.imports[alias] = ImportStatement(
|
|
2414
|
+
alias=alias, path=target, environment=env
|
|
2415
|
+
)
|
|
2416
|
+
|
|
2417
|
+
elif alias in self.imports:
|
|
2418
|
+
current = self.imports[alias]
|
|
2419
|
+
env = self.imports[alias].environment
|
|
2420
|
+
if current.path != target:
|
|
2421
|
+
raise ImportError(
|
|
2422
|
+
f"Attempted to import {target} with alias {alias} but {alias} is already imported from {current.path}"
|
|
2423
|
+
)
|
|
2424
|
+
else:
|
|
2425
|
+
try:
|
|
2426
|
+
with open(target, "r", encoding="utf-8") as f:
|
|
2427
|
+
text = f.read()
|
|
2428
|
+
nparser = ParseToObjects(
|
|
2429
|
+
visit_tokens=True,
|
|
2430
|
+
text=text,
|
|
2431
|
+
environment=Environment(
|
|
2432
|
+
working_path=target.parent,
|
|
2433
|
+
),
|
|
2434
|
+
parse_address=str(target),
|
|
2435
|
+
)
|
|
2436
|
+
nparser.transform(PARSER.parse(text))
|
|
2437
|
+
except Exception as e:
|
|
2438
|
+
raise ImportError(
|
|
2439
|
+
f"Unable to import file {target.parent}, parsing error: {e}"
|
|
2440
|
+
)
|
|
2441
|
+
env = nparser.environment
|
|
2442
|
+
if env:
|
|
2443
|
+
for _, concept in env.concepts.items():
|
|
2444
|
+
self.add_concept(concept.with_namespace(alias))
|
|
2445
|
+
|
|
2446
|
+
for _, datasource in env.datasources.items():
|
|
2447
|
+
self.add_datasource(datasource.with_namespace(alias))
|
|
2448
|
+
imps = ImportStatement(alias=alias, path=target, environment=env)
|
|
2449
|
+
self.imports[alias] = imps
|
|
2450
|
+
return imps
|
|
2401
2451
|
|
|
2402
2452
|
def parse(
|
|
2403
2453
|
self, input: str, namespace: str | None = None, persist: bool = False
|
|
@@ -2448,21 +2498,22 @@ class Environment(BaseModel):
|
|
|
2448
2498
|
from trilogy.core.environment_helpers import generate_related_concepts
|
|
2449
2499
|
|
|
2450
2500
|
generate_related_concepts(concept, self)
|
|
2501
|
+
self.gen_materialized_concepts()
|
|
2451
2502
|
return concept
|
|
2452
2503
|
|
|
2453
2504
|
def add_datasource(
|
|
2454
2505
|
self,
|
|
2455
2506
|
datasource: Datasource,
|
|
2507
|
+
meta: Meta | None = None,
|
|
2456
2508
|
):
|
|
2457
|
-
if datasource.namespace == DEFAULT_NAMESPACE:
|
|
2458
|
-
self.datasources[datasource.name] = datasource
|
|
2459
|
-
return datasource
|
|
2460
|
-
if not datasource.namespace:
|
|
2509
|
+
if not datasource.namespace or datasource.namespace == DEFAULT_NAMESPACE:
|
|
2461
2510
|
self.datasources[datasource.name] = datasource
|
|
2511
|
+
self.gen_materialized_concepts()
|
|
2462
2512
|
return datasource
|
|
2463
2513
|
self.datasources[datasource.namespace + "." + datasource.identifier] = (
|
|
2464
2514
|
datasource
|
|
2465
2515
|
)
|
|
2516
|
+
self.gen_materialized_concepts()
|
|
2466
2517
|
return datasource
|
|
2467
2518
|
|
|
2468
2519
|
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/concept_strategies_v3.py
RENAMED
|
@@ -23,7 +23,6 @@ from trilogy.core.processing.node_generators import (
|
|
|
23
23
|
gen_window_node,
|
|
24
24
|
gen_group_node,
|
|
25
25
|
gen_basic_node,
|
|
26
|
-
gen_select_node,
|
|
27
26
|
gen_unnest_node,
|
|
28
27
|
gen_merge_node,
|
|
29
28
|
gen_group_to_node,
|
|
@@ -208,7 +207,8 @@ def generate_node(
|
|
|
208
207
|
history: History | None = None,
|
|
209
208
|
) -> StrategyNode | None:
|
|
210
209
|
# first check in case there is a materialized_concept
|
|
211
|
-
|
|
210
|
+
history = history or History()
|
|
211
|
+
candidate = history.gen_select_node(
|
|
212
212
|
concept,
|
|
213
213
|
local_optional,
|
|
214
214
|
environment,
|
|
@@ -218,6 +218,7 @@ def generate_node(
|
|
|
218
218
|
accept_partial=accept_partial,
|
|
219
219
|
accept_partial_optional=False,
|
|
220
220
|
)
|
|
221
|
+
|
|
221
222
|
if candidate:
|
|
222
223
|
return candidate
|
|
223
224
|
|
|
@@ -320,7 +321,7 @@ def generate_node(
|
|
|
320
321
|
logger.info(
|
|
321
322
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating select node with optional {[x.address for x in local_optional]}"
|
|
322
323
|
)
|
|
323
|
-
return gen_select_node(
|
|
324
|
+
return history.gen_select_node(
|
|
324
325
|
concept,
|
|
325
326
|
local_optional,
|
|
326
327
|
environment,
|
|
@@ -328,6 +329,7 @@ def generate_node(
|
|
|
328
329
|
depth + 1,
|
|
329
330
|
fail_if_not_found=False,
|
|
330
331
|
accept_partial=accept_partial,
|
|
332
|
+
accept_partial_optional=True,
|
|
331
333
|
)
|
|
332
334
|
else:
|
|
333
335
|
raise ValueError(f"Unknown derivation {concept.derivation}")
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/select_node.py
RENAMED
|
@@ -2,7 +2,13 @@ from itertools import combinations
|
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
4
|
from trilogy.core.enums import PurposeLineage
|
|
5
|
-
from trilogy.core.models import
|
|
5
|
+
from trilogy.core.models import (
|
|
6
|
+
Concept,
|
|
7
|
+
Environment,
|
|
8
|
+
Grain,
|
|
9
|
+
LooseConceptList,
|
|
10
|
+
Datasource,
|
|
11
|
+
)
|
|
6
12
|
from trilogy.core.processing.nodes import (
|
|
7
13
|
StrategyNode,
|
|
8
14
|
SelectNode,
|
|
@@ -15,10 +21,211 @@ import networkx as nx
|
|
|
15
21
|
from trilogy.core.graph_models import concept_to_node, datasource_to_node
|
|
16
22
|
from trilogy.constants import logger
|
|
17
23
|
from trilogy.core.processing.utility import padding
|
|
24
|
+
from dataclasses import dataclass
|
|
18
25
|
|
|
19
26
|
LOGGER_PREFIX = "[GEN_SELECT_NODE]"
|
|
20
27
|
|
|
21
28
|
|
|
29
|
+
@dataclass
|
|
30
|
+
class DatasourceMatch:
|
|
31
|
+
key: str
|
|
32
|
+
datasource: Datasource
|
|
33
|
+
matched: LooseConceptList
|
|
34
|
+
partial: LooseConceptList
|
|
35
|
+
|
|
36
|
+
def __repr__(self):
|
|
37
|
+
return f"DatasourceMatch({self.key}, {self.datasource.identifier}, {str(self.matched)}, {str(self.partial)})"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dm_to_strategy_node(
|
|
41
|
+
dm: DatasourceMatch,
|
|
42
|
+
target_grain: Grain,
|
|
43
|
+
environment: Environment,
|
|
44
|
+
g: nx.DiGraph,
|
|
45
|
+
depth: int,
|
|
46
|
+
accept_partial: bool = False,
|
|
47
|
+
) -> StrategyNode:
|
|
48
|
+
datasource = dm.datasource
|
|
49
|
+
if target_grain and target_grain.issubset(datasource.grain):
|
|
50
|
+
if all([x in dm.matched for x in target_grain.components]):
|
|
51
|
+
force_group = False
|
|
52
|
+
# if we are not returning the grain
|
|
53
|
+
# we have to group
|
|
54
|
+
else:
|
|
55
|
+
logger.info(
|
|
56
|
+
f"{padding(depth)}{LOGGER_PREFIX} not all grain components are in output {str(dm.matched)}, group to actual grain"
|
|
57
|
+
)
|
|
58
|
+
force_group = True
|
|
59
|
+
elif all([x in dm.matched for x in datasource.grain.components]):
|
|
60
|
+
logger.info(
|
|
61
|
+
f"{padding(depth)}{LOGGER_PREFIX} query output includes all grain components, no reason to group further"
|
|
62
|
+
)
|
|
63
|
+
force_group = False
|
|
64
|
+
else:
|
|
65
|
+
logger.info(
|
|
66
|
+
f"{padding(depth)}{LOGGER_PREFIX} target grain is not subset of datasource grain {datasource.grain}, required to group"
|
|
67
|
+
)
|
|
68
|
+
force_group = True
|
|
69
|
+
bcandidate: StrategyNode = SelectNode(
|
|
70
|
+
input_concepts=[c.concept for c in datasource.columns],
|
|
71
|
+
output_concepts=dm.matched.concepts,
|
|
72
|
+
environment=environment,
|
|
73
|
+
g=g,
|
|
74
|
+
parents=[],
|
|
75
|
+
depth=depth,
|
|
76
|
+
partial_concepts=dm.partial.concepts,
|
|
77
|
+
accept_partial=accept_partial,
|
|
78
|
+
datasource=datasource,
|
|
79
|
+
grain=Grain(components=dm.matched.concepts),
|
|
80
|
+
)
|
|
81
|
+
# we need to nest the group node one further
|
|
82
|
+
if force_group is True:
|
|
83
|
+
candidate: StrategyNode = GroupNode(
|
|
84
|
+
output_concepts=dm.matched.concepts,
|
|
85
|
+
input_concepts=dm.matched.concepts,
|
|
86
|
+
environment=environment,
|
|
87
|
+
g=g,
|
|
88
|
+
parents=[bcandidate],
|
|
89
|
+
depth=depth,
|
|
90
|
+
partial_concepts=bcandidate.partial_concepts,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
candidate = bcandidate
|
|
94
|
+
return candidate
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def gen_select_nodes_from_tables_v2(
|
|
98
|
+
mandatory_concept: Concept,
|
|
99
|
+
all_concepts: List[Concept],
|
|
100
|
+
g: nx.DiGraph,
|
|
101
|
+
environment: Environment,
|
|
102
|
+
depth: int,
|
|
103
|
+
target_grain: Grain,
|
|
104
|
+
accept_partial: bool = False,
|
|
105
|
+
) -> tuple[bool, list[Concept], list[StrategyNode]]:
|
|
106
|
+
# if we have only constants
|
|
107
|
+
# we don't need a table
|
|
108
|
+
# so verify nothing, select node will render
|
|
109
|
+
all_lcl = LooseConceptList(concepts=all_concepts)
|
|
110
|
+
if all([c.derivation == PurposeLineage.CONSTANT for c in all_lcl]):
|
|
111
|
+
logger.info(
|
|
112
|
+
f"{padding(depth)}{LOGGER_PREFIX} All concepts {[x.address for x in all_lcl]} are constants, returning constant node"
|
|
113
|
+
)
|
|
114
|
+
return (
|
|
115
|
+
True,
|
|
116
|
+
all_lcl.concepts,
|
|
117
|
+
[
|
|
118
|
+
ConstantNode(
|
|
119
|
+
output_concepts=all_lcl.concepts,
|
|
120
|
+
input_concepts=[],
|
|
121
|
+
environment=environment,
|
|
122
|
+
g=g,
|
|
123
|
+
parents=[],
|
|
124
|
+
depth=depth,
|
|
125
|
+
# no partial for constants
|
|
126
|
+
partial_concepts=[],
|
|
127
|
+
force_group=False,
|
|
128
|
+
)
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
# otherwise, we need to look for a table
|
|
132
|
+
nodes_to_find = [concept_to_node(x.with_default_grain()) for x in all_lcl.concepts]
|
|
133
|
+
matches: dict[str, DatasourceMatch] = {}
|
|
134
|
+
for k, datasource in environment.datasources.items():
|
|
135
|
+
matched = []
|
|
136
|
+
matched_paths = []
|
|
137
|
+
for idx, req_concept in enumerate(nodes_to_find):
|
|
138
|
+
try:
|
|
139
|
+
path = nx.shortest_path(
|
|
140
|
+
g,
|
|
141
|
+
source=datasource_to_node(datasource),
|
|
142
|
+
target=req_concept,
|
|
143
|
+
)
|
|
144
|
+
ds_valid = (
|
|
145
|
+
sum(
|
|
146
|
+
[
|
|
147
|
+
1 if g.nodes[node]["type"] == "datasource" else 0
|
|
148
|
+
for node in path
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
== 1
|
|
152
|
+
)
|
|
153
|
+
address_valid = (
|
|
154
|
+
sum(
|
|
155
|
+
[
|
|
156
|
+
(
|
|
157
|
+
1
|
|
158
|
+
if g.nodes[node]["type"] == "concept"
|
|
159
|
+
and g.nodes[node]["concept"].address
|
|
160
|
+
!= all_lcl.concepts[idx].address
|
|
161
|
+
else 0
|
|
162
|
+
)
|
|
163
|
+
for node in path
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
== 0
|
|
167
|
+
)
|
|
168
|
+
if ds_valid and address_valid:
|
|
169
|
+
matched_paths.append(path)
|
|
170
|
+
matched.append(all_lcl.concepts[idx])
|
|
171
|
+
except nx.NodeNotFound:
|
|
172
|
+
continue
|
|
173
|
+
except nx.exception.NetworkXNoPath:
|
|
174
|
+
continue
|
|
175
|
+
dm = DatasourceMatch(
|
|
176
|
+
key=k,
|
|
177
|
+
datasource=datasource,
|
|
178
|
+
matched=LooseConceptList(concepts=matched),
|
|
179
|
+
partial=LooseConceptList(
|
|
180
|
+
concepts=[
|
|
181
|
+
c.concept
|
|
182
|
+
for c in datasource.columns
|
|
183
|
+
if not c.is_complete and c.concept.address in all_lcl
|
|
184
|
+
]
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
if not matched:
|
|
188
|
+
continue
|
|
189
|
+
if mandatory_concept.address not in dm.matched:
|
|
190
|
+
continue
|
|
191
|
+
if not accept_partial and dm.partial.addresses:
|
|
192
|
+
continue
|
|
193
|
+
matches[k] = dm
|
|
194
|
+
found: set[str] = set()
|
|
195
|
+
all_found = False
|
|
196
|
+
all_checked = False
|
|
197
|
+
to_return: list[StrategyNode] = []
|
|
198
|
+
if not matches:
|
|
199
|
+
return False, [], []
|
|
200
|
+
while not all_found and not all_checked:
|
|
201
|
+
final_key: str = max(
|
|
202
|
+
matches,
|
|
203
|
+
key=lambda x: len(
|
|
204
|
+
[m for m in matches[x].matched.addresses if m not in found]
|
|
205
|
+
)
|
|
206
|
+
- 0.1 * len(matches[x].partial.addresses),
|
|
207
|
+
)
|
|
208
|
+
final: DatasourceMatch = matches[final_key]
|
|
209
|
+
candidate = dm_to_strategy_node(
|
|
210
|
+
final,
|
|
211
|
+
target_grain=Grain(
|
|
212
|
+
components=[
|
|
213
|
+
x for x in target_grain.components if x.address in final.matched
|
|
214
|
+
]
|
|
215
|
+
),
|
|
216
|
+
environment=environment,
|
|
217
|
+
g=g,
|
|
218
|
+
depth=depth,
|
|
219
|
+
accept_partial=accept_partial,
|
|
220
|
+
)
|
|
221
|
+
to_return.append(candidate)
|
|
222
|
+
del matches[final_key]
|
|
223
|
+
found = found.union(final.matched.addresses)
|
|
224
|
+
all_found = all_lcl.addresses.issubset(found)
|
|
225
|
+
all_checked = len(matches) == 0
|
|
226
|
+
return all_found, [x for x in all_concepts if x.address in found], to_return
|
|
227
|
+
|
|
228
|
+
|
|
22
229
|
def gen_select_node_from_table(
|
|
23
230
|
target_concept: Concept,
|
|
24
231
|
all_concepts: List[Concept],
|
|
@@ -166,58 +373,15 @@ def gen_select_node_from_table(
|
|
|
166
373
|
return candidates[final]
|
|
167
374
|
|
|
168
375
|
|
|
169
|
-
def
|
|
170
|
-
concept: Concept,
|
|
376
|
+
def gen_select_nodes_from_tables(
|
|
171
377
|
local_optional: List[Concept],
|
|
172
|
-
environment: Environment,
|
|
173
|
-
g,
|
|
174
378
|
depth: int,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
all_lcl = LooseConceptList(concepts=all_concepts)
|
|
182
|
-
materialized_lcl = LooseConceptList(
|
|
183
|
-
concepts=[
|
|
184
|
-
x
|
|
185
|
-
for x in all_concepts
|
|
186
|
-
if x.address in [z.address for z in environment.materialized_concepts]
|
|
187
|
-
or x.derivation == PurposeLineage.CONSTANT
|
|
188
|
-
]
|
|
189
|
-
)
|
|
190
|
-
if not target_grain:
|
|
191
|
-
target_grain = Grain()
|
|
192
|
-
for ac in all_concepts:
|
|
193
|
-
target_grain += ac.grain
|
|
194
|
-
if materialized_lcl != all_lcl:
|
|
195
|
-
logger.info(
|
|
196
|
-
f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concept.address} "
|
|
197
|
-
f" as it + optional (looking for all {all_lcl}) includes non-materialized concepts {all_lcl.difference(materialized_lcl)} vs materialized: {materialized_lcl}"
|
|
198
|
-
)
|
|
199
|
-
if fail_if_not_found:
|
|
200
|
-
raise NoDatasourceException(f"No datasource exists for {concept}")
|
|
201
|
-
return None
|
|
202
|
-
|
|
203
|
-
ds: StrategyNode | None = None
|
|
204
|
-
|
|
205
|
-
# attempt to select all concepts from table
|
|
206
|
-
ds = gen_select_node_from_table(
|
|
207
|
-
concept,
|
|
208
|
-
[concept] + local_optional,
|
|
209
|
-
g=g,
|
|
210
|
-
environment=environment,
|
|
211
|
-
depth=depth,
|
|
212
|
-
accept_partial=accept_partial,
|
|
213
|
-
target_grain=target_grain,
|
|
214
|
-
)
|
|
215
|
-
if ds:
|
|
216
|
-
logger.info(
|
|
217
|
-
f"{padding(depth)}{LOGGER_PREFIX} Found select node with all target concepts, force group is {ds.force_group}, target grain {target_grain}"
|
|
218
|
-
)
|
|
219
|
-
return ds
|
|
220
|
-
# if we cannot find a match
|
|
379
|
+
concept: Concept,
|
|
380
|
+
environment: Environment,
|
|
381
|
+
g: nx.DiGraph,
|
|
382
|
+
accept_partial: bool,
|
|
383
|
+
all_concepts: List[Concept],
|
|
384
|
+
) -> tuple[bool, list[Concept], list[StrategyNode]]:
|
|
221
385
|
parents: List[StrategyNode] = []
|
|
222
386
|
found: List[Concept] = []
|
|
223
387
|
logger.info(
|
|
@@ -238,7 +402,7 @@ def gen_select_node(
|
|
|
238
402
|
)
|
|
239
403
|
if not ds:
|
|
240
404
|
unreachable.append(opt_con.address)
|
|
241
|
-
|
|
405
|
+
all_found = False
|
|
242
406
|
for x in reversed(range(1, len(local_optional) + 1)):
|
|
243
407
|
if all_found:
|
|
244
408
|
break
|
|
@@ -275,6 +439,64 @@ def gen_select_node(
|
|
|
275
439
|
f"{padding(depth)}{LOGGER_PREFIX} found all optional {[c.address for c in local_optional]}"
|
|
276
440
|
)
|
|
277
441
|
all_found = True
|
|
442
|
+
return all_found, found, parents
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def gen_select_node(
|
|
446
|
+
concept: Concept,
|
|
447
|
+
local_optional: List[Concept],
|
|
448
|
+
environment: Environment,
|
|
449
|
+
g,
|
|
450
|
+
depth: int,
|
|
451
|
+
accept_partial: bool = False,
|
|
452
|
+
fail_if_not_found: bool = True,
|
|
453
|
+
accept_partial_optional: bool = True,
|
|
454
|
+
target_grain: Grain | None = None,
|
|
455
|
+
) -> StrategyNode | None:
|
|
456
|
+
all_concepts = [concept] + local_optional
|
|
457
|
+
all_lcl = LooseConceptList(concepts=all_concepts)
|
|
458
|
+
materialized_lcl = LooseConceptList(
|
|
459
|
+
concepts=[
|
|
460
|
+
x
|
|
461
|
+
for x in all_concepts
|
|
462
|
+
if x.address in [z.address for z in environment.materialized_concepts]
|
|
463
|
+
or x.derivation == PurposeLineage.CONSTANT
|
|
464
|
+
]
|
|
465
|
+
)
|
|
466
|
+
if not target_grain:
|
|
467
|
+
target_grain = Grain()
|
|
468
|
+
for ac in all_concepts:
|
|
469
|
+
target_grain += ac.grain
|
|
470
|
+
if materialized_lcl != all_lcl:
|
|
471
|
+
logger.info(
|
|
472
|
+
f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concept.address} "
|
|
473
|
+
f" as it + optional (looking for all {all_lcl}) includes non-materialized concepts {all_lcl.difference(materialized_lcl)} vs materialized: {materialized_lcl}"
|
|
474
|
+
)
|
|
475
|
+
if fail_if_not_found:
|
|
476
|
+
raise NoDatasourceException(f"No datasource exists for {concept}")
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
ds: StrategyNode | None = None
|
|
480
|
+
|
|
481
|
+
# attempt to select all concepts from table
|
|
482
|
+
ds = gen_select_node_from_table(
|
|
483
|
+
concept,
|
|
484
|
+
[concept] + local_optional,
|
|
485
|
+
g=g,
|
|
486
|
+
environment=environment,
|
|
487
|
+
depth=depth,
|
|
488
|
+
accept_partial=accept_partial,
|
|
489
|
+
target_grain=target_grain,
|
|
490
|
+
)
|
|
491
|
+
if ds:
|
|
492
|
+
logger.info(
|
|
493
|
+
f"{padding(depth)}{LOGGER_PREFIX} Found select node with all target concepts, force group is {ds.force_group}, target grain {target_grain}"
|
|
494
|
+
)
|
|
495
|
+
return ds
|
|
496
|
+
# if we cannot find a match
|
|
497
|
+
all_found, found, parents = gen_select_nodes_from_tables_v2(
|
|
498
|
+
concept, all_concepts, g, environment, depth, target_grain, accept_partial
|
|
499
|
+
)
|
|
278
500
|
if parents and (all_found or accept_partial_optional):
|
|
279
501
|
if all_found:
|
|
280
502
|
logger.info(
|
|
@@ -282,7 +504,7 @@ def gen_select_node(
|
|
|
282
504
|
)
|
|
283
505
|
else:
|
|
284
506
|
logger.info(
|
|
285
|
-
f"{padding(depth)}{LOGGER_PREFIX} found some optional
|
|
507
|
+
f"{padding(depth)}{LOGGER_PREFIX} found some optional, returning"
|
|
286
508
|
)
|
|
287
509
|
all_partial = [
|
|
288
510
|
c
|
|
@@ -6,11 +6,12 @@ from .window_node import WindowNode
|
|
|
6
6
|
from .base_node import StrategyNode, NodeJoin
|
|
7
7
|
from .unnest_node import UnnestNode
|
|
8
8
|
from pydantic import BaseModel, Field, ConfigDict
|
|
9
|
-
from trilogy.core.models import Concept
|
|
9
|
+
from trilogy.core.models import Concept, Environment
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class History(BaseModel):
|
|
13
13
|
history: dict[str, StrategyNode | None] = Field(default_factory=dict)
|
|
14
|
+
select_history: dict[str, StrategyNode | None] = Field(default_factory=dict)
|
|
14
15
|
started: set[str] = Field(default_factory=set)
|
|
15
16
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
16
17
|
|
|
@@ -60,6 +61,58 @@ class History(BaseModel):
|
|
|
60
61
|
in self.started
|
|
61
62
|
)
|
|
62
63
|
|
|
64
|
+
def _select_concepts_to_lookup(
|
|
65
|
+
self,
|
|
66
|
+
main: Concept,
|
|
67
|
+
search: list[Concept],
|
|
68
|
+
accept_partial: bool,
|
|
69
|
+
fail_if_not_found: bool,
|
|
70
|
+
accept_partial_optional: bool,
|
|
71
|
+
) -> str:
|
|
72
|
+
return (
|
|
73
|
+
str(main.address)
|
|
74
|
+
+ "|"
|
|
75
|
+
+ "-".join([c.address for c in search])
|
|
76
|
+
+ str(accept_partial)
|
|
77
|
+
+ str(fail_if_not_found)
|
|
78
|
+
+ str(accept_partial_optional)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def gen_select_node(
|
|
82
|
+
self,
|
|
83
|
+
concept: Concept,
|
|
84
|
+
local_optional: list[Concept],
|
|
85
|
+
environment: Environment,
|
|
86
|
+
g,
|
|
87
|
+
depth: int,
|
|
88
|
+
fail_if_not_found: bool = False,
|
|
89
|
+
accept_partial: bool = False,
|
|
90
|
+
accept_partial_optional: bool = False,
|
|
91
|
+
) -> StrategyNode | None:
|
|
92
|
+
from trilogy.core.processing.node_generators.select_node import gen_select_node
|
|
93
|
+
|
|
94
|
+
fingerprint = self._select_concepts_to_lookup(
|
|
95
|
+
concept,
|
|
96
|
+
local_optional,
|
|
97
|
+
accept_partial,
|
|
98
|
+
fail_if_not_found,
|
|
99
|
+
accept_partial_optional,
|
|
100
|
+
)
|
|
101
|
+
if fingerprint in self.select_history:
|
|
102
|
+
return self.select_history[fingerprint]
|
|
103
|
+
gen = gen_select_node(
|
|
104
|
+
concept,
|
|
105
|
+
local_optional,
|
|
106
|
+
environment,
|
|
107
|
+
g,
|
|
108
|
+
depth + 1,
|
|
109
|
+
fail_if_not_found=fail_if_not_found,
|
|
110
|
+
accept_partial=accept_partial,
|
|
111
|
+
accept_partial_optional=accept_partial_optional,
|
|
112
|
+
)
|
|
113
|
+
self.select_history[fingerprint] = gen
|
|
114
|
+
return gen
|
|
115
|
+
|
|
63
116
|
|
|
64
117
|
__all__ = [
|
|
65
118
|
"FilterNode",
|
|
@@ -9,6 +9,7 @@ from lark.exceptions import (
|
|
|
9
9
|
UnexpectedToken,
|
|
10
10
|
VisitError,
|
|
11
11
|
)
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
from lark.tree import Meta
|
|
13
14
|
from pydantic import ValidationError
|
|
14
15
|
from trilogy.core.internal import INTERNAL_NAMESPACE, ALL_ROWS_CONCEPT
|
|
@@ -466,25 +467,34 @@ class ParseToObjects(Transformer):
|
|
|
466
467
|
text,
|
|
467
468
|
environment: Environment,
|
|
468
469
|
parse_address: str | None = None,
|
|
469
|
-
parsed: dict | None = None,
|
|
470
|
+
parsed: dict[str, "ParseToObjects"] | None = None,
|
|
470
471
|
):
|
|
471
472
|
Transformer.__init__(self, visit_tokens)
|
|
472
473
|
self.text = text
|
|
473
474
|
self.environment: Environment = environment
|
|
474
|
-
self.imported: set[str] = set()
|
|
475
475
|
self.parse_address = parse_address or "root"
|
|
476
476
|
self.parsed: dict[str, ParseToObjects] = parsed if parsed else {}
|
|
477
477
|
# we do a second pass to pick up circular dependencies
|
|
478
478
|
# after initial parsing
|
|
479
479
|
self.pass_count = 1
|
|
480
|
+
self._results_stash = None
|
|
481
|
+
|
|
482
|
+
def transform(self, tree):
|
|
483
|
+
results = super().transform(tree)
|
|
484
|
+
self._results_stash = results
|
|
485
|
+
self.environment._parse_count += 1
|
|
486
|
+
return results
|
|
480
487
|
|
|
481
488
|
def hydrate_missing(self):
|
|
482
489
|
self.pass_count = 2
|
|
483
490
|
for k, v in self.parsed.items():
|
|
491
|
+
|
|
484
492
|
if v.pass_count == 2:
|
|
485
493
|
continue
|
|
486
494
|
v.hydrate_missing()
|
|
487
495
|
self.environment.concepts.fail_on_missing = True
|
|
496
|
+
# if not self.environment.concepts.undefined:
|
|
497
|
+
# return self._results_stash
|
|
488
498
|
reparsed = self.transform(PARSER.parse(self.text))
|
|
489
499
|
self.environment.concepts.undefined = {}
|
|
490
500
|
return reparsed
|
|
@@ -932,7 +942,7 @@ class ParseToObjects(Transformer):
|
|
|
932
942
|
)
|
|
933
943
|
for column in columns:
|
|
934
944
|
column.concept = column.concept.with_grain(datasource.grain)
|
|
935
|
-
self.environment.
|
|
945
|
+
self.environment.add_datasource(datasource, meta=meta)
|
|
936
946
|
return datasource
|
|
937
947
|
|
|
938
948
|
@v_args(meta=True)
|
|
@@ -1046,12 +1056,11 @@ class ParseToObjects(Transformer):
|
|
|
1046
1056
|
self.environment.add_concept(new, meta=meta)
|
|
1047
1057
|
return merge
|
|
1048
1058
|
|
|
1049
|
-
def import_statement(self, args: list[str]):
|
|
1059
|
+
def import_statement(self, args: list[str]) -> ImportStatement:
|
|
1050
1060
|
alias = args[-1]
|
|
1051
1061
|
path = args[0].split(".")
|
|
1052
1062
|
|
|
1053
1063
|
target = join(self.environment.working_path, *path) + ".preql"
|
|
1054
|
-
self.imported.add(target)
|
|
1055
1064
|
if target in self.parsed:
|
|
1056
1065
|
nparser = self.parsed[target]
|
|
1057
1066
|
else:
|
|
@@ -1070,21 +1079,23 @@ class ParseToObjects(Transformer):
|
|
|
1070
1079
|
)
|
|
1071
1080
|
nparser.transform(PARSER.parse(text))
|
|
1072
1081
|
self.parsed[target] = nparser
|
|
1082
|
+
# add the parsed objects of the import in
|
|
1083
|
+
self.parsed = {**self.parsed, **nparser.parsed}
|
|
1073
1084
|
except Exception as e:
|
|
1074
1085
|
raise ImportError(
|
|
1075
1086
|
f"Unable to import file {dirname(target)}, parsing error: {e}"
|
|
1076
1087
|
)
|
|
1077
1088
|
|
|
1078
|
-
for
|
|
1079
|
-
# self.environment.concepts[f"{alias}.{key}"] = concept.with_namespace(new_namespace)
|
|
1089
|
+
for _, concept in nparser.environment.concepts.items():
|
|
1080
1090
|
self.environment.add_concept(concept.with_namespace(alias))
|
|
1081
1091
|
|
|
1082
|
-
for
|
|
1092
|
+
for _, datasource in nparser.environment.datasources.items():
|
|
1083
1093
|
self.environment.add_datasource(datasource.with_namespace(alias))
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1094
|
+
imps = ImportStatement(
|
|
1095
|
+
alias=alias, path=Path(args[0]), environment=nparser.environment
|
|
1096
|
+
)
|
|
1097
|
+
self.environment.imports[alias] = imps
|
|
1098
|
+
return imps
|
|
1088
1099
|
|
|
1089
1100
|
@v_args(meta=True)
|
|
1090
1101
|
def show_category(self, meta: Meta, args) -> ShowCategory:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/__init__.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/basic_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/common.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/concept_merge.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_to_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/merge_node.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/rowset_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/unnest_node.py
RENAMED
|
File without changes
|
{pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/window_node.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|