uptimely-core 0.1.0.dev0__tar.gz → 0.2.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 (154) hide show
  1. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/PKG-INFO +27 -10
  2. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/pyproject.toml +5 -5
  3. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/readme.md +24 -7
  4. uptimely_core-0.2.0/src/uptimely/__init__.py +40 -0
  5. uptimely_core-0.2.0/src/uptimely/cli/cli.py +38 -0
  6. uptimely_core-0.2.0/src/uptimely/cli/common.py +38 -0
  7. uptimely_core-0.2.0/src/uptimely/cli/engine.py +83 -0
  8. uptimely_core-0.1.0.dev0/src/uptimely/cli/cli.py → uptimely_core-0.2.0/src/uptimely/cli/examples.py +55 -43
  9. uptimely_core-0.2.0/src/uptimely/cli/explorer.py +92 -0
  10. uptimely_core-0.2.0/src/uptimely/cli/plan.py +18 -0
  11. uptimely_core-0.2.0/src/uptimely/cli/project.py +62 -0
  12. uptimely_core-0.2.0/src/uptimely/cli/spec.py +39 -0
  13. uptimely_core-0.2.0/src/uptimely/compiler/__init__.py +12 -0
  14. uptimely_core-0.2.0/src/uptimely/compiler/callable_node.py +54 -0
  15. uptimely_core-0.2.0/src/uptimely/compiler/compiler.py +201 -0
  16. uptimely_core-0.2.0/src/uptimely/compiler/execution_graph.py +264 -0
  17. uptimely_core-0.2.0/src/uptimely/compiler/plan/__init__.py +6 -0
  18. uptimely_core-0.2.0/src/uptimely/compiler/plan/models.py +187 -0
  19. uptimely_core-0.2.0/src/uptimely/compiler/plan/serialization.py +16 -0
  20. uptimely_core-0.2.0/src/uptimely/contracts.py +56 -0
  21. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/src/uptimely/engine/__init__.py +2 -2
  22. uptimely_core-0.2.0/src/uptimely/engine/backend.py +158 -0
  23. uptimely_core-0.2.0/src/uptimely/engine/bindings.py +80 -0
  24. uptimely_core-0.2.0/src/uptimely/engine/config.py +35 -0
  25. uptimely_core-0.2.0/src/uptimely/engine/dataframe.py +119 -0
  26. uptimely_core-0.2.0/src/uptimely/engine/engine.py +147 -0
  27. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/src/uptimely/engine/result.py +6 -7
  28. uptimely_core-0.2.0/src/uptimely/engine/state.py +33 -0
  29. uptimely_core-0.2.0/src/uptimely/entrypoint.py +172 -0
  30. uptimely_core-0.2.0/src/uptimely/explorer/explorer.py +120 -0
  31. uptimely_core-0.2.0/src/uptimely/explorer/graph_render.py +147 -0
  32. uptimely_core-0.2.0/src/uptimely/explorer/lineage.py +168 -0
  33. uptimely_core-0.2.0/src/uptimely/explorer/plan_render.py +237 -0
  34. uptimely_core-0.2.0/src/uptimely/explorer/spec_render.py +262 -0
  35. uptimely_core-0.2.0/src/uptimely/mcp/__init__.py +5 -0
  36. uptimely_core-0.2.0/src/uptimely/mcp/server.py +374 -0
  37. uptimely_core-0.2.0/src/uptimely/project/__init__.py +67 -0
  38. uptimely_core-0.2.0/src/uptimely/project/build.py +623 -0
  39. uptimely_core-0.2.0/src/uptimely/project/paths.py +27 -0
  40. uptimely_core-0.2.0/src/uptimely/render.py +32 -0
  41. uptimely_core-0.2.0/src/uptimely/row_context.py +216 -0
  42. uptimely_core-0.2.0/src/uptimely/spec/__init__.py +65 -0
  43. uptimely_core-0.2.0/src/uptimely/spec/api.py +82 -0
  44. uptimely_core-0.2.0/src/uptimely/spec/catalog.py +41 -0
  45. uptimely_core-0.2.0/src/uptimely/spec/feature_graph/__init__.py +14 -0
  46. uptimely_core-0.1.0.dev0/src/uptimely/analytics/feature_graph.py → uptimely_core-0.2.0/src/uptimely/spec/feature_graph/graph.py +135 -110
  47. {uptimely_core-0.1.0.dev0/src/uptimely/analytics → uptimely_core-0.2.0/src/uptimely/spec/feature_graph}/models.py +17 -23
  48. uptimely_core-0.2.0/src/uptimely/spec/json/__init__.py +1 -0
  49. uptimely_core-0.2.0/src/uptimely/spec/json/codec.py +41 -0
  50. uptimely_core-0.2.0/src/uptimely/spec/model/__init__.py +48 -0
  51. uptimely_core-0.2.0/src/uptimely/spec/model/common.py +47 -0
  52. uptimely_core-0.2.0/src/uptimely/spec/model/dataset.py +115 -0
  53. uptimely_core-0.2.0/src/uptimely/spec/model/functions.py +57 -0
  54. uptimely_core-0.2.0/src/uptimely/spec/model/spec.py +86 -0
  55. uptimely_core-0.2.0/src/uptimely/spec/python/__init__.py +7 -0
  56. uptimely_core-0.2.0/src/uptimely/spec/python/bind.py +121 -0
  57. uptimely_core-0.2.0/src/uptimely/spec/python/conversion.py +158 -0
  58. uptimely_core-0.2.0/src/uptimely/spec/python/dataset.py +140 -0
  59. {uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec → uptimely_core-0.2.0/src/uptimely/spec/python}/dimension.py +5 -5
  60. uptimely_core-0.2.0/src/uptimely/spec/python/entity.py +35 -0
  61. uptimely_core-0.2.0/src/uptimely/spec/python/feature.py +28 -0
  62. uptimely_core-0.2.0/src/uptimely/spec/python/mappings.py +87 -0
  63. uptimely_core-0.2.0/src/uptimely/spec/python/ref.py +139 -0
  64. uptimely_core-0.2.0/src/uptimely/spec/python/row_context.py +165 -0
  65. uptimely_core-0.2.0/src/uptimely/spec/python/spec.py +60 -0
  66. uptimely_core-0.2.0/src/uptimely/spec/references.py +145 -0
  67. uptimely_core-0.2.0/src/uptimely/spec/resolution.py +78 -0
  68. uptimely_core-0.2.0/src/uptimely/spec/resolved.py +60 -0
  69. uptimely_core-0.2.0/src/uptimely/spec/validation/__init__.py +20 -0
  70. uptimely_core-0.2.0/src/uptimely/spec/validation/context.py +174 -0
  71. uptimely_core-0.2.0/src/uptimely/spec/validation/dependencies.py +23 -0
  72. uptimely_core-0.2.0/src/uptimely/spec/validation/dimensions.py +111 -0
  73. uptimely_core-0.2.0/src/uptimely/spec/validation/errors.py +13 -0
  74. uptimely_core-0.2.0/src/uptimely/spec/validation/functions.py +150 -0
  75. uptimely_core-0.2.0/src/uptimely/spec/validation/mappings.py +434 -0
  76. uptimely_core-0.2.0/src/uptimely/spec/validation/prepared.py +155 -0
  77. uptimely_core-0.2.0/src/uptimely/spec/validation/spec.py +38 -0
  78. {uptimely_core-0.1.0.dev0/src/uptimely/spec → uptimely_core-0.2.0/src/uptimely}/storage.py +33 -16
  79. uptimely_core-0.2.0/src/uptimely/templates/base.html.j2 +104 -0
  80. uptimely_core-0.2.0/src/uptimely/templates/execution/data_lineage.html.j2 +403 -0
  81. uptimely_core-0.2.0/src/uptimely/templates/graph/base.html.j2 +62 -0
  82. uptimely_core-0.2.0/src/uptimely/templates/graph/feature_graph.html.j2 +50 -0
  83. uptimely_core-0.2.0/src/uptimely/templates/graph/feature_graph_shared.html.j2 +49 -0
  84. uptimely_core-0.2.0/src/uptimely/templates/graph/feature_renderer_2d.js.j2 +259 -0
  85. uptimely_core-0.2.0/src/uptimely/templates/graph/feature_renderer_3d.js.j2 +60 -0
  86. uptimely_core-0.2.0/src/uptimely/templates/graph/view_controller.js.j2 +56 -0
  87. uptimely_core-0.2.0/src/uptimely/templates/graph/view_controls.html.j2 +10 -0
  88. uptimely_core-0.2.0/src/uptimely/templates/icons.html.j2 +17 -0
  89. uptimely_core-0.2.0/src/uptimely/templates/index.html.j2 +153 -0
  90. {uptimely_core-0.1.0.dev0/src/uptimely/compile/templates → uptimely_core-0.2.0/src/uptimely/templates/plan}/callable_graph.html.j2 +42 -36
  91. uptimely_core-0.2.0/src/uptimely/templates/plan/callable_renderer_2d.js.j2 +124 -0
  92. uptimely_core-0.2.0/src/uptimely/templates/plan/graph.html.j2 +6 -0
  93. uptimely_core-0.2.0/src/uptimely/templates/plan/index.html.j2 +136 -0
  94. uptimely_core-0.2.0/src/uptimely/templates/plan/overview.html.j2 +3 -0
  95. uptimely_core-0.2.0/src/uptimely/templates/plan/stages.html.j2 +3 -0
  96. uptimely_core-0.2.0/src/uptimely/templates/spec/datasets.html.j2 +5 -0
  97. uptimely_core-0.2.0/src/uptimely/templates/spec/dimensions.html.j2 +5 -0
  98. uptimely_core-0.2.0/src/uptimely/templates/spec/entities.html.j2 +5 -0
  99. uptimely_core-0.2.0/src/uptimely/templates/spec/features.html.j2 +5 -0
  100. uptimely_core-0.2.0/src/uptimely/templates/spec/functions.html.j2 +5 -0
  101. uptimely_core-0.2.0/src/uptimely/templates/spec/index.html.j2 +26 -0
  102. uptimely_core-0.2.0/src/uptimely/templates/spec/overview.html.j2 +5 -0
  103. uptimely_core-0.2.0/src/uptimely/templates/spec/section_header.html.j2 +13 -0
  104. uptimely_core-0.2.0/src/uptimely/templates/spec/sections/dataset_list.html.j2 +124 -0
  105. uptimely_core-0.2.0/src/uptimely/templates/spec/sections/dimensions.html.j2 +17 -0
  106. uptimely_core-0.2.0/src/uptimely/templates/spec/sections/entities.html.j2 +80 -0
  107. uptimely_core-0.2.0/src/uptimely/templates/spec/sections/entity_list.html.j2 +39 -0
  108. uptimely_core-0.2.0/src/uptimely/templates/spec/sections/functions.html.j2 +64 -0
  109. uptimely_core-0.2.0/src/uptimely/templates/spec/sections/overview.html.j2 +42 -0
  110. uptimely_core-0.1.0.dev0/src/uptimely/__init__.py +0 -27
  111. uptimely_core-0.1.0.dev0/src/uptimely/analytics/__init__.py +0 -18
  112. uptimely_core-0.1.0.dev0/src/uptimely/analytics/render.py +0 -110
  113. uptimely_core-0.1.0.dev0/src/uptimely/analytics/templates/base.html.j2 +0 -31
  114. uptimely_core-0.1.0.dev0/src/uptimely/analytics/templates/graph.html.j2 +0 -118
  115. uptimely_core-0.1.0.dev0/src/uptimely/compile/__init__.py +0 -16
  116. uptimely_core-0.1.0.dev0/src/uptimely/compile/compiler.py +0 -161
  117. uptimely_core-0.1.0.dev0/src/uptimely/compile/entrypoint.py +0 -89
  118. uptimely_core-0.1.0.dev0/src/uptimely/compile/execution_graph.py +0 -272
  119. uptimely_core-0.1.0.dev0/src/uptimely/compile/node.py +0 -68
  120. uptimely_core-0.1.0.dev0/src/uptimely/compile/render.py +0 -85
  121. uptimely_core-0.1.0.dev0/src/uptimely/compile/templates/report.html.j2 +0 -108
  122. uptimely_core-0.1.0.dev0/src/uptimely/docs/docs.py +0 -248
  123. uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/docs.html.j2 +0 -42
  124. uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/dimensions.html.j2 +0 -4
  125. uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/entities.html.j2 +0 -172
  126. uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/functions.html.j2 +0 -48
  127. uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/overview.html.j2 +0 -18
  128. uptimely_core-0.1.0.dev0/src/uptimely/engine/backend.py +0 -127
  129. uptimely_core-0.1.0.dev0/src/uptimely/engine/engine.py +0 -424
  130. uptimely_core-0.1.0.dev0/src/uptimely/mcp/__init__.py +0 -5
  131. uptimely_core-0.1.0.dev0/src/uptimely/mcp/server.py +0 -321
  132. uptimely_core-0.1.0.dev0/src/uptimely/plan/__init__.py +0 -6
  133. uptimely_core-0.1.0.dev0/src/uptimely/plan/models.py +0 -275
  134. uptimely_core-0.1.0.dev0/src/uptimely/plan/serialization.py +0 -16
  135. uptimely_core-0.1.0.dev0/src/uptimely/spec/__init__.py +0 -50
  136. uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/__init__.py +0 -1
  137. uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/bindings.py +0 -95
  138. uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_functions.py +0 -353
  139. uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_model.py +0 -360
  140. uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_parser.py +0 -378
  141. uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_serializer.py +0 -32
  142. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/__init__.py +0 -1
  143. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/bind.py +0 -101
  144. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/dataset.py +0 -199
  145. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/entity.py +0 -53
  146. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/feature.py +0 -93
  147. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/fragment.py +0 -106
  148. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/ref.py +0 -63
  149. uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/spec.py +0 -76
  150. uptimely_core-0.1.0.dev0/src/uptimely/spec/validation.py +0 -323
  151. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/LICENSE +0 -0
  152. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/src/uptimely/cli/__init__.py +0 -0
  153. {uptimely_core-0.1.0.dev0/src/uptimely/docs → uptimely_core-0.2.0/src/uptimely/explorer}/__init__.py +0 -0
  154. {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0}/src/uptimely/py.typed +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uptimely-core
