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.
Files changed (122) hide show
  1. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/PKG-INFO +1 -1
  2. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/pyproject.toml +1 -1
  3. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/__init__.py +0 -8
  4. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/callable_node.py +3 -3
  5. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/compiler.py +2 -0
  6. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/execution_graph.py +11 -28
  7. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/contracts.py +8 -3
  8. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/engine.py +20 -6
  9. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/graph_render.py +1 -0
  10. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/lineage.py +10 -2
  11. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/plan_render.py +13 -2
  12. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/spec_render.py +43 -9
  13. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/mcp/server.py +50 -16
  14. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/__init__.py +1 -6
  15. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/api.py +3 -23
  16. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/feature_graph/graph.py +33 -69
  17. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/feature_graph/models.py +20 -16
  18. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/references.py +63 -7
  19. uptimely_core-0.3.0/src/uptimely/spec/validation/__init__.py +20 -0
  20. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/validation/context.py +57 -76
  21. uptimely_core-0.3.0/src/uptimely/spec/validation/dataset.py +89 -0
  22. uptimely_core-0.3.0/src/uptimely/spec/validation/dependencies.py +99 -0
  23. uptimely_core-0.3.0/src/uptimely/spec/validation/dimensions.py +68 -0
  24. uptimely_core-0.3.0/src/uptimely/spec/validation/functions.py +147 -0
  25. uptimely_core-0.3.0/src/uptimely/spec/validation/mapping_contracts.py +221 -0
  26. uptimely_core-0.3.0/src/uptimely/spec/validation/mapping_inputs.py +288 -0
  27. uptimely_core-0.3.0/src/uptimely/spec/validation/mapping_outputs.py +223 -0
  28. uptimely_core-0.3.0/src/uptimely/spec/validation/spec.py +45 -0
  29. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/execution/data_lineage.html.j2 +13 -5
  30. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_graph_shared.html.j2 +4 -1
  31. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/callable_graph.html.j2 +5 -3
  32. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/index.html.j2 +5 -2
  33. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/dataset_list.html.j2 +12 -3
  34. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/entities.html.j2 +6 -2
  35. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/functions.html.j2 +18 -3
  36. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/overview.html.j2 +2 -2
  37. uptimely_core-0.2.0.dev1/src/uptimely/spec/resolution.py +0 -78
  38. uptimely_core-0.2.0.dev1/src/uptimely/spec/resolved.py +0 -60
  39. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/__init__.py +0 -20
  40. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/dependencies.py +0 -23
  41. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/dimensions.py +0 -111
  42. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/functions.py +0 -150
  43. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/mappings.py +0 -434
  44. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/prepared.py +0 -155
  45. uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/spec.py +0 -38
  46. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/LICENSE +0 -0
  47. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/readme.md +0 -0
  48. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/__init__.py +0 -0
  49. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/cli.py +0 -0
  50. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/common.py +0 -0
  51. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/engine.py +0 -0
  52. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/examples.py +0 -0
  53. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/explorer.py +0 -0
  54. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/plan.py +0 -0
  55. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/project.py +0 -0
  56. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/cli/spec.py +0 -0
  57. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/__init__.py +0 -0
  58. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/plan/__init__.py +0 -0
  59. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/plan/models.py +0 -0
  60. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/compiler/plan/serialization.py +0 -0
  61. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/__init__.py +0 -0
  62. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/backend.py +0 -0
  63. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/bindings.py +0 -0
  64. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/config.py +0 -0
  65. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/dataframe.py +0 -0
  66. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/result.py +0 -0
  67. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/engine/state.py +0 -0
  68. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/entrypoint.py +0 -0
  69. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/__init__.py +0 -0
  70. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/explorer/explorer.py +0 -0
  71. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/mcp/__init__.py +0 -0
  72. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/project/__init__.py +0 -0
  73. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/project/build.py +0 -0
  74. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/project/paths.py +0 -0
  75. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/py.typed +0 -0
  76. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/render.py +0 -0
  77. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/row_context.py +0 -0
  78. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/catalog.py +0 -0
  79. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/feature_graph/__init__.py +0 -0
  80. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/json/__init__.py +0 -0
  81. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/json/codec.py +0 -0
  82. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/__init__.py +0 -0
  83. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/common.py +0 -0
  84. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/dataset.py +0 -0
  85. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/functions.py +0 -0
  86. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/model/spec.py +0 -0
  87. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/__init__.py +0 -0
  88. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/bind.py +0 -0
  89. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/conversion.py +0 -0
  90. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/dataset.py +0 -0
  91. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/dimension.py +0 -0
  92. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/entity.py +0 -0
  93. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/feature.py +0 -0
  94. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/mappings.py +0 -0
  95. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/ref.py +0 -0
  96. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/row_context.py +0 -0
  97. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/python/spec.py +0 -0
  98. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/spec/validation/errors.py +0 -0
  99. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/storage.py +0 -0
  100. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/base.html.j2 +0 -0
  101. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/base.html.j2 +0 -0
  102. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_graph.html.j2 +0 -0
  103. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_renderer_2d.js.j2 +0 -0
  104. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/feature_renderer_3d.js.j2 +0 -0
  105. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/view_controller.js.j2 +0 -0
  106. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/graph/view_controls.html.j2 +0 -0
  107. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/icons.html.j2 +0 -0
  108. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/index.html.j2 +0 -0
  109. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/callable_renderer_2d.js.j2 +0 -0
  110. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/graph.html.j2 +0 -0
  111. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/overview.html.j2 +0 -0
  112. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/plan/stages.html.j2 +0 -0
  113. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/datasets.html.j2 +0 -0
  114. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/dimensions.html.j2 +0 -0
  115. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/entities.html.j2 +0 -0
  116. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/features.html.j2 +0 -0
  117. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/functions.html.j2 +0 -0
  118. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/index.html.j2 +0 -0
  119. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/overview.html.j2 +0 -0
  120. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/section_header.html.j2 +0 -0
  121. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/dimensions.html.j2 +0 -0
  122. {uptimely_core-0.2.0.dev1 → uptimely_core-0.3.0}/src/uptimely/templates/spec/sections/entity_list.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uptimely-core
