uptimely-core 0.2.0.dev1__tar.gz → 0.4.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.4.0}/PKG-INFO +1 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/pyproject.toml +1 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/__init__.py +0 -8
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/common.py +2 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/engine.py +2 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/project.py +4 -4
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/callable_node.py +3 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/compiler.py +6 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/execution_graph.py +11 -28
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/contracts.py +8 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/backend.py +1 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/engine.py +20 -6
- uptimely_core-0.4.0/src/uptimely/entrypoint.py +210 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/explorer/graph_render.py +1 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/explorer/lineage.py +10 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/explorer/plan_render.py +13 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/explorer/spec_render.py +55 -16
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/mcp/server.py +65 -22
- uptimely_core-0.4.0/src/uptimely/project/build/__init__.py +32 -0
- uptimely_core-0.4.0/src/uptimely/project/build/builder.py +227 -0
- uptimely_core-0.4.0/src/uptimely/project/build/bundle.py +178 -0
- uptimely_core-0.4.0/src/uptimely/project/build/errors.py +50 -0
- uptimely_core-0.4.0/src/uptimely/project/build/features.py +23 -0
- uptimely_core-0.4.0/src/uptimely/project/build/locking.py +67 -0
- uptimely_core-0.4.0/src/uptimely/project/build/models.py +130 -0
- uptimely_core-0.4.0/src/uptimely/project/build/source.py +123 -0
- uptimely_core-0.4.0/src/uptimely/project/imports.py +20 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/__init__.py +1 -6
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/api.py +3 -23
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/catalog.py +3 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/feature_graph/graph.py +33 -69
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/feature_graph/models.py +20 -16
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/model/functions.py +4 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/dataset.py +1 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/ref.py +14 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/references.py +63 -7
- uptimely_core-0.4.0/src/uptimely/spec/validation/__init__.py +19 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/bindings.py +49 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/collapse_inputs.py +82 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/context.py +655 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/dataset.py +58 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/dependencies.py +41 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/dimensions.py +40 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/functions.py +42 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/mapping_contracts.py +197 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/mapping_inputs.py +76 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/mapping_outputs.py +139 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/row_context.py +50 -0
- uptimely_core-0.4.0/src/uptimely/spec/validation/spec.py +171 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/execution/data_lineage.html.j2 +13 -5
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/feature_graph_shared.html.j2 +4 -1
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/plan/callable_graph.html.j2 +5 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/plan/index.html.j2 +5 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/sections/dataset_list.html.j2 +12 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/sections/entities.html.j2 +6 -2
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/sections/functions.html.j2 +31 -3
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/sections/overview.html.j2 +2 -2
- uptimely_core-0.2.0.dev1/src/uptimely/entrypoint.py +0 -172
- uptimely_core-0.2.0.dev1/src/uptimely/project/build.py +0 -623
- 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/context.py +0 -174
- 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.4.0}/LICENSE +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/readme.md +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/cli.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/examples.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/explorer.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/plan.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/cli/spec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/plan/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/plan/models.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/compiler/plan/serialization.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/bindings.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/config.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/dataframe.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/result.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/engine/state.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/explorer/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/explorer/explorer.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/mcp/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/project/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/project/paths.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/py.typed +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/render.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/row_context.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/feature_graph/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/json/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/json/codec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/model/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/model/common.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/model/dataset.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/model/spec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/__init__.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/bind.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/conversion.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/dimension.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/entity.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/feature.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/mappings.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/row_context.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/python/spec.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/spec/validation/errors.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/storage.py +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/base.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/base.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/feature_graph.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/feature_renderer_2d.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/feature_renderer_3d.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/view_controller.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/graph/view_controls.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/icons.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/index.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/plan/callable_renderer_2d.js.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/plan/graph.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/plan/overview.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/plan/stages.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/datasets.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/dimensions.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/entities.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/features.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/functions.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/index.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/overview.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/section_header.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.0}/src/uptimely/templates/spec/sections/dimensions.html.j2 +0 -0
- {uptimely_core-0.2.0.dev1 → uptimely_core-0.4.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.4.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",
|
|
@@ -4,7 +4,7 @@ import argparse
|
|
|
4
4
|
import pathlib
|
|
5
5
|
|
|
6
6
|
from uptimely import spec
|
|
7
|
-
from uptimely.project
|
|
7
|
+
from uptimely.project import imports
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def configure_project_arguments(parser: argparse.ArgumentParser) -> None:
|
|
@@ -34,5 +34,5 @@ def project_path(
|
|
|
34
34
|
|
|
35
35
|
def load_spec(path: pathlib.Path, project_root: pathlib.Path) -> spec.Spec:
|
|
36
36
|
"""Load a spec document with project functions on the import path."""
|
|
37
|
-
prepare_project_root(project_root)
|
|
37
|
+
imports.prepare_project_root(project_root)
|
|
38
38
|
return spec.load_spec(path)
|
|
@@ -9,6 +9,7 @@ import pydantic
|
|
|
9
9
|
from uptimely import engine as engine_module
|
|
10
10
|
from uptimely import project
|
|
11
11
|
from uptimely.cli import common, plan
|
|
12
|
+
from uptimely.project import imports
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class EngineSettings(pydantic.BaseModel):
|
|
@@ -47,7 +48,7 @@ def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
|
|
|
47
48
|
root = args.project_root or pathlib.Path.cwd()
|
|
48
49
|
config = common.project_path(args.config, args.project_root, project.ENGINE_CONFIG_FILE)
|
|
49
50
|
try:
|
|
50
|
-
|
|
51
|
+
imports.prepare_project_root(root)
|
|
51
52
|
with project.read_build_bundle(root) as bundle:
|
|
52
53
|
settings = EngineSettings.model_validate_json(config.read_text(encoding="utf-8"))
|
|
53
54
|
execution_plan = bundle.execution_plan
|
|
@@ -15,12 +15,12 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
15
15
|
)
|
|
16
16
|
source = build.add_mutually_exclusive_group()
|
|
17
17
|
source.add_argument(
|
|
18
|
-
"--
|
|
18
|
+
"--python-spec",
|
|
19
19
|
metavar="MODULE:ATTRIBUTE",
|
|
20
20
|
help="Importable module and attribute containing a PythonSpec instance",
|
|
21
21
|
)
|
|
22
22
|
source.add_argument(
|
|
23
|
-
"--spec",
|
|
23
|
+
"--json-spec",
|
|
24
24
|
type=pathlib.Path,
|
|
25
25
|
help="Source spec JSON path (overrides standard source discovery)",
|
|
26
26
|
)
|
|
@@ -43,8 +43,8 @@ def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
|
|
|
43
43
|
try:
|
|
44
44
|
bundle = project_build.build_project(
|
|
45
45
|
root,
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
python_spec=args.python_spec,
|
|
47
|
+
json_spec=args.json_spec,
|
|
48
48
|
features=args.features,
|
|
49
49
|
)
|
|
50
50
|
except (
|
|
@@ -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
|
)
|
|
@@ -167,7 +167,10 @@ class Compiler:
|
|
|
167
167
|
if function is None or not function.entrypoint:
|
|
168
168
|
continue
|
|
169
169
|
try:
|
|
170
|
-
|
|
170
|
+
function_entrypoint = entrypoint.Entrypoint(function.entrypoint, pathlib.Path.cwd())
|
|
171
|
+
if not function.validate_signature:
|
|
172
|
+
continue
|
|
173
|
+
signature = function_entrypoint.function.signature
|
|
171
174
|
except (ImportError, AttributeError, ValueError, TypeError) as error:
|
|
172
175
|
raise ValueError(
|
|
173
176
|
f"Could not read signature of function {function.id} "
|
|
@@ -183,6 +186,8 @@ class Compiler:
|
|
|
183
186
|
) -> None:
|
|
184
187
|
"""Require the Python return annotation to satisfy the function contract."""
|
|
185
188
|
returns = function.returns
|
|
189
|
+
if returns == "any":
|
|
190
|
+
return
|
|
186
191
|
if annotation is None:
|
|
187
192
|
raise ValueError(
|
|
188
193
|
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",
|
|
@@ -155,4 +155,4 @@ class EntrypointLoader:
|
|
|
155
155
|
"""Default callable loader using the package entrypoint implementation."""
|
|
156
156
|
|
|
157
157
|
def load(self, entrypoint: str) -> abc.Callable[..., typing.Any]:
|
|
158
|
-
return entrypoint_module.
|
|
158
|
+
return entrypoint_module.Entrypoint(entrypoint, pathlib.Path.cwd()).function.load()
|
|
@@ -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,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Load Python function entrypoints and read their annotated signatures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib
|
|
8
|
+
import inspect
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
import types
|
|
12
|
+
import typing
|
|
13
|
+
from importlib import abc, util
|
|
14
|
+
from urllib import parse, request
|
|
15
|
+
|
|
16
|
+
from uptimely import storage as storage_module
|
|
17
|
+
|
|
18
|
+
REMOTE_SCHEMES = {"https", "s3"}
|
|
19
|
+
REMOTE_READ_TIMEOUT = 30
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _RemoteModuleLoader(abc.SourceLoader):
|
|
23
|
+
"""Load one Python source file from a supported remote location."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, location: str) -> None:
|
|
26
|
+
self.location = location
|
|
27
|
+
|
|
28
|
+
def get_filename(self, _fullname: str) -> str:
|
|
29
|
+
return self.location
|
|
30
|
+
|
|
31
|
+
def get_data(self, _path: str) -> bytes:
|
|
32
|
+
if parse.urlsplit(self.location).scheme == "s3":
|
|
33
|
+
return storage_module.Storage.read(self.location).encode("utf-8")
|
|
34
|
+
with request.urlopen(self.location, timeout=REMOTE_READ_TIMEOUT) as response:
|
|
35
|
+
if parse.urlsplit(response.geturl()).scheme != "https":
|
|
36
|
+
raise ValueError("HTTPS function references must not redirect to another scheme")
|
|
37
|
+
return response.read()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Module:
|
|
41
|
+
"""Represent and load a Python module from a file, import path, or URL."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, location: str, root: pathlib.Path | None = None) -> None:
|
|
44
|
+
self.location = location
|
|
45
|
+
self.root = root
|
|
46
|
+
|
|
47
|
+
def load(self) -> types.ModuleType:
|
|
48
|
+
"""Import the represented Python module."""
|
|
49
|
+
if self.root is not None and str(self.root) not in sys.path:
|
|
50
|
+
sys.path.insert(0, str(self.root))
|
|
51
|
+
|
|
52
|
+
scheme = parse.urlsplit(self.location).scheme
|
|
53
|
+
if scheme in REMOTE_SCHEMES:
|
|
54
|
+
return self.load_external()
|
|
55
|
+
|
|
56
|
+
path = pathlib.Path(self.location)
|
|
57
|
+
if path.is_absolute() or self.location.startswith(("./", "../")):
|
|
58
|
+
path = path if path.is_absolute() else (self.root or pathlib.Path.cwd()) / path
|
|
59
|
+
return self._load_module(str(path.resolve()))
|
|
60
|
+
|
|
61
|
+
module_path = self.location[:-3] if self.location.endswith(".py") else self.location
|
|
62
|
+
return importlib.import_module(module_path.replace("/", "."))
|
|
63
|
+
|
|
64
|
+
def load_external(self) -> types.ModuleType:
|
|
65
|
+
"""Load the represented source file from HTTPS or S3."""
|
|
66
|
+
location = parse.urlsplit(self.location)
|
|
67
|
+
if location.scheme not in REMOTE_SCHEMES:
|
|
68
|
+
raise ValueError(f"Unsupported external module scheme: {location.scheme}")
|
|
69
|
+
if not location.netloc or not location.path.strip("/"):
|
|
70
|
+
raise ValueError(f"Function reference must point to a source file: {self.location}")
|
|
71
|
+
return self._load_module(self.location, _RemoteModuleLoader(self.location))
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _load_module(location: str, loader: abc.Loader | None = None) -> types.ModuleType:
|
|
75
|
+
"""Load a module once, removing failed imports from the cache."""
|
|
76
|
+
module_name = f"_uptimely_function_{hashlib.sha256(location.encode()).hexdigest()}"
|
|
77
|
+
if module_name in sys.modules:
|
|
78
|
+
return sys.modules[module_name]
|
|
79
|
+
module_spec = (
|
|
80
|
+
util.spec_from_loader(module_name, loader, origin=location)
|
|
81
|
+
if loader is not None
|
|
82
|
+
else util.spec_from_file_location(module_name, location)
|
|
83
|
+
)
|
|
84
|
+
if module_spec is None or module_spec.loader is None:
|
|
85
|
+
raise ImportError(f"Cannot load Python module from {location}")
|
|
86
|
+
module = util.module_from_spec(module_spec)
|
|
87
|
+
module.__file__ = location
|
|
88
|
+
sys.modules[module_name] = module
|
|
89
|
+
try:
|
|
90
|
+
module_spec.loader.exec_module(module)
|
|
91
|
+
except BaseException:
|
|
92
|
+
del sys.modules[module_name]
|
|
93
|
+
raise
|
|
94
|
+
return module
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TypeAnnotation:
|
|
98
|
+
"""Format Python type annotations for display."""
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def name(annotation: object) -> str:
|
|
102
|
+
"""Return a readable name for a type annotation."""
|
|
103
|
+
if annotation is None or annotation is type(None):
|
|
104
|
+
return "None"
|
|
105
|
+
|
|
106
|
+
origin = typing.get_origin(annotation)
|
|
107
|
+
if origin in (typing.Union, types.UnionType):
|
|
108
|
+
return " | ".join(TypeAnnotation.name(arg) for arg in typing.get_args(annotation))
|
|
109
|
+
|
|
110
|
+
if origin is not None:
|
|
111
|
+
arguments = ", ".join(TypeAnnotation.name(arg) for arg in typing.get_args(annotation))
|
|
112
|
+
return (
|
|
113
|
+
f"{TypeAnnotation.name(origin)}[{arguments}]"
|
|
114
|
+
if arguments
|
|
115
|
+
else TypeAnnotation.name(origin)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return getattr(annotation, "__name__", str(annotation))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclasses.dataclass
|
|
122
|
+
class Signature:
|
|
123
|
+
"""Annotated input and output types of a Python function."""
|
|
124
|
+
|
|
125
|
+
args: dict[str, str] = dataclasses.field(default_factory=dict)
|
|
126
|
+
returns: str | None = None
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def read(cls, function: Function) -> Signature:
|
|
130
|
+
"""Read the annotated argument and return types of a function."""
|
|
131
|
+
signature = inspect.signature(function.load(), eval_str=True)
|
|
132
|
+
return cls(
|
|
133
|
+
args={
|
|
134
|
+
name: TypeAnnotation.name(parameter.annotation)
|
|
135
|
+
for name, parameter in signature.parameters.items()
|
|
136
|
+
if parameter.annotation is not inspect.Parameter.empty
|
|
137
|
+
},
|
|
138
|
+
returns=(
|
|
139
|
+
TypeAnnotation.name(signature.return_annotation)
|
|
140
|
+
if signature.return_annotation is not inspect.Signature.empty
|
|
141
|
+
else None
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Function:
|
|
147
|
+
"""Represent a named callable in a Python module."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, module: Module, name: str) -> None:
|
|
150
|
+
self.module = module
|
|
151
|
+
self.name = name
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def signature(self) -> Signature:
|
|
155
|
+
"""Return the annotated signature of this function."""
|
|
156
|
+
return Signature.read(self)
|
|
157
|
+
|
|
158
|
+
def load(self) -> typing.Callable[..., typing.Any]:
|
|
159
|
+
"""Import the callable from its module."""
|
|
160
|
+
module = self.module.load()
|
|
161
|
+
function = getattr(module, self.name)
|
|
162
|
+
if not callable(function):
|
|
163
|
+
raise TypeError(f"{self.name} in module {self.module.location} is not callable")
|
|
164
|
+
return function
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Entrypoint:
|
|
168
|
+
"""Parse and expose the module and function addressed by an entrypoint."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, entrypoint: str, root: pathlib.Path | None = None) -> None:
|
|
171
|
+
if ":" not in entrypoint:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Invalid entrypoint: {entrypoint}. "
|
|
174
|
+
"Expected 'module.path:function' or 'path/to/module.py:function'"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
module_path, function_name = entrypoint.rsplit(":", 1)
|
|
178
|
+
scheme = parse.urlsplit(module_path).scheme
|
|
179
|
+
if not module_path.strip() or not function_name.isidentifier():
|
|
180
|
+
raise ValueError(f"Invalid function entrypoint: {entrypoint}")
|
|
181
|
+
if (scheme and scheme not in REMOTE_SCHEMES) or module_path.startswith("git@"):
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Unsupported function reference scheme: {module_path}. "
|
|
184
|
+
"Use a Python module, local file, https:// URL, or s3:// URI."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self.root = root
|
|
188
|
+
self._module_path = module_path
|
|
189
|
+
self._function_name = function_name
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def module(self) -> Module:
|
|
193
|
+
"""Return the module component of this entrypoint."""
|
|
194
|
+
return Module(self._module_path, self.root)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def function(self) -> Function:
|
|
198
|
+
"""Return the function component of this entrypoint."""
|
|
199
|
+
return Function(self.module, self._function_name)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"Entrypoint",
|
|
204
|
+
"Function",
|
|
205
|
+
"Module",
|
|
206
|
+
"REMOTE_READ_TIMEOUT",
|
|
207
|
+
"REMOTE_SCHEMES",
|
|
208
|
+
"Signature",
|
|
209
|
+
"TypeAnnotation",
|
|
210
|
+
]
|
|
@@ -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)
|