uptimely-core 0.2.0.dev1__tar.gz → 0.3.0__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.
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/PKG-INFO +1 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/pyproject.toml +1 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/__init__.py +0 -8
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/callable_node.py +3 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/compiler.py +2 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/execution_graph.py +11 -28
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/contracts.py +8 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/engine.py +20 -6
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/graph_render.py +1 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/lineage.py +10 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/plan_render.py +13 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/spec_render.py +43 -9
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/mcp/server.py +50 -16
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/__init__.py +1 -6
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/api.py +3 -23
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/feature_graph/graph.py +33 -69
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/feature_graph/models.py +20 -16
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/references.py +63 -7
- uptimely_core-0.3.0/src/uptimely/spec/validation/__init__.py +20 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/validation/context.py +57 -76
- uptimely_core-0.3.0/src/uptimely/spec/validation/dataset.py +89 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/dependencies.py +99 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/dimensions.py +68 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/functions.py +147 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/mapping_contracts.py +221 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/mapping_inputs.py +288 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/mapping_outputs.py +223 -0
- uptimely_core-0.3.0/src/uptimely/spec/validation/spec.py +45 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/execution/data_lineage.html.j2 +13 -5
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_graph_shared.html.j2 +4 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/callable_graph.html.j2 +5 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/index.html.j2 +5 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/dataset_list.html.j2 +12 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/entities.html.j2 +6 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/functions.html.j2 +18 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/overview.html.j2 +2 -2
- uptimely_core-0.2.0.dev1/src/uptimely/spec/resolution.py +0 -78
- uptimely_core-0.2.0.dev1/src/uptimely/spec/resolved.py +0 -60
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/__init__.py +0 -20
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/dependencies.py +0 -23
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/dimensions.py +0 -111
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/functions.py +0 -150
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/mappings.py +0 -434
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/prepared.py +0 -155
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/spec.py +0 -38
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/LICENSE +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/readme.md +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/cli.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/common.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/engine.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/examples.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/explorer.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/plan.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/project.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/spec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/plan/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/plan/models.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/plan/serialization.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/backend.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/bindings.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/config.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/dataframe.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/result.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/state.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/entrypoint.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/explorer.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/mcp/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/project/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/project/build.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/project/paths.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/py.typed +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/render.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/row_context.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/catalog.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/feature_graph/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/json/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/json/codec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/common.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/dataset.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/functions.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/spec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/bind.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/conversion.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/dataset.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/dimension.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/entity.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/feature.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/mappings.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/ref.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/row_context.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/spec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/validation/errors.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/storage.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/base.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/base.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_graph.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_renderer_2d.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_renderer_3d.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/view_controller.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/view_controls.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/icons.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/index.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/callable_renderer_2d.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/graph.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/overview.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/stages.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/datasets.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/dimensions.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/entities.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/features.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/functions.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/index.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/overview.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/section_header.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/dimensions.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/entity_list.html.j2 +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "uptimely-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Manage data-driven numerical analytics with complex dependencies at scale"
|
|
9
9
|
authors = [{ name = "Mikael Ahonen", email = "mikael.ahonen@uptimely.ai" }]
|
|
10
10
|
license = "MIT"
|
|
@@ -3,36 +3,28 @@
|
|
|
3
3
|
from .compiler import CallableGraph, CallableNode, Compiler, ExecutionPlan
|
|
4
4
|
from .engine import Engine
|
|
5
5
|
from .spec import (
|
|
6
|
-
Call,
|
|
7
6
|
Dimension,
|
|
8
7
|
Entity,
|
|
9
8
|
FeatureGraph,
|
|
10
|
-
Function,
|
|
11
|
-
PreparedSpec,
|
|
12
9
|
PythonSpec,
|
|
13
10
|
Spec,
|
|
14
11
|
SubGraph,
|
|
15
12
|
from_python,
|
|
16
13
|
load_spec,
|
|
17
|
-
prepare_spec,
|
|
18
14
|
write_document,
|
|
19
15
|
)
|
|
20
16
|
|
|
21
17
|
__all__ = [
|
|
22
18
|
"Spec",
|
|
23
|
-
"Call",
|
|
24
19
|
"CallableGraph",
|
|
25
20
|
"CallableNode",
|
|
26
21
|
"Compiler",
|
|
27
22
|
"Dimension",
|
|
28
23
|
"Entity",
|
|
29
24
|
"ExecutionPlan",
|
|
30
|
-
"Function",
|
|
31
25
|
"PythonSpec",
|
|
32
|
-
"PreparedSpec",
|
|
33
26
|
"from_python",
|
|
34
27
|
"load_spec",
|
|
35
|
-
"prepare_spec",
|
|
36
28
|
"write_document",
|
|
37
29
|
"SubGraph",
|
|
38
30
|
"FeatureGraph",
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
|
|
5
|
-
from uptimely import render, row_context
|
|
5
|
+
from uptimely import render, row_context
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -17,7 +17,7 @@ class CallableNode:
|
|
|
17
17
|
mapping_id: str | None = None
|
|
18
18
|
output_features: tuple[str, ...] = ()
|
|
19
19
|
args: dict[str, object] = dataclasses.field(default_factory=dict, compare=False, hash=False)
|
|
20
|
-
|
|
20
|
+
input_features: tuple[str, ...] = ()
|
|
21
21
|
row_context: row_context.RowContext | None = dataclasses.field(
|
|
22
22
|
default=None, compare=False, hash=False
|
|
23
23
|
)
|
|
@@ -49,6 +49,6 @@ class CallableNode:
|
|
|
49
49
|
feature=self.feature,
|
|
50
50
|
output_features=list(self.output_features),
|
|
51
51
|
depends_on_features=(
|
|
52
|
-
self.
|
|
52
|
+
list(self.input_features) if self.operation == "vectorize" else None
|
|
53
53
|
),
|
|
54
54
|
)
|
|
@@ -183,6 +183,8 @@ class Compiler:
|
|
|
183
183
|
) -> None:
|
|
184
184
|
"""Require the Python return annotation to satisfy the function contract."""
|
|
185
185
|
returns = function.returns
|
|
186
|
+
if returns == "any":
|
|
187
|
+
return
|
|
186
188
|
if annotation is None:
|
|
187
189
|
raise ValueError(
|
|
188
190
|
f"Function {function.id} must annotate its return as "
|
|
@@ -6,7 +6,7 @@ import graphable as graphable_
|
|
|
6
6
|
from graphable.views import texttree
|
|
7
7
|
|
|
8
8
|
from uptimely import contracts, spec
|
|
9
|
-
from uptimely.spec import
|
|
9
|
+
from uptimely.spec import validation
|
|
10
10
|
|
|
11
11
|
from ..spec import references as bindings
|
|
12
12
|
from . import callable_node as callable_node_module
|
|
@@ -25,16 +25,12 @@ class CallableGraph:
|
|
|
25
25
|
spec: The spec document containing function declarations and invocations.
|
|
26
26
|
"""
|
|
27
27
|
self.spec = spec
|
|
28
|
-
|
|
28
|
+
validation.validate_spec(spec)
|
|
29
29
|
self._datasets = {
|
|
30
30
|
(entity_id, dataset.id): dataset
|
|
31
31
|
for entity_id, entity in spec.entities.items()
|
|
32
32
|
for dataset in entity.datasets
|
|
33
33
|
}
|
|
34
|
-
self.calls = prepared.calls
|
|
35
|
-
self.functions = [function for function in prepared.functions if function.role == "spec"]
|
|
36
|
-
# Index all callables by (function_id, entity, feature) to resolve dependencies
|
|
37
|
-
self._callable_index: dict[tuple[str, str, str], callable_node_module.CallableNode] = {}
|
|
38
34
|
# Map feature names to the callables that produce them
|
|
39
35
|
self._feature_producers: dict[str, callable_node_module.CallableNode] = {}
|
|
40
36
|
self._dataset_sources: dict[tuple[str, str], list[callable_node_module.CallableNode]] = {}
|
|
@@ -53,14 +49,6 @@ class CallableGraph:
|
|
|
53
49
|
primary_feature = (
|
|
54
50
|
mapping.output_features[0] if mapping.output_features else None
|
|
55
51
|
)
|
|
56
|
-
call = next(
|
|
57
|
-
(
|
|
58
|
-
call
|
|
59
|
-
for call in self.calls
|
|
60
|
-
if call.entity == entity_id and call.feature in mapping.output_features
|
|
61
|
-
),
|
|
62
|
-
None,
|
|
63
|
-
)
|
|
64
52
|
node = callable_node_module.CallableNode(
|
|
65
53
|
function_id=mapping.binding.function,
|
|
66
54
|
entity=entity_id,
|
|
@@ -70,20 +58,18 @@ class CallableGraph:
|
|
|
70
58
|
mapping_id=mapping.id,
|
|
71
59
|
output_features=tuple(mapping.output_features),
|
|
72
60
|
args=mapping.binding.args,
|
|
73
|
-
|
|
61
|
+
input_features=tuple(mapping.input_features),
|
|
74
62
|
row_context=mapping.row_context,
|
|
75
63
|
)
|
|
76
64
|
nodes[node.node_id] = graphable_.graphable.Graphable(node)
|
|
77
65
|
if operation == "write":
|
|
78
66
|
self._dataset_sinks.setdefault(dataset_key, []).append(node)
|
|
79
|
-
elif operation
|
|
67
|
+
elif operation in contracts.FRAGMENT_OPERATIONS:
|
|
80
68
|
self._dataset_sources.setdefault(dataset_key, []).append(node)
|
|
81
69
|
for feature in mapping.output_features:
|
|
82
70
|
if feature in self.spec.dimensions:
|
|
83
71
|
continue
|
|
84
72
|
self._feature_producers[feature] = node
|
|
85
|
-
if operation == "vectorize":
|
|
86
|
-
self._callable_index[(node.function_id, entity_id, feature)] = node
|
|
87
73
|
graph = graphable_.graph.Graph(set(nodes.values()), discover=False)
|
|
88
74
|
# Source transforms follow reads in their dataset.
|
|
89
75
|
for fragments in self._dataset_sources.values():
|
|
@@ -129,7 +115,10 @@ class CallableGraph:
|
|
|
129
115
|
graph.add_edge(nodes[producer.node_id], graph_node)
|
|
130
116
|
|
|
131
117
|
# Vectorize mappings consume the assembled dataset frame.
|
|
132
|
-
|
|
118
|
+
calculation_nodes = [
|
|
119
|
+
node.reference for node in nodes.values() if node.reference.operation == "vectorize"
|
|
120
|
+
]
|
|
121
|
+
for callable_node in calculation_nodes:
|
|
133
122
|
consumer_node = nodes[callable_node.node_id]
|
|
134
123
|
dataset_key = (callable_node.entity, callable_node.dataset_id)
|
|
135
124
|
for source in self._dataset_sources.get(dataset_key, []):
|
|
@@ -137,13 +126,9 @@ class CallableGraph:
|
|
|
137
126
|
|
|
138
127
|
for dataset_key, sinks in self._dataset_sinks.items():
|
|
139
128
|
sources = self._dataset_sources.get(dataset_key, [])
|
|
140
|
-
calculations =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for node in self._callable_index.values()
|
|
144
|
-
if (node.entity, node.dataset_id) == dataset_key
|
|
145
|
-
)
|
|
146
|
-
)
|
|
129
|
+
calculations = [
|
|
130
|
+
node for node in calculation_nodes if (node.entity, node.dataset_id) == dataset_key
|
|
131
|
+
]
|
|
147
132
|
for sink in sinks:
|
|
148
133
|
sink_node = nodes[sink.node_id]
|
|
149
134
|
for source in sources:
|
|
@@ -220,8 +205,6 @@ class CallableGraph:
|
|
|
220
205
|
subset = self.__class__.__new__(self.__class__)
|
|
221
206
|
subset.spec = self.spec
|
|
222
207
|
subset._datasets = self._datasets
|
|
223
|
-
subset.functions = self.functions
|
|
224
|
-
subset._callable_index = self._callable_index
|
|
225
208
|
subset._feature_producers = self._feature_producers
|
|
226
209
|
subset._dataset_sources = self._dataset_sources
|
|
227
210
|
subset._dataset_sinks = self._dataset_sinks
|
|
@@ -5,13 +5,16 @@ import typing
|
|
|
5
5
|
FUNCTION_OPERATIONS = (
|
|
6
6
|
"read",
|
|
7
7
|
"write",
|
|
8
|
+
"escape",
|
|
8
9
|
"vectorize",
|
|
9
10
|
"collapse",
|
|
10
11
|
"expand",
|
|
11
12
|
"conform",
|
|
12
13
|
)
|
|
13
|
-
FunctionOperation = typing.Literal[
|
|
14
|
-
|
|
14
|
+
FunctionOperation = typing.Literal[
|
|
15
|
+
"read", "write", "escape", "vectorize", "collapse", "expand", "conform"
|
|
16
|
+
]
|
|
17
|
+
FunctionReturns = typing.Literal["dataframe", "none", "any"]
|
|
15
18
|
FRAGMENT_OPERATIONS: tuple[FunctionOperation, ...] = ("read", "conform", "expand", "collapse")
|
|
16
19
|
|
|
17
20
|
FEATURE_PREFIX = "$features."
|
|
@@ -22,17 +25,19 @@ ID_SELECTOR = ".id"
|
|
|
22
25
|
DATAFRAME_SELECTOR = ".dataframe"
|
|
23
26
|
STRUCTURAL_PREFIXES = (FEATURE_PREFIX, DATASET_PREFIX, DIMENSION_PREFIX)
|
|
24
27
|
|
|
25
|
-
DATAFRAME_INPUT_COUNTS: dict[FunctionOperation, int] = {
|
|
28
|
+
DATAFRAME_INPUT_COUNTS: dict[FunctionOperation, int | None] = {
|
|
26
29
|
"read": 0,
|
|
27
30
|
"collapse": 1,
|
|
28
31
|
"conform": 2,
|
|
29
32
|
"expand": 2,
|
|
30
33
|
"vectorize": 1,
|
|
31
34
|
"write": 1,
|
|
35
|
+
"escape": None,
|
|
32
36
|
}
|
|
33
37
|
OPERATION_RETURN_KINDS: dict[FunctionOperation, FunctionReturns] = {
|
|
34
38
|
"read": "dataframe",
|
|
35
39
|
"write": "none",
|
|
40
|
+
"escape": "any",
|
|
36
41
|
"vectorize": "dataframe",
|
|
37
42
|
"collapse": "dataframe",
|
|
38
43
|
"expand": "dataframe",
|
|
@@ -78,13 +78,18 @@ class Engine:
|
|
|
78
78
|
started = time.perf_counter()
|
|
79
79
|
logger.debug("Running %s callable %s", callable.operation, callable.id)
|
|
80
80
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self._execute_dataframe_mapping(callable, state, bindings, dataframe)
|
|
84
|
-
elif contracts.OPERATION_RETURN_KINDS.get(callable.operation) == "none":
|
|
85
|
-
self._execute_sink(callable, state, bindings)
|
|
81
|
+
if callable.operation == "escape":
|
|
82
|
+
self._execute_escape(callable, bindings)
|
|
86
83
|
else:
|
|
87
|
-
|
|
84
|
+
dataframe.validate_inputs(callable)
|
|
85
|
+
if contracts.OPERATION_RETURN_KINDS.get(callable.operation) == "dataframe":
|
|
86
|
+
self._execute_dataframe_mapping(callable, state, bindings, dataframe)
|
|
87
|
+
elif contracts.OPERATION_RETURN_KINDS.get(callable.operation) == "none":
|
|
88
|
+
self._execute_sink(callable, state, bindings)
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Unsupported callable operation: {callable.operation}"
|
|
92
|
+
)
|
|
88
93
|
except Exception as error:
|
|
89
94
|
statuses[callable.id] = "failed"
|
|
90
95
|
failures[callable.id] = str(error)
|
|
@@ -133,6 +138,15 @@ class Engine:
|
|
|
133
138
|
fragment, [*dimensions, output]
|
|
134
139
|
)
|
|
135
140
|
|
|
141
|
+
def _execute_escape(
|
|
142
|
+
self,
|
|
143
|
+
callable: plan_module.Callable,
|
|
144
|
+
bindings: bindings_module.RuntimeBindings,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Invoke an uncontrolled mapping and discard its return value."""
|
|
147
|
+
kwargs = bindings.resolve_args(callable)
|
|
148
|
+
self.callable_loader.load(callable.entrypoint)(**kwargs)
|
|
149
|
+
|
|
136
150
|
def _execute_sink(
|
|
137
151
|
self,
|
|
138
152
|
callable: plan_module.Callable,
|
|
@@ -18,6 +18,7 @@ class DataLineageColumn:
|
|
|
18
18
|
role: str
|
|
19
19
|
color: str
|
|
20
20
|
producer: int | None
|
|
21
|
+
external: bool = False
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -89,8 +90,14 @@ def _build_stages(
|
|
|
89
90
|
for name in dimensions
|
|
90
91
|
]
|
|
91
92
|
for role, features in (
|
|
92
|
-
(
|
|
93
|
-
|
|
93
|
+
(
|
|
94
|
+
"Declared input" if callable.operation == "escape" else "Input",
|
|
95
|
+
callable.input_features,
|
|
96
|
+
),
|
|
97
|
+
(
|
|
98
|
+
"Declared external output" if callable.operation == "escape" else "Output",
|
|
99
|
+
callable.output_features,
|
|
100
|
+
),
|
|
94
101
|
):
|
|
95
102
|
for feature in features:
|
|
96
103
|
if feature in dimensions:
|
|
@@ -107,6 +114,7 @@ def _build_stages(
|
|
|
107
114
|
if feature in spec_definition.dimensions
|
|
108
115
|
else "#cbd5e1",
|
|
109
116
|
numbers[producer.id] if producer else None,
|
|
117
|
+
producer is not None and producer.operation == "escape",
|
|
110
118
|
)
|
|
111
119
|
)
|
|
112
120
|
dataset = datasets.get(callable.dataset_id)
|
|
@@ -75,11 +75,14 @@ def _render_step(
|
|
|
75
75
|
"dataset": callable.dataset_id,
|
|
76
76
|
"feature": (
|
|
77
77
|
", ".join(callable.output_features)
|
|
78
|
-
if callable.operation
|
|
78
|
+
if callable.operation in {"collapse", "escape"}
|
|
79
79
|
else callable.output
|
|
80
80
|
if callable.operation == "vectorize"
|
|
81
81
|
else None
|
|
82
82
|
),
|
|
83
|
+
"input_features": callable.input_features,
|
|
84
|
+
"output_features": callable.output_features,
|
|
85
|
+
"uncontrolled": callable.operation == "escape",
|
|
83
86
|
"row_context": callable.row_context.to_dict() if callable.row_context else None,
|
|
84
87
|
"depends_on_callables": [
|
|
85
88
|
{
|
|
@@ -155,7 +158,15 @@ def _create_website_pages(
|
|
|
155
158
|
"function_id": callable.function_id,
|
|
156
159
|
"entity": callable.entity_id,
|
|
157
160
|
"dataset_id": callable.dataset_id,
|
|
158
|
-
"feature":
|
|
161
|
+
"feature": (
|
|
162
|
+
", ".join(callable.output_features)
|
|
163
|
+
if callable.operation == "escape"
|
|
164
|
+
else callable.output
|
|
165
|
+
if callable.operation == "vectorize"
|
|
166
|
+
else None
|
|
167
|
+
),
|
|
168
|
+
"input_features": callable.input_features,
|
|
169
|
+
"output_features": callable.output_features,
|
|
159
170
|
"dependency_ids": sorted(callable.dependency_ids),
|
|
160
171
|
}
|
|
161
172
|
for callable in sorted(plan.callables, key=lambda callable: callable.id)
|
|
@@ -9,9 +9,10 @@ import typing
|
|
|
9
9
|
from importlib import util
|
|
10
10
|
from urllib import parse
|
|
11
11
|
|
|
12
|
-
from uptimely import contracts, project, render
|
|
12
|
+
from uptimely import contracts, project, render
|
|
13
13
|
from uptimely import entrypoint as entrypoint_module
|
|
14
14
|
from uptimely.spec import model as spec_model
|
|
15
|
+
from uptimely.spec import references
|
|
15
16
|
|
|
16
17
|
SPEC_PAGES = render.WEBSITE_PAGES["spec"]
|
|
17
18
|
__all__ = [
|
|
@@ -117,12 +118,15 @@ def _create_website_pages(
|
|
|
117
118
|
)
|
|
118
119
|
|
|
119
120
|
|
|
120
|
-
def _feature_data(
|
|
121
|
+
def _feature_data(
|
|
122
|
+
configuration: dict[str, typing.Any],
|
|
123
|
+
) -> tuple[list[dict], list[dict], set[str], list[dict]]:
|
|
121
124
|
"""Collect mapped feature metadata and calculated feature references."""
|
|
122
125
|
entities = configuration.get("entities", {})
|
|
123
126
|
functions = configuration.get("functions", {})
|
|
124
127
|
fragment_features: list[dict] = []
|
|
125
128
|
calculated_features: list[dict] = []
|
|
129
|
+
escape_features: list[dict] = []
|
|
126
130
|
seen_inputs: set[tuple[str, str]] = set()
|
|
127
131
|
for entity in entities.values():
|
|
128
132
|
for dataset in entity.get("datasets", []):
|
|
@@ -142,6 +146,10 @@ def _feature_data(configuration: dict[str, typing.Any]) -> tuple[list[dict], lis
|
|
|
142
146
|
"inputs": mapping.get("features", {}).get("inputs", []),
|
|
143
147
|
}
|
|
144
148
|
)
|
|
149
|
+
elif operation == "escape":
|
|
150
|
+
if (dataset.get("id", ""), feature_id) not in seen_inputs:
|
|
151
|
+
escape_features.append(feature)
|
|
152
|
+
seen_inputs.add((dataset.get("id", ""), feature_id))
|
|
145
153
|
elif (dataset.get("id", ""), feature_id) not in seen_inputs:
|
|
146
154
|
fragment_features.append(feature)
|
|
147
155
|
seen_inputs.add((dataset.get("id", ""), feature_id))
|
|
@@ -153,7 +161,7 @@ def _feature_data(configuration: dict[str, typing.Any]) -> tuple[list[dict], lis
|
|
|
153
161
|
for reference in calculation.get("inputs", [])
|
|
154
162
|
)
|
|
155
163
|
}
|
|
156
|
-
return fragment_features, calculated_features, referenced_features
|
|
164
|
+
return fragment_features, calculated_features, referenced_features, escape_features
|
|
157
165
|
|
|
158
166
|
|
|
159
167
|
def _build_context(spec_definition: spec_model.Spec, include_generated_at: bool) -> ExplorerContext:
|
|
@@ -177,6 +185,23 @@ def _build_context(spec_definition: spec_model.Spec, include_generated_at: bool)
|
|
|
177
185
|
operation = function.get("operation")
|
|
178
186
|
if isinstance(operation, str):
|
|
179
187
|
function_groups.setdefault(operation, {})[function_id] = function
|
|
188
|
+
function["declared_returns"] = contracts.OPERATION_RETURN_KINDS[operation]
|
|
189
|
+
function["escape_mappings"] = []
|
|
190
|
+
for entity_id, dataset, mapping in references.iter_mappings(spec_definition):
|
|
191
|
+
function = function_definitions[mapping.binding.function]
|
|
192
|
+
if function.get("operation") == "escape":
|
|
193
|
+
function["escape_mappings"].append(
|
|
194
|
+
{
|
|
195
|
+
"entity": entity_id,
|
|
196
|
+
"dataset": dataset.id,
|
|
197
|
+
"mapping": mapping.id,
|
|
198
|
+
"inputs": mapping.input_features,
|
|
199
|
+
"outputs": mapping.output_features,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
for function in function_definitions.values():
|
|
203
|
+
if not isinstance(function, dict):
|
|
204
|
+
continue
|
|
180
205
|
entrypoint = function.get("entrypoint")
|
|
181
206
|
if not isinstance(entrypoint, str) or ":" not in entrypoint:
|
|
182
207
|
continue
|
|
@@ -202,7 +227,12 @@ def _build_context(spec_definition: spec_model.Spec, include_generated_at: bool)
|
|
|
202
227
|
return_type = None
|
|
203
228
|
if return_type:
|
|
204
229
|
function["return_type"] = return_type
|
|
205
|
-
|
|
230
|
+
(
|
|
231
|
+
fragment_features,
|
|
232
|
+
calculated_features,
|
|
233
|
+
referenced_features,
|
|
234
|
+
escape_features,
|
|
235
|
+
) = _feature_data(configuration)
|
|
206
236
|
feature_buckets = {
|
|
207
237
|
"input": len(fragment_features),
|
|
208
238
|
"intermediate": sum(
|
|
@@ -211,6 +241,7 @@ def _build_context(spec_definition: spec_model.Spec, include_generated_at: bool)
|
|
|
211
241
|
"output": sum(
|
|
212
242
|
calculation["id"] not in referenced_features for calculation in calculated_features
|
|
213
243
|
),
|
|
244
|
+
"external": len(escape_features),
|
|
214
245
|
}
|
|
215
246
|
function_buckets = {
|
|
216
247
|
operation: len(function_groups.get(operation, {}))
|
|
@@ -232,7 +263,7 @@ def _build_context(spec_definition: spec_model.Spec, include_generated_at: bool)
|
|
|
232
263
|
dataset_count=dataset_count,
|
|
233
264
|
mapping_count=mapping_count,
|
|
234
265
|
dimension_count=len(dimensions),
|
|
235
|
-
feature_count=len([*fragment_features, *calculated_features]),
|
|
266
|
+
feature_count=len([*fragment_features, *calculated_features, *escape_features]),
|
|
236
267
|
bound_feature_count=len(calculated_features),
|
|
237
268
|
function_count=sum(
|
|
238
269
|
len(group) for group in function_groups.values() if isinstance(group, dict)
|
|
@@ -243,11 +274,14 @@ def _build_context(spec_definition: spec_model.Spec, include_generated_at: bool)
|
|
|
243
274
|
)
|
|
244
275
|
dependencies = [
|
|
245
276
|
{
|
|
246
|
-
"function":
|
|
247
|
-
"
|
|
248
|
-
"
|
|
277
|
+
"function": mapping.binding.function,
|
|
278
|
+
"operation": spec_definition.functions[mapping.binding.function].operation,
|
|
279
|
+
"feature": feature,
|
|
280
|
+
"dependencies": mapping.input_features,
|
|
249
281
|
}
|
|
250
|
-
for
|
|
282
|
+
for _entity_id, _dataset, mapping in references.iter_mappings(spec_definition)
|
|
283
|
+
if spec_definition.functions[mapping.binding.function].operation in {"escape", "vectorize"}
|
|
284
|
+
for feature in mapping.output_features
|
|
251
285
|
]
|
|
252
286
|
generated_at = (
|
|
253
287
|
datetime.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
@@ -18,10 +18,11 @@ import typing
|
|
|
18
18
|
import fastmcp
|
|
19
19
|
import polars as pl
|
|
20
20
|
|
|
21
|
+
from uptimely import contracts, entrypoint, spec
|
|
21
22
|
from uptimely import engine as uptimely_engine
|
|
22
|
-
from uptimely import entrypoint, spec
|
|
23
23
|
from uptimely.project import build as project_build
|
|
24
24
|
from uptimely.spec import model as spec_model
|
|
25
|
+
from uptimely.spec import references
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
@@ -110,24 +111,31 @@ def create_server(state: ServerState) -> fastmcp.FastMCP:
|
|
|
110
111
|
|
|
111
112
|
@mcp.tool()
|
|
112
113
|
def list_functions() -> str:
|
|
113
|
-
"""List every function id and
|
|
114
|
+
"""List every function id, operation, and declared return contract."""
|
|
114
115
|
catalog = state.load_snapshot().spec.functions
|
|
115
116
|
return _to_json(
|
|
116
117
|
[
|
|
117
|
-
{
|
|
118
|
+
{
|
|
119
|
+
"id": function_id,
|
|
120
|
+
"operation": function.operation,
|
|
121
|
+
"returns": contracts.OPERATION_RETURN_KINDS[function.operation],
|
|
122
|
+
}
|
|
118
123
|
for function_id, function in sorted(catalog.root.items())
|
|
119
124
|
]
|
|
120
125
|
)
|
|
121
126
|
|
|
122
127
|
@mcp.tool()
|
|
123
128
|
def describe_function(function_id: str) -> str:
|
|
124
|
-
"""Describe
|
|
129
|
+
"""Describe a function's return contract, bindings, args, and Python signature.
|
|
130
|
+
|
|
131
|
+
Escape functions include declared feature boundaries and their uncontrolled
|
|
132
|
+
execution semantics.
|
|
125
133
|
|
|
126
134
|
Args:
|
|
127
135
|
function_id: The function id from `list_functions`.
|
|
128
136
|
"""
|
|
129
|
-
|
|
130
|
-
template =
|
|
137
|
+
snapshot = state.load_snapshot()
|
|
138
|
+
template = snapshot.spec.functions.get(function_id)
|
|
131
139
|
if template is None:
|
|
132
140
|
raise ValueError(f"Unknown function: {function_id}")
|
|
133
141
|
signature = entrypoint.read_signature(template.entrypoint, root=state.project_root)
|
|
@@ -135,6 +143,26 @@ def create_server(state: ServerState) -> fastmcp.FastMCP:
|
|
|
135
143
|
{
|
|
136
144
|
"id": function_id,
|
|
137
145
|
"operation": template.operation,
|
|
146
|
+
"declared_returns": contracts.OPERATION_RETURN_KINDS[template.operation],
|
|
147
|
+
"escape_mappings": [
|
|
148
|
+
{
|
|
149
|
+
"entity": entity_id,
|
|
150
|
+
"dataset": dataset.id,
|
|
151
|
+
"mapping": mapping_id,
|
|
152
|
+
"inputs": mapping.input_features,
|
|
153
|
+
"outputs": mapping.output_features,
|
|
154
|
+
}
|
|
155
|
+
for entity_id, dataset, mapping_id, mapping in references.iter_mapping_items(
|
|
156
|
+
snapshot.spec
|
|
157
|
+
)
|
|
158
|
+
if mapping.binding.function == function_id and template.operation == "escape"
|
|
159
|
+
],
|
|
160
|
+
"execution_semantics": (
|
|
161
|
+
"Uncontrolled external operation; its return value is ignored, and declared "
|
|
162
|
+
"outputs are not verified or materialized as dataframe columns."
|
|
163
|
+
if template.operation == "escape"
|
|
164
|
+
else None
|
|
165
|
+
),
|
|
138
166
|
"entrypoint": template.entrypoint,
|
|
139
167
|
"declared_args": {
|
|
140
168
|
name: {"type": argument.type, "comment": argument.comment}
|
|
@@ -183,23 +211,29 @@ def create_server(state: ServerState) -> fastmcp.FastMCP:
|
|
|
183
211
|
features needed to compute it.
|
|
184
212
|
"""
|
|
185
213
|
spec_definition = state.load_snapshot().spec
|
|
186
|
-
by_feature = {
|
|
187
|
-
|
|
188
|
-
|
|
214
|
+
by_feature = {
|
|
215
|
+
feature: (entity_id, mapping)
|
|
216
|
+
for entity_id, _dataset, mapping in references.iter_mappings(spec_definition)
|
|
217
|
+
if spec_definition.functions[mapping.binding.function].operation == "vectorize"
|
|
218
|
+
for feature in mapping.output_features
|
|
219
|
+
}
|
|
220
|
+
producer = by_feature.get(feature_id)
|
|
221
|
+
if producer is None:
|
|
189
222
|
raise ValueError(
|
|
190
223
|
f"Unknown calculated feature: {feature_id}. "
|
|
191
224
|
"Use list_calculated_features to see available features."
|
|
192
225
|
)
|
|
193
226
|
|
|
227
|
+
entity_id, mapping = producer
|
|
194
228
|
transitive: list[str] = []
|
|
195
|
-
queue = list(
|
|
229
|
+
queue = list(mapping.input_features)
|
|
196
230
|
seen = set(queue)
|
|
197
231
|
while queue:
|
|
198
232
|
current = queue.pop(0)
|
|
199
233
|
transitive.append(current)
|
|
200
|
-
|
|
201
|
-
if
|
|
202
|
-
for parent in
|
|
234
|
+
parent_producer = by_feature.get(current)
|
|
235
|
+
if parent_producer is not None:
|
|
236
|
+
for parent in parent_producer[1].input_features:
|
|
203
237
|
if parent not in seen:
|
|
204
238
|
seen.add(parent)
|
|
205
239
|
queue.append(parent)
|
|
@@ -207,9 +241,9 @@ def create_server(state: ServerState) -> fastmcp.FastMCP:
|
|
|
207
241
|
return _to_json(
|
|
208
242
|
{
|
|
209
243
|
"feature": feature_id,
|
|
210
|
-
"entity":
|
|
211
|
-
"function":
|
|
212
|
-
"direct_dependencies":
|
|
244
|
+
"entity": entity_id,
|
|
245
|
+
"function": mapping.binding.function,
|
|
246
|
+
"direct_dependencies": mapping.input_features,
|
|
213
247
|
"transitive_dependencies": transitive,
|
|
214
248
|
"input_features": [name for name in transitive if name not in by_feature],
|
|
215
249
|
"calculated_dependencies": [name for name in transitive if name in by_feature],
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from uptimely.row_context import RowContext
|
|
4
4
|
|
|
5
|
-
from .api import from_python, load_spec
|
|
5
|
+
from .api import from_python, load_spec
|
|
6
6
|
from .feature_graph import FeatureGraph, SubGraph
|
|
7
7
|
from .json.codec import write_document
|
|
8
8
|
from .model import (
|
|
@@ -29,18 +29,15 @@ from .python import (
|
|
|
29
29
|
ref,
|
|
30
30
|
row_context,
|
|
31
31
|
)
|
|
32
|
-
from .resolved import Call, Function, PreparedSpec
|
|
33
32
|
|
|
34
33
|
__all__ = [
|
|
35
34
|
"Argument",
|
|
36
35
|
"Binding",
|
|
37
|
-
"Call",
|
|
38
36
|
"Dataset",
|
|
39
37
|
"Dimension",
|
|
40
38
|
"Entity",
|
|
41
39
|
"Feature",
|
|
42
40
|
"FeatureGraph",
|
|
43
|
-
"Function",
|
|
44
41
|
"FunctionCatalog",
|
|
45
42
|
"Mapping",
|
|
46
43
|
"Order",
|
|
@@ -48,10 +45,8 @@ __all__ = [
|
|
|
48
45
|
"RowContext",
|
|
49
46
|
"RowScope",
|
|
50
47
|
"Spec",
|
|
51
|
-
"PreparedSpec",
|
|
52
48
|
"from_python",
|
|
53
49
|
"load_spec",
|
|
54
|
-
"prepare_spec",
|
|
55
50
|
"write_document",
|
|
56
51
|
"SubGraph",
|
|
57
52
|
"bind",
|