3
- Version: 0.2.0.dev1
3
+ Version: 0.3.0
4
4
  Summary: Manage data-driven numerical analytics with complex dependencies at scale
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "uptimely-core"
7
- version = "0.2.0.dev1"
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, spec
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
- callable: spec.Call | None = dataclasses.field(default=None, compare=False, hash=False)
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.callable.depends_on_features if self.callable is not None else None
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 api as spec_api
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
- prepared = spec_api.prepare_spec(spec)
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
- callable=call,
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 != "vectorize":
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
- for callable_node in set(self._callable_index.values()):
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 = list(
141
- dict.fromkeys(
142
- node
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["read", "write", "vectorize", "collapse", "expand", "conform"]
14
- FunctionReturns = typing.Literal["dataframe", "none"]
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
- dataframe.validate_inputs(callable)
82
- if contracts.OPERATION_RETURN_KINDS.get(callable.operation) == "dataframe":
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
- raise ValueError(f"Unsupported callable operation: {callable.operation}")
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,
@@ -23,6 +23,7 @@ class GraphStyle:
23
23
  function_colors: dict[str, str] = dataclasses.field(
24
24
  default_factory=lambda: {
25
25
  "read": "#4c78a8",
26
+ "escape": "#a78bfa",
26
27
  "vectorize": "#f58518",
27
28
  "collapse": "#e45756",
28
29
  "conform": "#72b7b2",
@@ -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
- ("Input", callable.input_features),
93
- ("Output", callable.output_features),
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 == "collapse"
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": callable.output if callable.operation == "vectorize" else None,
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, spec
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(configuration: dict[str, typing.Any]) -> tuple[list[dict], list[dict], set[str]]:
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
- fragment_features, calculated_features, referenced_features = _feature_data(configuration)
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": call.function,
247
- "feature": call.feature,
248
- "dependencies": call.depends_on_features,
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 call in spec.prepare_spec(spec_definition).calls
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 its operation."""
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
- {"id": function_id, "operation": function.operation}
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 one catalog function: entrypoint, declared args, Python signature.
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
- catalog = state.load_snapshot().spec.functions
130
- template = catalog.get(function_id)
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 = {call.feature: call for call in spec.prepare_spec(spec_definition).calls}
187
- call = by_feature.get(feature_id)
188
- if call is None:
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(call.depends_on_features)
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
- parent_call = by_feature.get(current)
201
- if parent_call is not None:
202
- for parent in parent_call.depends_on_features:
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": call.entity,
211
- "function": call.function,
212
- "direct_dependencies": call.depends_on_features,
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, prepare_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",