uptimely-core 0.1.0.dev0__tar.gz → 0.2.0.dev1__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.1.0.dev0 → uptimely_core-0.2.0.dev1}/PKG-INFO +27 -10
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/pyproject.toml +5 -5
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/readme.md +24 -7
- uptimely_core-0.2.0.dev1/src/uptimely/__init__.py +40 -0
- uptimely_core-0.2.0.dev1/src/uptimely/cli/cli.py +38 -0
- uptimely_core-0.2.0.dev1/src/uptimely/cli/common.py +38 -0
- uptimely_core-0.2.0.dev1/src/uptimely/cli/engine.py +83 -0
- uptimely_core-0.1.0.dev0/src/uptimely/cli/cli.py → uptimely_core-0.2.0.dev1/src/uptimely/cli/examples.py +55 -43
- uptimely_core-0.2.0.dev1/src/uptimely/cli/explorer.py +92 -0
- uptimely_core-0.2.0.dev1/src/uptimely/cli/plan.py +18 -0
- uptimely_core-0.2.0.dev1/src/uptimely/cli/project.py +62 -0
- uptimely_core-0.2.0.dev1/src/uptimely/cli/spec.py +39 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/__init__.py +12 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/callable_node.py +54 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/compiler.py +201 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/execution_graph.py +264 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/plan/__init__.py +6 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/plan/models.py +187 -0
- uptimely_core-0.2.0.dev1/src/uptimely/compiler/plan/serialization.py +16 -0
- uptimely_core-0.2.0.dev1/src/uptimely/contracts.py +56 -0
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/src/uptimely/engine/__init__.py +2 -2
- uptimely_core-0.2.0.dev1/src/uptimely/engine/backend.py +158 -0
- uptimely_core-0.2.0.dev1/src/uptimely/engine/bindings.py +80 -0
- uptimely_core-0.2.0.dev1/src/uptimely/engine/config.py +35 -0
- uptimely_core-0.2.0.dev1/src/uptimely/engine/dataframe.py +119 -0
- uptimely_core-0.2.0.dev1/src/uptimely/engine/engine.py +147 -0
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/src/uptimely/engine/result.py +6 -7
- uptimely_core-0.2.0.dev1/src/uptimely/engine/state.py +33 -0
- uptimely_core-0.2.0.dev1/src/uptimely/entrypoint.py +172 -0
- uptimely_core-0.2.0.dev1/src/uptimely/explorer/explorer.py +120 -0
- uptimely_core-0.2.0.dev1/src/uptimely/explorer/graph_render.py +147 -0
- uptimely_core-0.2.0.dev1/src/uptimely/explorer/lineage.py +168 -0
- uptimely_core-0.2.0.dev1/src/uptimely/explorer/plan_render.py +237 -0
- uptimely_core-0.2.0.dev1/src/uptimely/explorer/spec_render.py +262 -0
- uptimely_core-0.2.0.dev1/src/uptimely/mcp/__init__.py +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/mcp/server.py +374 -0
- uptimely_core-0.2.0.dev1/src/uptimely/project/__init__.py +67 -0
- uptimely_core-0.2.0.dev1/src/uptimely/project/build.py +623 -0
- uptimely_core-0.2.0.dev1/src/uptimely/project/paths.py +27 -0
- uptimely_core-0.2.0.dev1/src/uptimely/render.py +32 -0
- uptimely_core-0.2.0.dev1/src/uptimely/row_context.py +216 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/__init__.py +65 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/api.py +82 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/catalog.py +41 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/feature_graph/__init__.py +14 -0
- uptimely_core-0.1.0.dev0/src/uptimely/analytics/feature_graph.py → uptimely_core-0.2.0.dev1/src/uptimely/spec/feature_graph/graph.py +135 -110
- {uptimely_core-0.1.0.dev0/src/uptimely/analytics → uptimely_core-0.2.0.dev1/src/uptimely/spec/feature_graph}/models.py +17 -23
- uptimely_core-0.2.0.dev1/src/uptimely/spec/json/__init__.py +1 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/json/codec.py +41 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/model/__init__.py +48 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/model/common.py +47 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/model/dataset.py +115 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/model/functions.py +57 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/model/spec.py +86 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/__init__.py +7 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/bind.py +121 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/conversion.py +158 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/dataset.py +140 -0
- {uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec → uptimely_core-0.2.0.dev1/src/uptimely/spec/python}/dimension.py +5 -5
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/entity.py +35 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/feature.py +28 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/mappings.py +87 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/ref.py +139 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/row_context.py +165 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/python/spec.py +60 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/references.py +145 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/resolution.py +78 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/resolved.py +60 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/__init__.py +20 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/context.py +174 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/dependencies.py +23 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/dimensions.py +111 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/errors.py +13 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/functions.py +150 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/mappings.py +434 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/prepared.py +155 -0
- uptimely_core-0.2.0.dev1/src/uptimely/spec/validation/spec.py +38 -0
- {uptimely_core-0.1.0.dev0/src/uptimely/spec → uptimely_core-0.2.0.dev1/src/uptimely}/storage.py +33 -16
- uptimely_core-0.2.0.dev1/src/uptimely/templates/base.html.j2 +104 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/execution/data_lineage.html.j2 +403 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/base.html.j2 +62 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/feature_graph.html.j2 +50 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/feature_graph_shared.html.j2 +49 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/feature_renderer_2d.js.j2 +259 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/feature_renderer_3d.js.j2 +60 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/view_controller.js.j2 +56 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/graph/view_controls.html.j2 +10 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/icons.html.j2 +17 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/index.html.j2 +153 -0
- {uptimely_core-0.1.0.dev0/src/uptimely/compile/templates → uptimely_core-0.2.0.dev1/src/uptimely/templates/plan}/callable_graph.html.j2 +42 -36
- uptimely_core-0.2.0.dev1/src/uptimely/templates/plan/callable_renderer_2d.js.j2 +124 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/plan/graph.html.j2 +6 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/plan/index.html.j2 +136 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/plan/overview.html.j2 +3 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/plan/stages.html.j2 +3 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/datasets.html.j2 +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/dimensions.html.j2 +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/entities.html.j2 +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/features.html.j2 +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/functions.html.j2 +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/index.html.j2 +26 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/overview.html.j2 +5 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/section_header.html.j2 +13 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/sections/dataset_list.html.j2 +124 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/sections/dimensions.html.j2 +17 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/sections/entities.html.j2 +80 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/sections/entity_list.html.j2 +39 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/sections/functions.html.j2 +64 -0
- uptimely_core-0.2.0.dev1/src/uptimely/templates/spec/sections/overview.html.j2 +42 -0
- uptimely_core-0.1.0.dev0/src/uptimely/__init__.py +0 -27
- uptimely_core-0.1.0.dev0/src/uptimely/analytics/__init__.py +0 -18
- uptimely_core-0.1.0.dev0/src/uptimely/analytics/render.py +0 -110
- uptimely_core-0.1.0.dev0/src/uptimely/analytics/templates/base.html.j2 +0 -31
- uptimely_core-0.1.0.dev0/src/uptimely/analytics/templates/graph.html.j2 +0 -118
- uptimely_core-0.1.0.dev0/src/uptimely/compile/__init__.py +0 -16
- uptimely_core-0.1.0.dev0/src/uptimely/compile/compiler.py +0 -161
- uptimely_core-0.1.0.dev0/src/uptimely/compile/entrypoint.py +0 -89
- uptimely_core-0.1.0.dev0/src/uptimely/compile/execution_graph.py +0 -272
- uptimely_core-0.1.0.dev0/src/uptimely/compile/node.py +0 -68
- uptimely_core-0.1.0.dev0/src/uptimely/compile/render.py +0 -85
- uptimely_core-0.1.0.dev0/src/uptimely/compile/templates/report.html.j2 +0 -108
- uptimely_core-0.1.0.dev0/src/uptimely/docs/docs.py +0 -248
- uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/docs.html.j2 +0 -42
- uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/dimensions.html.j2 +0 -4
- uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/entities.html.j2 +0 -172
- uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/functions.html.j2 +0 -48
- uptimely_core-0.1.0.dev0/src/uptimely/docs/templates/sections/overview.html.j2 +0 -18
- uptimely_core-0.1.0.dev0/src/uptimely/engine/backend.py +0 -127
- uptimely_core-0.1.0.dev0/src/uptimely/engine/engine.py +0 -424
- uptimely_core-0.1.0.dev0/src/uptimely/mcp/__init__.py +0 -5
- uptimely_core-0.1.0.dev0/src/uptimely/mcp/server.py +0 -321
- uptimely_core-0.1.0.dev0/src/uptimely/plan/__init__.py +0 -6
- uptimely_core-0.1.0.dev0/src/uptimely/plan/models.py +0 -275
- uptimely_core-0.1.0.dev0/src/uptimely/plan/serialization.py +0 -16
- uptimely_core-0.1.0.dev0/src/uptimely/spec/__init__.py +0 -50
- uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/__init__.py +0 -1
- uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/bindings.py +0 -95
- uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_functions.py +0 -353
- uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_model.py +0 -360
- uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_parser.py +0 -378
- uptimely_core-0.1.0.dev0/src/uptimely/spec/json_spec/json_serializer.py +0 -32
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/__init__.py +0 -1
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/bind.py +0 -101
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/dataset.py +0 -199
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/entity.py +0 -53
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/feature.py +0 -93
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/fragment.py +0 -106
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/ref.py +0 -63
- uptimely_core-0.1.0.dev0/src/uptimely/spec/python_spec/spec.py +0 -76
- uptimely_core-0.1.0.dev0/src/uptimely/spec/validation.py +0 -323
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/LICENSE +0 -0
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/src/uptimely/cli/__init__.py +0 -0
- {uptimely_core-0.1.0.dev0/src/uptimely/docs → uptimely_core-0.2.0.dev1/src/uptimely/explorer}/__init__.py +0 -0
- {uptimely_core-0.1.0.dev0 → uptimely_core-0.2.0.dev1}/src/uptimely/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uptimely-core
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0.dev1
|
|
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,
|
|
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
|
|
33
|
+
Uptimely Core is an open source Python package for declarative numerical
|
|
34
|
+
analytics with complex dependencies at scale.
|
|
34
35
|
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.2.0.dev1"
|
|
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 = "
|
|
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
|
|
3
|
+
Uptimely Core is an open source Python package for declarative numerical
|
|
4
|
+
analytics with complex dependencies at scale.
|
|
4
5
|
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from urllib
|
|
12
|
-
from urllib
|
|
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(
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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(
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
115
|
-
"""
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}")
|