3
- Version: 0.1.0.dev0
4
- Summary: Declarative numerical analytics with complex dependencies at scale
3
+ Version: 0.2.0
4
+ Summary: Manage data-driven numerical analytics with complex dependencies at scale
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Keywords: analytics,declarative,industrial,feature-engineering,polars
@@ -24,15 +24,18 @@ Requires-Dist: numpy (>=1.26,<3.0)
24
24
  Requires-Dist: polars (>=1.0,<2.0)
25
25
  Requires-Dist: pydantic (>=2.10,<3.0)
26
26
  Project-URL: Documentation, https://uptimely-ai.github.io/uptimely-core
27
- Project-URL: Homepage, https://github.com/uptimely-ai/uptimely-core
27
+ Project-URL: Homepage, http://uptimely.ai/core/
28
28
  Project-URL: Repository, https://github.com/uptimely-ai/uptimely-core
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  # Uptimely Core
32
32
 
33
- Uptimely Core is an open source Python package for declarative numerical analytics with complex dependencies at scale.
33
+ Uptimely Core is an open source Python package for declarative numerical
34
+ analytics with complex dependencies at scale.
34
35
 
35
- It describes analytics workflows in a specification similar to OpenAPI, uses a declarative model inspired by Kubernetes. The framework generates plans, graphs, and documentation from that specification.
36
+ Analytics definitions describe workflows in JSON, using a declarative model
37
+ inspired by OpenAPI and Kubernetes. The compiler generates plans from the
38
+ analytics; the package can also render dependency graphs and documentation.
36
39
 
37
40
  Ships with simple engine and plans to integrate to common execution backends.
38
41
 
@@ -53,15 +56,29 @@ Read more at [uptimely.ai website](https://uptimely.ai).
53
56
 
54
57
  ## Quick start
55
58
 
56
- Install the package, download examples and run.
59
+ Python 3.13 or newer is required. Install the package, download the examples
60
+ and run.
61
+
62
+ With pip:
57
63
 
58
64
  ```sh
59
65
  pip install uptimely-core
60
- uptimely examples download
61
- python examples/run_all.py
66
+ uptimely examples download --output examples
67
+ uptimely project build --project-root examples/pump_analytics
68
+ uptimely engine run --project-root examples/pump_analytics
69
+ uptimely explorer serve --project-root examples/pump_analytics
70
+ ```
71
+
72
+ With Poetry, from your Poetry project:
73
+
74
+ ```sh
75
+ poetry add uptimely-core
76
+ poetry run uptimely examples download --output examples
77
+ poetry run uptimely project build --project-root examples/pump_analytics
78
+ poetry run uptimely engine run --project-root examples/pump_analytics
79
+ poetry run uptimely explorer serve --project-root examples/pump_analytics
62
80
  ```
63
81
 
64
- Explore the examples files, generated resources and modify for the needs of your own project.
65
82
 
66
83
  ## Documentation
67
84
 
@@ -74,7 +91,7 @@ Explore the [documentation site](https://uptimely-ai.github.io/uptimely-core/).
74
91
  | [`docs/`](docs/index.md) | Architecture, specification, and development documentation. |
75
92
  | [`examples/`](examples/README.md) | Runnable examples and generated artifacts. |
76
93
  | [`scripts/`](scripts) | Code snippets for repository management |
77
- | [`src/uptimely/`](src/uptimely/) | Package implementation for specifications, compilation, dependencies, documentation, and engine. |
94
+ | [`src/uptimely/`](src/uptimely/) | Package implementation for analytics, compilation, dependencies, documentation, and execution. |
78
95
  | [`tests/`](tests/) | Automated tests for the package and examples. |
79
96
 
80
97
  ## Development
@@ -4,8 +4,8 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "uptimely-core"
7
- version = "0.1.0.dev0"
8
- description = "Declarative numerical analytics with complex dependencies at scale"
7
+ version = "0.2.0"
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"
11
11
  license-files = ["LICENSE"]
@@ -39,12 +39,12 @@ uptimely = "uptimely.cli.cli:main"
39
39
  uptimely-mcp = "uptimely.mcp.server:main"
40
40
 
41
41
  [project.urls]
42
- Homepage = "https://github.com/uptimely-ai/uptimely-core"
42
+ Homepage = "http://uptimely.ai/core/"
43
43
  Repository = "https://github.com/uptimely-ai/uptimely-core"
44
44
  Documentation = "https://uptimely-ai.github.io/uptimely-core"
45
45
 
46
46
  [dependency-groups]
47
- test = ["pytest>=8.0", "pytest-cov>=5.0"]
47
+ test = ["pytest>=8.0", "pytest-cov>=5.0", "pytest-xdist (>=3.6,<4.0)"]
48
48
  dev = ["pre-commit>=3.0", "ruff>=0.8"]
49
49
  docs = ["mkdocs>=1.6,<2.0", "mkdocs-material>=9.7,<10.0", "mkdocstrings[python]>=1.0,<2.0"]
50
50
 
@@ -68,7 +68,7 @@ convention = "google"
68
68
 
69
69
  [tool.pytest.ini_options]
70
70
  testpaths = ["tests"]
71
- addopts = "--strict-markers --strict-config"
71
+ addopts = "--strict-markers --strict-config -n 4"
72
72
  markers = [
73
73
  "integration: end-to-end tests using example projects",
74
74
  ]
@@ -1,8 +1,11 @@
1
1
  # Uptimely Core
2
2
 
3
- Uptimely Core is an open source Python package for declarative numerical analytics with complex dependencies at scale.
3
+ Uptimely Core is an open source Python package for declarative numerical
4
+ analytics with complex dependencies at scale.
4
5
 
5
- It describes analytics workflows in a specification similar to OpenAPI, uses a declarative model inspired by Kubernetes. The framework generates plans, graphs, and documentation from that specification.
6
+ Analytics definitions describe workflows in JSON, using a declarative model
7
+ inspired by OpenAPI and Kubernetes. The compiler generates plans from the
8
+ analytics; the package can also render dependency graphs and documentation.
6
9
 
7
10
  Ships with simple engine and plans to integrate to common execution backends.
8
11
 
@@ -23,15 +26,29 @@ Read more at [uptimely.ai website](https://uptimely.ai).
23
26
 
24
27
  ## Quick start
25
28
 
26
- Install the package, download examples and run.
29
+ Python 3.13 or newer is required. Install the package, download the examples
30
+ and run.
31
+
32
+ With pip:
27
33
 
28
34
  ```sh
29
35
  pip install uptimely-core
30
- uptimely examples download
31
- python examples/run_all.py
36
+ uptimely examples download --output examples
37
+ uptimely project build --project-root examples/pump_analytics
38
+ uptimely engine run --project-root examples/pump_analytics
39
+ uptimely explorer serve --project-root examples/pump_analytics
40
+ ```
41
+
42
+ With Poetry, from your Poetry project:
43
+
44
+ ```sh
45
+ poetry add uptimely-core
46
+ poetry run uptimely examples download --output examples
47
+ poetry run uptimely project build --project-root examples/pump_analytics
48
+ poetry run uptimely engine run --project-root examples/pump_analytics
49
+ poetry run uptimely explorer serve --project-root examples/pump_analytics
32
50
  ```
33
51
 
34
- Explore the examples files, generated resources and modify for the needs of your own project.
35
52
 
36
53
  ## Documentation
37
54
 
@@ -44,7 +61,7 @@ Explore the [documentation site](https://uptimely-ai.github.io/uptimely-core/).
44
61
  | [`docs/`](docs/index.md) | Architecture, specification, and development documentation. |
45
62
  | [`examples/`](examples/README.md) | Runnable examples and generated artifacts. |
46
63
  | [`scripts/`](scripts) | Code snippets for repository management |
47
- | [`src/uptimely/`](src/uptimely/) | Package implementation for specifications, compilation, dependencies, documentation, and engine. |
64
+ | [`src/uptimely/`](src/uptimely/) | Package implementation for analytics, compilation, dependencies, documentation, and execution. |
48
65
  | [`tests/`](tests/) | Automated tests for the package and examples. |
49
66
 
50
67
  ## Development
@@ -0,0 +1,40 @@
1
+ """Public package namespace for uptimely."""
2
+
3
+ from .compiler import CallableGraph, CallableNode, Compiler, ExecutionPlan
4
+ from .engine import Engine
5
+ from .spec import (
6
+ Call,
7
+ Dimension,
8
+ Entity,
9
+ FeatureGraph,
10
+ Function,
11
+ PreparedSpec,
12
+ PythonSpec,
13
+ Spec,
14
+ SubGraph,
15
+ from_python,
16
+ load_spec,
17
+ prepare_spec,
18
+ write_document,
19
+ )
20
+
21
+ __all__ = [
22
+ "Spec",
23
+ "Call",
24
+ "CallableGraph",
25
+ "CallableNode",
26
+ "Compiler",
27
+ "Dimension",
28
+ "Entity",
29
+ "ExecutionPlan",
30
+ "Function",
31
+ "PythonSpec",
32
+ "PreparedSpec",
33
+ "from_python",
34
+ "load_spec",
35
+ "prepare_spec",
36
+ "write_document",
37
+ "SubGraph",
38
+ "FeatureGraph",
39
+ "Engine",
40
+ ]
@@ -0,0 +1,38 @@
1
+ """Entry point for the Uptimely command-line interface."""
2
+
3
+ import argparse
4
+
5
+ from uptimely.cli import engine, examples, explorer, project, spec
6
+
7
+ download_examples = examples.download_examples
8
+ serve_explorer = explorer.serve_explorer
9
+
10
+
11
+ def main() -> None:
12
+ """Run the Uptimely command-line interface."""
13
+ parser = argparse.ArgumentParser(prog="uptimely")
14
+ commands = parser.add_subparsers(dest="command", required=True)
15
+ examples.configure_parser(commands.add_parser("examples", help="Manage example projects"))
16
+ explorer.configure_parser(
17
+ commands.add_parser("explorer", help="Explore spec, lineage, and execution plans")
18
+ )
19
+ spec.configure_parser(
20
+ commands.add_parser("spec", help="Inspect specs and feature dependencies")
21
+ )
22
+ project.configure_parser(commands.add_parser("project", help="Build project resources"))
23
+ engine.configure_parser(commands.add_parser("engine", help="Run analytics workflows"))
24
+ args = parser.parse_args()
25
+ if args.command == "examples":
26
+ examples.run(args, parser)
27
+ elif args.command == "explorer":
28
+ explorer.run(args, parser)
29
+ elif args.command == "spec":
30
+ spec.run(args, parser)
31
+ elif args.command == "project":
32
+ project.run(args, parser)
33
+ else:
34
+ engine.run(args, parser)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,38 @@
1
+ """Shared project paths and spec loading for CLI commands."""
2
+
3
+ import argparse
4
+ import pathlib
5
+
6
+ from uptimely import spec
7
+ from uptimely.project.build import prepare_project_root
8
+
9
+
10
+ def configure_project_arguments(parser: argparse.ArgumentParser) -> None:
11
+ """Add spec, import-root, feature-selection, and diagnostic options."""
12
+ parser.add_argument(
13
+ "--spec",
14
+ type=pathlib.Path,
15
+ help="Spec JSON to inspect (default: spec from the active project build)",
16
+ )
17
+ parser.add_argument(
18
+ "--project-root",
19
+ type=pathlib.Path,
20
+ help="Root for importing function entrypoints and resolving default paths",
21
+ )
22
+ parser.add_argument(
23
+ "--features", nargs="+", help="Select output features and their dependencies"
24
+ )
25
+ parser.add_argument("--verbose", action="store_true", help="Print diagnostic details")
26
+
27
+
28
+ def project_path(
29
+ path: pathlib.Path | None, project_root: pathlib.Path | None, default: pathlib.Path
30
+ ) -> pathlib.Path:
31
+ """Resolve a default beneath the project root, preserving explicit paths."""
32
+ return path if path is not None else (project_root or pathlib.Path()) / default
33
+
34
+
35
+ def load_spec(path: pathlib.Path, project_root: pathlib.Path) -> spec.Spec:
36
+ """Load a spec document with project functions on the import path."""
37
+ prepare_project_root(project_root)
38
+ return spec.load_spec(path)
@@ -0,0 +1,83 @@
1
+ """Command-line tools for executing analytics workflows."""
2
+
3
+ import argparse
4
+ import pathlib
5
+ import typing
6
+
7
+ import pydantic
8
+
9
+ from uptimely import engine as engine_module
10
+ from uptimely import project
11
+ from uptimely.cli import common, plan
12
+
13
+
14
+ class EngineSettings(pydantic.BaseModel):
15
+ """Validated engine.json backend configuration and runtime bindings."""
16
+
17
+ model_config = pydantic.ConfigDict(extra="forbid")
18
+
19
+ backend: engine_module.EngineConfig = pydantic.Field(default_factory=engine_module.EngineConfig)
20
+ bind: dict[str, dict[str, dict[str, typing.Any]]] = pydantic.Field(default_factory=dict)
21
+
22
+
23
+ def configure_parser(parser: argparse.ArgumentParser) -> None:
24
+ """Configure the engine run action."""
25
+ actions = parser.add_subparsers(dest="action", required=True)
26
+ execute = actions.add_parser("run", help="Execute the active project's saved build")
27
+ execute.add_argument(
28
+ "--project-root",
29
+ type=pathlib.Path,
30
+ help="Project root containing the active build and function entrypoints",
31
+ )
32
+ execute.add_argument(
33
+ "--verbose", action="store_true", help="Print execution stages and dataset details"
34
+ )
35
+ execute.add_argument(
36
+ "--config",
37
+ type=pathlib.Path,
38
+ help=(
39
+ "Backend settings and runtime bindings "
40
+ f"(default: {project.ENGINE_CONFIG_FILE} under --project-root)"
41
+ ),
42
+ )
43
+
44
+
45
+ def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
46
+ """Execute the active saved plan and report failures explicitly."""
47
+ root = args.project_root or pathlib.Path.cwd()
48
+ config = common.project_path(args.config, args.project_root, project.ENGINE_CONFIG_FILE)
49
+ try:
50
+ common.prepare_project_root(root)
51
+ with project.read_build_bundle(root) as bundle:
52
+ settings = EngineSettings.model_validate_json(config.read_text(encoding="utf-8"))
53
+ execution_plan = bundle.execution_plan
54
+ result = engine_module.Engine(
55
+ execution_plan, config=settings.backend, bind=settings.bind
56
+ ).execute()
57
+ if result.failures:
58
+ failures = "; ".join(result.failures.values())
59
+ raise RuntimeError(f"Engine flow failed: {failures}")
60
+ except (OSError, ValueError, ImportError, AttributeError, TypeError, RuntimeError) as error:
61
+ parser.exit(1, f"uptimely: error: {error}\n")
62
+ calculated_features = sorted({feature_id for _, _, feature_id in result.calculated_outputs})
63
+ scope = (
64
+ "all features"
65
+ if bundle.manifest.features is None
66
+ else f"selected features: {', '.join(bundle.manifest.features)}"
67
+ )
68
+ print("Engine completed the flow")
69
+ print(f"Build scope: {scope}")
70
+ print(f"{len(execution_plan.callables)} callables")
71
+ print("Calculated features: " + ", ".join(calculated_features))
72
+ if args.verbose:
73
+ plan.print_stages(execution_plan)
74
+ print("\nDataset registry")
75
+ datasets = execution_plan.plan.resources.get("datasets", {})
76
+ for entity_id, dataset_id in dict.fromkeys(
77
+ (callable.entity_id, callable.dataset_id) for callable in execution_plan.callables
78
+ ):
79
+ dataset = datasets.get(dataset_id)
80
+ if dataset is None:
81
+ print(f"Dataset({entity_id}, {dataset_id}): feature metadata unavailable")
82
+ continue
83
+ print(f"Dataset({entity_id}, {dataset_id}): {len(dataset['features'])} features")
@@ -1,33 +1,36 @@
1
- """Command-line tools for working with Uptimely."""
1
+ """Command-line tools for downloading example projects."""
2
2
 
3
3
  import argparse
4
+ import pathlib
4
5
  import re
5
6
  import shutil
6
7
  import stat
7
- from http.client import HTTPException
8
- from importlib.metadata import PackageNotFoundError, metadata
9
- from pathlib import Path
10
- from tempfile import TemporaryDirectory, TemporaryFile
11
- from urllib.error import URLError
12
- from urllib.parse import quote, urlsplit
13
- from urllib.request import urlopen
14
- from zipfile import BadZipFile, ZipFile
8
+ import tempfile
9
+ import zipfile
10
+ from http import client
11
+ from importlib import metadata
12
+ from urllib import error as urllib_error
13
+ from urllib import parse, request
15
14
 
16
15
  EXAMPLES_FOLDER: str = "examples"
16
+ DISTRIBUTION_NAME: str = "uptimely-core"
17
+ RELEASE_TAG_PREFIX: str = "v"
18
+ EXAMPLES_DOWNLOAD_TIMEOUT: int = 30
19
+ TEMPORARY_DIRECTORY_PREFIX: str = ".uptimely-"
17
20
 
18
21
 
19
22
  def _repository_url() -> str:
20
23
  """Read the repository URL from the installed distribution metadata."""
21
- for entry in metadata("uptimely-core").get_all("Project-URL", []):
24
+ for entry in metadata.metadata(DISTRIBUTION_NAME).get_all("Project-URL", []):
22
25
  label, separator, url = entry.partition(",")
23
26
  if separator and label.strip().lower() == "repository":
24
27
  return url.strip()
25
- raise ValueError("Installed uptimely-core metadata has no Repository URL")
28
+ raise ValueError(f"Installed {DISTRIBUTION_NAME} metadata has no Repository URL")
26
29
 
27
30
 
28
31
  def _archive_url(repository: str, ref: str) -> str:
29
32
  """Build a GitHub archive URL for a repository and revision."""
30
- parsed = urlsplit(repository)
33
+ parsed = parse.urlsplit(repository)
31
34
  match = re.fullmatch(r"/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\.git)?/?", parsed.path)
32
35
  if (
33
36
  parsed.scheme != "https"
@@ -40,10 +43,10 @@ def _archive_url(repository: str, ref: str) -> str:
40
43
  if not ref.strip():
41
44
  raise ValueError("The Git revision must not be empty")
42
45
  owner, name = match.groups()
43
- return f"https://codeload.github.com/{owner}/{name}/zip/{quote(ref, safe='')}"
46
+ return f"https://codeload.github.com/{owner}/{name}/zip/{parse.quote(ref, safe='')}"
44
47
 
45
48
 
46
- def _extract_examples(archive: ZipFile, destination: Path) -> None:
49
+ def _extract_examples(archive: zipfile.ZipFile, destination: pathlib.Path) -> None:
47
50
  """Copy regular files under EXAMPLES_FOLDER relative to the repository root."""
48
51
  source_parts = EXAMPLES_FOLDER.split("/")
49
52
  prefix_length = 1 + len(source_parts)
@@ -73,12 +76,14 @@ def _extract_examples(archive: ZipFile, destination: Path) -> None:
73
76
  raise ValueError("The repository archive contains no example files")
74
77
 
75
78
 
76
- def download_examples(output: Path, ref: str = "HEAD") -> Path:
79
+ def download_examples(output: pathlib.Path, ref: str | None = None) -> pathlib.Path:
77
80
  """Download the public repository's examples into a new directory.
78
81
 
79
82
  Args:
80
83
  output: Destination for the contents of the repository's examples directory.
81
- ref: GitHub branch, tag, or commit. HEAD selects the default branch.
84
+ ref: GitHub branch, tag, or commit. If omitted, use the release tag
85
+ matching the installed package version (v<version>).
86
+ Explicit HEAD selects the default branch.
82
87
 
83
88
  Returns:
84
89
  Absolute path to the downloaded examples.
@@ -86,7 +91,8 @@ def download_examples(output: Path, ref: str = "HEAD") -> Path:
86
91
  Raises:
87
92
  FileExistsError: The destination already exists.
88
93
  PackageNotFoundError: The distribution metadata is not installed.
89
- ValueError: Repository metadata, revision, or archive entries are invalid.
94
+ ValueError: Repository metadata, revision, or archive entries are invalid,
95
+ or the matching release tag is unavailable.
90
96
  URLError: GitHub cannot be reached or rejects the download.
91
97
  BadZipFile: The downloaded archive is invalid.
92
98
  OSError: The files cannot be written.
@@ -94,16 +100,27 @@ def download_examples(output: Path, ref: str = "HEAD") -> Path:
94
100
  output = output.absolute()
95
101
  if output.exists() or output.is_symlink():
96
102
  raise FileExistsError(f"Destination already exists: {output}")
97
- url = _archive_url(_repository_url(), ref)
103
+ revision = f"{RELEASE_TAG_PREFIX}{metadata.version(DISTRIBUTION_NAME)}" if ref is None else ref
104
+ url = _archive_url(_repository_url(), revision)
98
105
  output.parent.mkdir(parents=True, exist_ok=True)
99
- with TemporaryDirectory(prefix=".uptimely-", dir=output.parent) as temporary:
100
- staging = Path(temporary) / "examples"
106
+ with tempfile.TemporaryDirectory(
107
+ prefix=TEMPORARY_DIRECTORY_PREFIX, dir=output.parent
108
+ ) as temporary:
109
+ staging = pathlib.Path(temporary) / "examples"
101
110
  staging.mkdir()
102
- with TemporaryFile() as downloaded:
103
- with urlopen(url, timeout=30) as response:
104
- shutil.copyfileobj(response, downloaded)
111
+ with tempfile.TemporaryFile() as downloaded:
112
+ try:
113
+ with request.urlopen(url, timeout=EXAMPLES_DOWNLOAD_TIMEOUT) as response:
114
+ shutil.copyfileobj(response, downloaded)
115
+ except urllib_error.HTTPError as error:
116
+ if error.code == 404 and ref is None:
117
+ raise ValueError(
118
+ f"Matching examples release tag {revision!r} is unavailable. "
119
+ "Use --ref to select a revision compatible with your installed package."
120
+ ) from error
121
+ raise
105
122
  downloaded.seek(0)
106
- with ZipFile(downloaded) as archive:
123
+ with zipfile.ZipFile(downloaded) as archive:
107
124
  _extract_examples(archive, staging)
108
125
  if output.exists() or output.is_symlink():
109
126
  raise FileExistsError(f"Destination already exists: {output}")
@@ -111,38 +128,33 @@ def download_examples(output: Path, ref: str = "HEAD") -> Path:
111
128
  return output
112
129
 
113
130
 
114
- def main() -> None:
115
- """Run the Uptimely command-line interface."""
116
- parser = argparse.ArgumentParser(prog="uptimely")
117
- commands = parser.add_subparsers(dest="command", required=True)
118
- examples = commands.add_parser("examples", help="Manage example projects")
119
- actions = examples.add_subparsers(dest="action", required=True)
131
+ def configure_parser(parser: argparse.ArgumentParser) -> None:
132
+ """Configure the examples command and its download action."""
133
+ actions = parser.add_subparsers(dest="action", required=True)
120
134
  download = actions.add_parser("download", help="Download examples from public GitHub")
121
135
  download.add_argument(
122
136
  "--output",
123
- type=Path,
124
- default=Path("examples"),
137
+ type=pathlib.Path,
138
+ default=pathlib.Path("examples"),
125
139
  help="Destination directory (default: examples)",
126
140
  )
127
141
  download.add_argument(
128
142
  "--ref",
129
- default="HEAD",
130
- help="Branch, tag, or commit (default: the repository's default branch)",
143
+ help="Branch, tag, or commit (default: v<installed package version>)",
131
144
  )
132
- args = parser.parse_args()
145
+
146
+
147
+ def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
148
+ """Download examples using parsed command-line arguments."""
133
149
  try:
134
150
  destination = download_examples(args.output, args.ref)
135
151
  except (
136
152
  OSError,
137
- URLError,
138
- HTTPException,
139
- BadZipFile,
153
+ urllib_error.URLError,
154
+ client.HTTPException,
155
+ zipfile.BadZipFile,
140
156
  ValueError,
141
- PackageNotFoundError,
157
+ metadata.PackageNotFoundError,
142
158
  ) as error:
143
159
  parser.exit(1, f"uptimely: error: {error}\n")
144
160
  print(f"Downloaded examples to {destination}")
145
-
146
-
147
- if __name__ == "__main__":
148
- main()
@@ -0,0 +1,92 @@
1
+ """Command-line tools for building and serving the analytics explorer."""
2
+
3
+ import argparse
4
+ import functools
5
+ import pathlib
6
+ from http import server as http_server
7
+
8
+ from uptimely import project
9
+
10
+
11
+ def serve_explorer(
12
+ directory: pathlib.Path,
13
+ host: str = "127.0.0.1",
14
+ port: int = 8001,
15
+ scope: str = "all features",
16
+ ) -> None:
17
+ """Serve a saved analytics explorer website for local preview.
18
+
19
+ Args:
20
+ directory: Saved website directory containing an index.html file.
21
+ host: Address to bind; defaults to localhost.
22
+ port: TCP port; defaults to 8001. Zero selects an available port.
23
+ scope: Human-readable scope of the saved execution plan.
24
+
25
+ Raises:
26
+ NotADirectoryError: The website directory does not exist or is not a directory.
27
+ FileNotFoundError: The website index is missing.
28
+ ValueError: The port is outside the valid TCP range.
29
+ OSError: The server cannot bind to the requested address.
30
+ """
31
+ directory = directory.resolve()
32
+ if not directory.is_dir():
33
+ raise NotADirectoryError(f"Website directory does not exist: {directory}")
34
+ index = directory / project.EXPLORER_INDEX_FILE.name
35
+ if not index.is_file():
36
+ raise FileNotFoundError(
37
+ f"Website index not found: {index}. Generate it with 'uptimely project build'."
38
+ )
39
+ if not 0 <= port <= 65535:
40
+ raise ValueError("Port must be between 0 and 65535.")
41
+ handler = functools.partial(http_server.SimpleHTTPRequestHandler, directory=str(directory))
42
+ with http_server.ThreadingHTTPServer((host, port), handler) as server:
43
+ print(
44
+ f"Serving analytics explorer from {directory} at "
45
+ f"http://{host}:{server.server_port}/ (build scope: {scope}; Ctrl+C to stop)",
46
+ flush=True,
47
+ )
48
+ try:
49
+ server.serve_forever()
50
+ except KeyboardInterrupt:
51
+ print("\nSpec explorer server stopped.")
52
+
53
+
54
+ def _port(value: str) -> int:
55
+ """Parse a valid TCP port for the CLI."""
56
+ try:
57
+ port = int(value)
58
+ except ValueError as error:
59
+ raise argparse.ArgumentTypeError("Port must be an integer.") from error
60
+ if not 0 <= port <= 65535:
61
+ raise argparse.ArgumentTypeError("Port must be between 0 and 65535.")
62
+ return port
63
+
64
+
65
+ def configure_parser(parser: argparse.ArgumentParser) -> None:
66
+ """Configure the Explorer serve action."""
67
+ actions = parser.add_subparsers(dest="action", required=True)
68
+ serve = actions.add_parser("serve", help="Serve the active build's saved website")
69
+ serve.add_argument(
70
+ "--project-root",
71
+ type=pathlib.Path,
72
+ help="Project root containing the active build",
73
+ )
74
+ serve.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
75
+ serve.add_argument(
76
+ "--port", type=_port, default=8001, help="TCP port (default: 8001; 0 selects a free port)"
77
+ )
78
+
79
+
80
+ def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
81
+ """Serve the active build's validated, saved Explorer website."""
82
+ project_root = (args.project_root or pathlib.Path.cwd()).resolve()
83
+ try:
84
+ with project.read_build_bundle(project_root) as bundle:
85
+ scope = (
86
+ "all features"
87
+ if bundle.manifest.features is None
88
+ else f"selected features: {', '.join(bundle.manifest.features)}"
89
+ )
90
+ serve_explorer(bundle.explorer_path, args.host, args.port, scope)
91
+ except (OSError, ValueError, RuntimeError) as error:
92
+ parser.exit(1, f"uptimely: error: {error}\n")
@@ -0,0 +1,18 @@
1
+ """Display execution plan stages in the CLI."""
2
+
3
+ from uptimely.compiler import plan as plan_module
4
+
5
+
6
+ def print_stages(plan: plan_module.ExecutionPlan) -> None:
7
+ """Print execution stages and their function dependencies."""
8
+ callables_by_id = {callable.id: callable for callable in plan.callables}
9
+ print("\nExecution stages")
10
+ for stage_number, stage in enumerate(plan.execution_stages, 1):
11
+ print(f"Stage {stage_number}")
12
+ for callable in stage:
13
+ dependencies = [
14
+ callables_by_id[dependency_id].function_id
15
+ for dependency_id in callable.dependency_ids
16
+ ]
17
+ suffix = f" <- [{', '.join(dependencies)}]" if dependencies else ""
18
+ print(f" {callable.function_id}({callable.entity_id}) -> {callable.output}{suffix}")