odctl 1.2.0__tar.gz → 1.2.1__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.
- {odctl-1.2.0 → odctl-1.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +6 -6
- {odctl-1.2.0 → odctl-1.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
- {odctl-1.2.0 → odctl-1.2.1}/.github/scripts/select-profiles.py +39 -10
- {odctl-1.2.0 → odctl-1.2.1}/.github/scripts/smoke.sh +179 -51
- {odctl-1.2.0 → odctl-1.2.1}/CLAUDE.md +11 -6
- odctl-1.2.1/PKG-INFO +136 -0
- odctl-1.2.1/README.md +99 -0
- odctl-1.2.1/image/demo.gif +0 -0
- odctl-1.2.1/image/demo.png +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/mkdocs.yml +26 -12
- {odctl-1.2.0 → odctl-1.2.1}/pyproject.toml +3 -3
- {odctl-1.2.0 → odctl-1.2.1}/scripts/diagrams.py +87 -3
- {odctl-1.2.0 → odctl-1.2.1}/scripts/gen_pages.py +155 -20
- {odctl-1.2.0 → odctl-1.2.1}/scripts/llmstxt_sections.py +9 -0
- {odctl-1.2.0 → odctl-1.2.1}/scripts/local_links.py +21 -1
- odctl-1.2.1/scripts/release_links.py +96 -0
- {odctl-1.2.0 → odctl-1.2.1}/skills/odctl/SKILL.md +45 -6
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/config.py +7 -2
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/docker.py +16 -120
- odctl-1.2.1/src/odctl/logs.py +136 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/main.py +142 -28
- odctl-1.2.1/src/odctl/projects.py +79 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/ui.py +1 -1
- odctl-1.2.1/tests/docs_helpers.py +53 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_cli_extended.py +51 -0
- odctl-1.2.1/tests/test_confirmed_pages.py +23 -0
- odctl-1.2.1/tests/test_diagrams.py +69 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_docker.py +6 -36
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_docs.py +42 -57
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_evidently_profile.py +2 -1
- odctl-1.2.1/tests/test_feature_docs.py +120 -0
- odctl-1.2.1/tests/test_logs.py +36 -0
- odctl-1.2.1/tests/test_projects.py +71 -0
- odctl-1.2.1/tests/test_release_links.py +47 -0
- {odctl-1.2.0 → odctl-1.2.1}/uv.lock +1 -1
- odctl-1.2.0/PKG-INFO +0 -121
- odctl-1.2.0/README.md +0 -84
- odctl-1.2.0/image/demo.gif +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/renovate.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/scripts/spark_avro_producer.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/workflows/build-deps.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/workflows/build-platform-images.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/workflows/ci.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/workflows/e2e-test.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/workflows/pipeline.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.github/workflows/publish.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.gitignore +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.pre-commit-config.yaml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/.vscode/settings.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/LICENSE +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/image/diagram.drawio +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/image/diagram.png +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/scripts/release_notes.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/__init__.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/planner.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/registry.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/airflow/odctl_log_config.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/config.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/init.sql +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/keeper_config.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/macros-11-lite.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/macros-11.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/macros-12-lite.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/macros-12.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/macros-21.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/macros-22.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/clickhouse/users.xml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-analytics.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-deps.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-flink.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-infra.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-kafka.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-metadata.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-mlops.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-obsv.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-orch.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-spark.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/compose-store.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/Dockerfile +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/build.gradle +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/build_connectors.sh +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/fetch_flink_dependencies.sh +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/fetch_spark_dependencies.sh +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/fetch_standalone_plugins.sh +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/deps/versions.env +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/docker/airflow/Dockerfile +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/docker/mlflow/Dockerfile +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/docker/postgres/Dockerfile +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/docker/spark/Dockerfile +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/evidently/config.yaml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/feast/feature_store.yaml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/flink/flink-conf.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/airflow.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/clickhouse.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/feast.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/flink.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/fluss.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/iceberg-catalog.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/kafka-connect.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/kafka.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/karapace.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/marquez.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/metabase.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/mlflow.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/openmetadata.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/postgresql.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/seaweedfs.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/spark.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/temporal.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/trino.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/dashboards/valkey.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/odctl-dashboards.yaml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/grafana/sources.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/jmx/jmx-exporter.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/marquez/marquez.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/otelcol/odctl-receivers.yaml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/postgres/01-init-databases.sh +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/prometheus/prometheus.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/registry.yml +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/seaweed/s3.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/spark/log4j2.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/spark/metrics.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/spark/spark-defaults.conf +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/access-control.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/catalog/clickhouse.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/catalog/iceberg.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/catalog/kafka.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/catalog/postgres.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/catalog/valkey.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/group-provider.properties +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/groups.txt +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/resources/trino/rules.json +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/src/odctl/workspace.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/conftest.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_airflow_profile.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_cli.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_compose_profiles.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_config.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_feast_profile.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_grafana_dashboards.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_metrics_collector.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_metrics_jvm.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_metrics_python_services.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_planner.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_postgres_image.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_registry.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_release_notes.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_telemetry_profile.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_temporal_profile.py +0 -0
- {odctl-1.2.0 → odctl-1.2.1}/tests/test_workspace.py +0 -0
|
@@ -14,7 +14,7 @@ A clear and concise description of what the bug is.
|
|
|
14
14
|
|
|
15
15
|
Steps to reproduce the behavior:
|
|
16
16
|
|
|
17
|
-
1. What profiles were you trying to launch? (
|
|
17
|
+
1. What profiles were you trying to launch? (for example, `odctl up airflow spark-lite ch-lite`)
|
|
18
18
|
2. Run this CLI command: '...'
|
|
19
19
|
3. See error
|
|
20
20
|
|
|
@@ -34,13 +34,13 @@ If applicable, paste the full CLI error or the specific container logs here (you
|
|
|
34
34
|
|
|
35
35
|
**Environment (please complete the following information):**
|
|
36
36
|
|
|
37
|
-
* OS: [
|
|
38
|
-
* Python Version: [
|
|
39
|
-
* odctl version (`odctl info`): [
|
|
40
|
-
* Docker Version: [
|
|
37
|
+
* OS: [for example Ubuntu 24.04, macOS Sonoma, Windows 11 / WSL2]
|
|
38
|
+
* Python Version: [for example 3.12]
|
|
39
|
+
* odctl version (`odctl info`): [for example 1.0.0]
|
|
40
|
+
* Docker Version: [for example Docker Desktop 4.28.0, Docker Engine 25.0]
|
|
41
41
|
|
|
42
42
|
## **Additional context**
|
|
43
43
|
|
|
44
|
-
* Did you customize any files in your local `./.odctl/` workspace? (
|
|
44
|
+
* Did you customize any files in your local `./.odctl/` workspace? (for example, changing ports in `compose-orch.yml` or editing `registry.yml`)
|
|
45
45
|
* How much RAM/CPU is allocated to your Docker daemon? (Many Open Data profiles are resource-heavy)
|
|
46
46
|
* Add any other context about the problem here.
|
|
@@ -12,7 +12,7 @@ A clear and concise description of what the problem is. Ex. I'm always frustrate
|
|
|
12
12
|
|
|
13
13
|
**Describe the solution you'd like**
|
|
14
14
|
|
|
15
|
-
A clear and concise description of what you want to happen. (
|
|
15
|
+
A clear and concise description of what you want to happen. (for example, "I want a new profile added to `compose-orch.yml` for Dagster", or "Add a dedicated connector script for MongoDB CDC.")
|
|
16
16
|
|
|
17
17
|
**Describe alternatives you've considered**
|
|
18
18
|
|
|
@@ -70,12 +70,12 @@ REGISTRY = Path("src/odctl/resources/registry.yml")
|
|
|
70
70
|
|
|
71
71
|
# smoke.sh starts some profiles from another group's case: the kafka case starts
|
|
72
72
|
# catalog, the flink case starts kafka-lite and feast, the spark case starts
|
|
73
|
-
# kafka-lite, and the fluss
|
|
74
|
-
# needs these groups as well as the ones the registry maps to it.
|
|
73
|
+
# kafka-lite, and the fluss and feast cases start flink-lite. A change to that
|
|
74
|
+
# compose file needs these groups as well as the ones the registry maps to it.
|
|
75
75
|
ALSO_RUNS = {
|
|
76
76
|
"compose-infra.yml": ["kafka-lite kafka-full"],
|
|
77
77
|
"compose-kafka.yml": ["flink-lite flink-full", "spark-lite spark-full"],
|
|
78
|
-
"compose-flink.yml": ["fluss"],
|
|
78
|
+
"compose-flink.yml": ["fluss", "feast"],
|
|
79
79
|
"compose-mlops.yml": ["flink-lite flink-full"],
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -84,21 +84,23 @@ ALSO_RUNS = {
|
|
|
84
84
|
# map runs everything, and a test fails until it is added.
|
|
85
85
|
SNIPPET_PREFIX = "docs/snippets/"
|
|
86
86
|
SNIPPET_GROUPS = {
|
|
87
|
+
"getting-started": ["kafka-lite kafka-full"],
|
|
88
|
+
"how-it-works": ["storage", "catalog", "trino", "airflow"],
|
|
87
89
|
"kafka": ["kafka-lite kafka-full"],
|
|
88
90
|
"kafka-connect": ["kafka-lite kafka-full"],
|
|
89
91
|
"iceberg-ingestion": ["kafka-lite kafka-full", "flink-lite flink-full"],
|
|
90
|
-
"flink-sql": ["flink-lite flink-full"
|
|
92
|
+
"flink-sql": ["flink-lite flink-full"],
|
|
93
|
+
"rolling-windows": ["flink-lite flink-full"],
|
|
91
94
|
"fluss": ["fluss"],
|
|
92
95
|
"spark": ["spark-lite spark-full"],
|
|
93
96
|
"clickhouse": ["ch-lite ch-full"],
|
|
94
97
|
"trino-metabase": ["trino", "metabase"],
|
|
95
98
|
"hybrid-search": ["postgres"],
|
|
96
|
-
"storage": ["storage"],
|
|
97
99
|
"valkey": ["valkey"],
|
|
98
100
|
"airflow-dags": ["airflow"],
|
|
99
101
|
"temporal-workers": ["temporal"],
|
|
100
102
|
"mlflow": ["mlflow"],
|
|
101
|
-
"feast-iceberg": ["feast"],
|
|
103
|
+
"feast-iceberg": ["feast", "flink-lite flink-full", "mlflow"],
|
|
102
104
|
"evidently-reports": ["evidently"],
|
|
103
105
|
"lineage": ["lineage"],
|
|
104
106
|
"openmetadata": ["metadata"],
|
|
@@ -106,8 +108,16 @@ SNIPPET_GROUPS = {
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
def
|
|
110
|
-
"""
|
|
111
|
+
def _profile_to_file(registry_path: Path = REGISTRY) -> dict:
|
|
112
|
+
"""
|
|
113
|
+
Map each profile to the compose file the registry says provides it.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
registry_path (Path): The registry.yml to read.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict: The compose file name of each profile.
|
|
120
|
+
"""
|
|
111
121
|
data = yaml.safe_load(registry_path.read_text())
|
|
112
122
|
mapping = {}
|
|
113
123
|
for stack in data["stacks"].values():
|
|
@@ -117,12 +127,30 @@ def profile_to_file(registry_path: Path = REGISTRY) -> dict:
|
|
|
117
127
|
|
|
118
128
|
|
|
119
129
|
def select(changed: list, registry_path: Path = REGISTRY) -> list:
|
|
120
|
-
"""
|
|
130
|
+
"""
|
|
131
|
+
Return the profile groups that the changed files require.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
changed (list): The paths of the changed files, relative to the repository.
|
|
135
|
+
registry_path (Path): The registry.yml that maps profiles to compose files.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
list: The groups to run, every group when a change affects them all, or none.
|
|
139
|
+
"""
|
|
121
140
|
changed = [c for c in changed if c.strip()]
|
|
122
141
|
if not changed:
|
|
123
142
|
return []
|
|
124
143
|
|
|
125
144
|
def is_compose(path: str) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Tell whether a path is one of odctl's compose files.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
path (str): A changed file's path, relative to the repository.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
bool: True when the path is under the compose files' folder.
|
|
153
|
+
"""
|
|
126
154
|
return path.startswith(COMPOSE_PREFIX)
|
|
127
155
|
|
|
128
156
|
broad = [
|
|
@@ -141,7 +169,7 @@ def select(changed: list, registry_path: Path = REGISTRY) -> list:
|
|
|
141
169
|
return list(GROUPS)
|
|
142
170
|
snippet_groups.update(SNIPPET_GROUPS[guide])
|
|
143
171
|
|
|
144
|
-
mapping =
|
|
172
|
+
mapping = _profile_to_file(registry_path)
|
|
145
173
|
touched_files = {Path(c).name for c in changed if is_compose(c)}
|
|
146
174
|
|
|
147
175
|
selected = []
|
|
@@ -154,6 +182,7 @@ def select(changed: list, registry_path: Path = REGISTRY) -> list:
|
|
|
154
182
|
|
|
155
183
|
|
|
156
184
|
def main() -> None:
|
|
185
|
+
"""Print the groups for the files named on the command line, or every group with `--all`, as JSON."""
|
|
157
186
|
args = sys.argv[1:]
|
|
158
187
|
if "--all" in args:
|
|
159
188
|
print(json.dumps(list(GROUPS)))
|
|
@@ -72,6 +72,65 @@ run_snippet() {
|
|
|
72
72
|
bash -o pipefail -c "$code"
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
# Run a section and check its output: snippet_says kafka/commands.sh list -F 'orders'.
|
|
76
|
+
# The whole output is read before grep looks at it. A grep -q at the end of a pipe
|
|
77
|
+
# stops at the first match, the command then fails writing its next line, and
|
|
78
|
+
# pipefail reports the check as failed.
|
|
79
|
+
snippet_says() {
|
|
80
|
+
local file="$1" section="$2" out
|
|
81
|
+
shift 2
|
|
82
|
+
out=$(run_snippet "$file" "$section" 2>/dev/null) || return 1
|
|
83
|
+
grep -q "$@" <<<"$out"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Run a guide's Runnable Example from the current directory, where the .odctl
|
|
87
|
+
# workspace is, and check that its output matches an extended regular
|
|
88
|
+
# expression: run_example kafka '"n":5'. The optional third argument is the
|
|
89
|
+
# command that runs the file, such as a Python with the client installed.
|
|
90
|
+
run_example() {
|
|
91
|
+
local guide="$1" want="$2" runner="${3:-bash}" file out
|
|
92
|
+
file=$(ls "$SNIPPETS/$guide"/example.* 2>/dev/null | head -1)
|
|
93
|
+
[ -n "$file" ] || fail "no runnable example in docs/snippets/$guide"
|
|
94
|
+
out=$($runner "$file" 2>&1) || { echo "$out" | tail -20; fail "the $guide example failed"; }
|
|
95
|
+
grep -qE -- "$want" <<<"$out" || { echo "$out" | tail -20; fail "the $guide example printed no '$want'"; }
|
|
96
|
+
pass "the $guide guide's runnable example printed '$want'"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# The commands that run the MLflow and Evidently examples: a Python 3.12 with
|
|
100
|
+
# the client versions the images pin, as the guides tell the reader to install.
|
|
101
|
+
mlflow_runner() {
|
|
102
|
+
local dockerfile="$SNIPPETS/../../src/odctl/resources/docker/mlflow/Dockerfile" mlflow xgboost
|
|
103
|
+
mlflow=$(grep -oE 'mlflow/mlflow:v[0-9.]+' "$dockerfile" | head -1 | cut -dv -f2)
|
|
104
|
+
xgboost=$(grep -oE "xgboost~=[0-9.]+" "$dockerfile" | head -1 | cut -d= -f2)
|
|
105
|
+
echo "uv run --no-project --quiet --python 3.12 --with mlflow==$mlflow --with xgboost~=$xgboost"
|
|
106
|
+
}
|
|
107
|
+
evidently_runner() {
|
|
108
|
+
local version
|
|
109
|
+
version=$(grep -oE 'evidently/evidently-service:[0-9.]+' \
|
|
110
|
+
"$SNIPPETS/../../src/odctl/resources/compose-mlops.yml" | head -1 | cut -d: -f2)
|
|
111
|
+
echo "uv run --no-project --quiet --python 3.12 --with evidently==$version --with pandas<3.1 python"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# How It Works edits a workspace file and applies it with odctl recreate. Each
|
|
115
|
+
# check runs the guide's section, puts the file back and recreates again, so
|
|
116
|
+
# later profiles in the same job see the shipped settings, and matches the
|
|
117
|
+
# output against an extended regular expression.
|
|
118
|
+
how_it_works_edit() {
|
|
119
|
+
local section="$1" file="$2" profile="$3" want="$4" out
|
|
120
|
+
out=$(run_snippet how-it-works/commands.sh "$section" 2>&1) || { echo "$out" | tail -20; fail "the $section example failed"; }
|
|
121
|
+
mv "$file.bak" "$file" || fail "the $section example left no $file.bak"
|
|
122
|
+
odctl recreate "$profile" >/dev/null 2>&1 || fail "could not recreate $profile after the $section example"
|
|
123
|
+
grep -qE -- "$want" <<<"$out" || { echo "$out" | tail -20; fail "the $section example printed no '$want'"; }
|
|
124
|
+
pass "How It Works' $section example printed '$want'"
|
|
125
|
+
}
|
|
126
|
+
how_it_works_trino() {
|
|
127
|
+
how_it_works_edit trino-port .odctl/compose-analytics.yml trino '8080/tcp -> 127\.0\.0\.1:8180'
|
|
128
|
+
retry 30 5 http_ok "http://127.0.0.1:8080/v1/info" || fail "Trino did not answer on 8080 after the port was put back"
|
|
129
|
+
}
|
|
130
|
+
how_it_works_airflow() {
|
|
131
|
+
how_it_works_edit airflow-deps .odctl/.env airflow '^1\.4\.3$'
|
|
132
|
+
}
|
|
133
|
+
|
|
75
134
|
# Metrics: nothing else here reads a metrics endpoint, so a change that breaks
|
|
76
135
|
# one goes unseen. In 0.8.0 the JMX agent in KAFKA_OPTS broke every Kafka CLI
|
|
77
136
|
# run, and only an unrelated topic check caught it. So each profile reads its
|
|
@@ -176,6 +235,50 @@ connect_plugins() {
|
|
|
176
235
|
pass "Connect lists the Iceberg, Debezium and ClickHouse plugins"
|
|
177
236
|
}
|
|
178
237
|
|
|
238
|
+
# The Kafka Connect guide registers the MSK data generator, which needs only
|
|
239
|
+
# Kafka, reads three of its records, then checks and removes the connector.
|
|
240
|
+
connect_datagen() {
|
|
241
|
+
(cd "$SNIPPETS/kafka-connect" && run_snippet kafka-connect/commands.sh register) >/dev/null 2>&1 \
|
|
242
|
+
|| fail "Connect rejected the data generator configuration"
|
|
243
|
+
local got status
|
|
244
|
+
got=$(run_snippet kafka-connect/commands.sh read 2>/dev/null | grep -c '"name"')
|
|
245
|
+
[ "$got" -eq 3 ] || fail "read $got of 3 data generator records from customers"
|
|
246
|
+
retry 12 5 sh -c "curl -fsS http://127.0.0.1:8083/connectors/demo-datagen/status | grep -q '\"state\":\"RUNNING\"'" \
|
|
247
|
+
|| fail "the data generator was not RUNNING"
|
|
248
|
+
status=$(run_snippet kafka-connect/commands.sh status-remove 2>/dev/null)
|
|
249
|
+
grep -q '"state":"RUNNING"' <<<"$status" || { echo "$status"; fail "the data generator status was not RUNNING"; }
|
|
250
|
+
curl -fsS "http://127.0.0.1:8083/connectors/demo-datagen" >/dev/null 2>&1 \
|
|
251
|
+
&& fail "the data generator is still registered after the delete"
|
|
252
|
+
pass "the data generator wrote records to customers, and was checked and removed"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Getting Started runs each read-only command on kafka-lite, then restarts it.
|
|
256
|
+
getting_started() {
|
|
257
|
+
local out
|
|
258
|
+
out=$(run_snippet getting-started/commands.sh list 2>&1) || fail "odctl list failed"
|
|
259
|
+
grep -q "kafka-lite" <<<"$out" || fail "odctl list does not name kafka-lite"
|
|
260
|
+
out=$(run_snippet getting-started/commands.sh list-details 2>&1) || fail "odctl list -d failed"
|
|
261
|
+
grep -q "kafka-ui" <<<"$out" || fail "odctl list -d does not name the kafka-ui service"
|
|
262
|
+
out=$(run_snippet getting-started/commands.sh explain 2>&1) || fail "odctl explain failed"
|
|
263
|
+
grep -q "karapace" <<<"$out" || fail "odctl explain kafka-lite does not name karapace"
|
|
264
|
+
out=$(run_snippet getting-started/commands.sh info 2>&1) || fail "odctl info failed"
|
|
265
|
+
grep -q "Reachable" <<<"$out" || fail "odctl info does not say Docker is reachable"
|
|
266
|
+
out=$(run_snippet getting-started/commands.sh ps 2>&1) || fail "odctl ps failed"
|
|
267
|
+
grep -q "connect" <<<"$out" || fail "odctl ps kafka-lite does not list connect"
|
|
268
|
+
out=$(run_snippet getting-started/commands.sh ps-all 2>&1) || fail "odctl ps --all failed"
|
|
269
|
+
grep -q "karapace" <<<"$out" || fail "odctl ps --all does not list karapace"
|
|
270
|
+
out=$(run_snippet getting-started/commands.sh logs 2>&1) || fail "odctl logs failed"
|
|
271
|
+
grep -q "^connect" <<<"$out" || fail "odctl logs printed no connect lines"
|
|
272
|
+
out=$(run_snippet getting-started/commands.sh logs-time 2>&1) || fail "odctl logs with --since and --until failed"
|
|
273
|
+
grep -qE "^connect +\| [0-9]{4}-" <<<"$out" || fail "odctl logs -t printed no timestamped connect lines: $out"
|
|
274
|
+
out=$(run_snippet getting-started/commands.sh down-dry-run 2>&1) || fail "odctl down --dry-run failed"
|
|
275
|
+
docker inspect connect >/dev/null 2>&1 || fail "odctl down --dry-run removed a container"
|
|
276
|
+
out=$(run_snippet getting-started/commands.sh restart 2>&1) || fail "odctl restart failed"
|
|
277
|
+
grep -q "Restart complete" <<<"$out" || fail "odctl restart did not complete"
|
|
278
|
+
retry 40 5 http_ok "http://127.0.0.1:8083/connectors" || fail "Connect never answered after the restart"
|
|
279
|
+
pass "Getting Started's list, explain, info, ps, logs, restart and down --dry-run ran on kafka-lite"
|
|
280
|
+
}
|
|
281
|
+
|
|
179
282
|
# The Iceberg sink is built from source, so prove it commits a snapshot rather
|
|
180
283
|
# than only loading. It needs the catalog, which kafka-lite does not start.
|
|
181
284
|
smoke_iceberg_sink() {
|
|
@@ -186,7 +289,7 @@ smoke_iceberg_sink() {
|
|
|
186
289
|
run_snippet iceberg-ingestion/commands.sh topics-produce >/dev/null 2>&1 \
|
|
187
290
|
|| fail "could not create the topics or produce to demo-iceberg"
|
|
188
291
|
# From the snippet folder, where the guide's @demo-iceberg.json is.
|
|
189
|
-
(cd "$SNIPPETS/
|
|
292
|
+
(cd "$SNIPPETS/iceberg-ingestion" && run_snippet iceberg-ingestion/commands.sh register) >/dev/null 2>&1 \
|
|
190
293
|
|| fail "Connect rejected the Iceberg sink configuration"
|
|
191
294
|
|
|
192
295
|
# The first commit waits for the control consumer to join its group, which
|
|
@@ -208,7 +311,9 @@ print(snap[0]["summary"].get("total-records", "") if snap else "")' 2>/dev/null)
|
|
|
208
311
|
fi
|
|
209
312
|
run_snippet kafka-connect/commands.sh logs >/dev/null 2>&1 || fail "could not read the Connect log"
|
|
210
313
|
local status
|
|
211
|
-
|
|
314
|
+
snippet_says iceberg-ingestion/commands.sh records -F '"total-records":"5"' \
|
|
315
|
+
|| fail "the guide's records step does not show 5 records"
|
|
316
|
+
status=$(run_snippet iceberg-ingestion/commands.sh status-remove 2>/dev/null)
|
|
212
317
|
grep -q '"state":"RUNNING"' <<<"$status" || { echo "$status"; fail "the connector status was not RUNNING"; }
|
|
213
318
|
curl -fsS "http://127.0.0.1:8083/connectors/demo-iceberg" >/dev/null 2>&1 \
|
|
214
319
|
&& fail "the connector is still registered after the delete"
|
|
@@ -270,11 +375,11 @@ smoke_flink_kafka_jdbc() {
|
|
|
270
375
|
# Two different OVER windows in one streaming query are refused, so the guide
|
|
271
376
|
# runs one statement for each window size, each writing its own table.
|
|
272
377
|
local refused
|
|
273
|
-
refused=$(snippet
|
|
378
|
+
refused=$(snippet rolling-windows/steps.sql windows-refused | docker exec -i flink-jobmanager /opt/flink/bin/sql-client.sh 2>&1)
|
|
274
379
|
grep -qF '[ERROR]' <<<"$refused" || fail "Flink did not refuse two different OVER windows: $(tail -5 <<<"$refused")"
|
|
275
|
-
run_snippet
|
|
276
|
-
out=$(snippet
|
|
277
|
-
run_snippet
|
|
380
|
+
run_snippet rolling-windows/commands.sh create-window-tables >/dev/null 2>&1 || fail "could not create the window tables"
|
|
381
|
+
out=$(snippet rolling-windows/steps.sql windows-one-per-size | docker exec -i flink-jobmanager /opt/flink/bin/sql-client.sh 2>&1)
|
|
382
|
+
run_snippet rolling-windows/commands.sh check-windows >/dev/null 2>&1 || fail "the guide's window check query failed"
|
|
278
383
|
rows=$(docker exec postgres psql -U user -d odctl -tAc \
|
|
279
384
|
"SELECT (SELECT count(*) FROM card_sum_10m) || ',' || (SELECT count(*) FROM card_sum_1h)" 2>/dev/null)
|
|
280
385
|
[ "$rows" = "20,20" ] || { echo "$out" | grep -iE "error|exception" | tail -10; fail "one statement for each OVER window gave '$rows' rows, expected 20,20"; }
|
|
@@ -288,11 +393,11 @@ smoke_flink_kafka_jdbc() {
|
|
|
288
393
|
smoke_flink_http() {
|
|
289
394
|
odctl up feast >/dev/null 2>&1 || fail "could not start feast for the Flink HTTP check"
|
|
290
395
|
retry 40 5 http_ok "http://127.0.0.1:6566/health" || fail "Feast never answered on :6566"
|
|
291
|
-
(cd "$SNIPPETS/
|
|
396
|
+
(cd "$SNIPPETS/feast-iceberg" && run_snippet feast-iceberg/commands.sh push-apply) >/dev/null 2>&1 \
|
|
292
397
|
|| fail "could not apply the Feast push source"
|
|
293
398
|
local out
|
|
294
|
-
out=$(snippet
|
|
295
|
-
feast_value() { run_snippet
|
|
399
|
+
out=$(snippet feast-iceberg/steps.sql feast-push | docker exec -i flink-jobmanager /opt/flink/bin/sql-client.sh 2>&1)
|
|
400
|
+
feast_value() { run_snippet feast-iceberg/commands.sh push-read 2>/dev/null | grep -F 0.8999999761581421; }
|
|
296
401
|
retry 24 5 feast_value \
|
|
297
402
|
|| { echo "$out" | grep -iE "error|exception" | tail -10; fail "Feast never returned the value the Flink HTTP sink pushed"; }
|
|
298
403
|
odctl down feast >/dev/null 2>&1 || fail "could not stop feast after the Flink HTTP check"
|
|
@@ -421,7 +526,8 @@ smoke_infra() {
|
|
|
421
526
|
[ "$metres" = "714" ] || fail "PostGIS distance returned '$metres' km, expected 714"
|
|
422
527
|
pass "PostGIS: geography distance works"
|
|
423
528
|
# Read by the telemetry collector's postgresql receiver.
|
|
424
|
-
assert_pushed_metrics 'postgresql_.+' PostgreSQL
|
|
529
|
+
assert_pushed_metrics 'postgresql_.+' PostgreSQL
|
|
530
|
+
run_example hybrid-search '^ +2$' ;;
|
|
425
531
|
storage)
|
|
426
532
|
retry 30 5 http_reachable "http://127.0.0.1:8333" || fail "S3 API never answered"
|
|
427
533
|
# A port that answers is not a bucket that stores anything. Spark, Flink,
|
|
@@ -457,10 +563,11 @@ smoke_infra() {
|
|
|
457
563
|
pass "three uploads of 268 MB passed and seaweed was not killed"
|
|
458
564
|
# How it works shows how to create a bucket before its profile exists.
|
|
459
565
|
local buckets
|
|
460
|
-
buckets=$(run_snippet
|
|
566
|
+
buckets=$(run_snippet how-it-works/commands.sh create-bucket 2>&1) || fail "the create-bucket example failed: $buckets"
|
|
461
567
|
grep -q "'desflow'" <<<"$buckets" || fail "the bucket list has no desflow: $buckets"
|
|
462
568
|
docker exec seaweed sh -c "curl -fsS $sig '$cred' -X DELETE 'http://localhost:8333/desflow'" >/dev/null 2>&1 || true
|
|
463
569
|
pass "the create-bucket example created a bucket with the S3 login"
|
|
570
|
+
how_it_works_edit seaweed-memory .odctl/compose-infra.yml storage '^2147483648$'
|
|
464
571
|
assert_metrics http://seaweed:9327/metrics ;;
|
|
465
572
|
catalog)
|
|
466
573
|
retry 30 5 http_ok "http://127.0.0.1:8181/v1/config" || fail "REST catalog never answered"
|
|
@@ -481,6 +588,22 @@ smoke_infra() {
|
|
|
481
588
|
| grep -q 'metadata-location' || fail "table metadata did not read back"
|
|
482
589
|
curl -fsS -X DELETE "http://127.0.0.1:8181/v1/namespaces/smoke/tables/t" >/dev/null 2>&1 || true
|
|
483
590
|
pass "table created through the catalog and its metadata read back"
|
|
591
|
+
# How It Works reaches the catalog from a container on the odctl network,
|
|
592
|
+
# previews a start and pulls the catalog's images.
|
|
593
|
+
snippet_says how-it-works/commands.sh reach '"defaults"' \
|
|
594
|
+
|| fail "a container on the odctl network did not reach catalog:8181"
|
|
595
|
+
snippet_says how-it-works/commands.sh dry-run 'compose-flink.yml' \
|
|
596
|
+
|| fail "odctl up flink-lite --dry-run did not list compose-flink.yml"
|
|
597
|
+
snippet_says how-it-works/commands.sh pull 'All images pulled successfully' \
|
|
598
|
+
|| fail "odctl pull catalog did not finish"
|
|
599
|
+
run_snippet how-it-works/commands.sh up-pull >/dev/null 2>&1 || fail "odctl up catalog --pull failed"
|
|
600
|
+
snippet_says how-it-works/commands.sh tag-override 'odctl/postgres:1.1.0' \
|
|
601
|
+
|| fail "a TAG in the shell did not change the image odctl explain shows"
|
|
602
|
+
snippet_says how-it-works/commands.sh workspace-flag 'catalog' \
|
|
603
|
+
|| fail "odctl --workspace ps catalog listed no catalog container"
|
|
604
|
+
run_snippet how-it-works/commands.sh recreate-pull >/dev/null 2>&1 || fail "odctl recreate catalog --pull failed"
|
|
605
|
+
retry 30 5 http_ok "http://127.0.0.1:8181/v1/config" || fail "REST catalog never answered after the recreate"
|
|
606
|
+
pass "How It Works' reach, dry-run, pull, up --pull, TAG, --workspace and recreate --pull examples ran"
|
|
484
607
|
assert_metrics http://catalog:9404/metrics ;;
|
|
485
608
|
valkey)
|
|
486
609
|
retry 30 5 docker exec valkey valkey-cli --no-auth-warning -u "redis://user:password@localhost:6379" ping \
|
|
@@ -492,7 +615,8 @@ smoke_infra() {
|
|
|
492
615
|
[ "$got" -eq 2 ] || fail "batch MGET returned $got of 2 values, expected 2"
|
|
493
616
|
pass "authenticated, multi-key write and batch MGET round-tripped"
|
|
494
617
|
# Read by the telemetry collector's redis receiver.
|
|
495
|
-
assert_pushed_metrics 'redis_.+' Valkey
|
|
618
|
+
assert_pushed_metrics 'redis_.+' Valkey
|
|
619
|
+
run_example valkey '^\[5\]$' ;;
|
|
496
620
|
esac
|
|
497
621
|
}
|
|
498
622
|
|
|
@@ -544,7 +668,7 @@ for db in (d.get("data") if isinstance(d, dict) else d) or []:
|
|
|
544
668
|
smoke_metabase() {
|
|
545
669
|
local sid
|
|
546
670
|
retry 60 5 http_ok "http://127.0.0.1:3000/api/health" || fail "no HTTP response from :3000"
|
|
547
|
-
|
|
671
|
+
snippet_says trino-metabase/commands.sh metabase-health '"status":"ok"' \
|
|
548
672
|
|| fail "Metabase health did not report ok"
|
|
549
673
|
assert_metrics http://metabase:9191/metrics
|
|
550
674
|
sid=$(retry_out 12 5 mb_session) || fail "could not obtain a Metabase session"
|
|
@@ -676,17 +800,17 @@ smoke_model_server() {
|
|
|
676
800
|
smoke_model_features() {
|
|
677
801
|
odctl up feast >/dev/null 2>&1 || fail "could not start feast for the features model"
|
|
678
802
|
retry 40 5 http_ok "http://127.0.0.1:6566/health" || fail "Feast never answered on :6566"
|
|
679
|
-
(cd "$SNIPPETS/
|
|
803
|
+
(cd "$SNIPPETS/feast-iceberg" && run_snippet feast-iceberg/commands.sh push-apply) >/dev/null 2>&1 \
|
|
680
804
|
|| fail "could not apply the Feast push source"
|
|
681
805
|
local now
|
|
682
806
|
now=$(date -u +%Y-%m-%dT%H:%M:%S)
|
|
683
807
|
retry 12 5 curl -fsS -X POST http://127.0.0.1:6566/push -H 'Content-Type: application/json' \
|
|
684
|
-
-d "{\"push_source_name\": \"
|
|
808
|
+
-d "{\"push_source_name\": \"driver_push\", \"allow_registry_cache\": false, \"df\": {\"driver_id\": [1001], \"conv_rate\": [0.9], \"event_timestamp\": [\"$now\"]}}" \
|
|
685
809
|
>/dev/null 2>&1 || fail "could not push a feature value to Feast"
|
|
686
810
|
mlflow_snippet features-model >/dev/null 2>&1 || fail "could not register the features model"
|
|
687
811
|
sed -i'' -e '/^MODEL_URI=/d' .odctl/.env
|
|
688
812
|
cat "$SNIPPETS/mlflow/model-uri-features.env" >> .odctl/.env
|
|
689
|
-
odctl
|
|
813
|
+
odctl recreate mlflow >/dev/null 2>&1 || fail "could not recreate mlflow-serve with the features model"
|
|
690
814
|
retry 30 5 http_ok "http://127.0.0.1:5003/ping" || fail "no HTTP response from :5003 for the features model"
|
|
691
815
|
local got
|
|
692
816
|
# feast-serve caches its registry for up to a minute after apply, and until it
|
|
@@ -1109,6 +1233,11 @@ smoke_feast() {
|
|
|
1109
1233
|
# catalogs, and Iceberg's REST fixture server serves the spec with no prefix,
|
|
1110
1234
|
# so a real warehouse gives HTTP 400 "Ambiguous URI empty segment".
|
|
1111
1235
|
cp "$SNIPPETS/feast-iceberg/feature_repo/"* "$work/feature_repo/"
|
|
1236
|
+
# Its own project, because the guide's Runnable Example registers the same
|
|
1237
|
+
# feature view in demo afterwards. feast teardown leaves the view's row in
|
|
1238
|
+
# feature_view_version_history, so a second apply in demo fails on its key.
|
|
1239
|
+
sed 's/^project: demo$/project: smoke/' "$work/feature_repo/feature_store.yaml" \
|
|
1240
|
+
> "$work/feature_store.yaml" && mv "$work/feature_store.yaml" "$work/feature_repo/feature_store.yaml"
|
|
1112
1241
|
|
|
1113
1242
|
cat > "$work/check.py" <<'PYCHECK'
|
|
1114
1243
|
import datetime, sys, pandas as pd
|
|
@@ -1174,7 +1303,8 @@ PYONLINE
|
|
|
1174
1303
|
curl -fsS --max-time 10 -X DELETE \
|
|
1175
1304
|
"http://127.0.0.1:8181/v1/namespaces/demo/tables/driver_stats?purgeRequested=true" >/dev/null 2>&1 || true
|
|
1176
1305
|
curl -fsS --max-time 10 -X DELETE "http://127.0.0.1:8181/v1/namespaces/demo" >/dev/null 2>&1 || true
|
|
1177
|
-
|
|
1306
|
+
# The venv stays: the guide's Runnable Example runs with it next, and the feast
|
|
1307
|
+
# case removes $work after that.
|
|
1178
1308
|
}
|
|
1179
1309
|
|
|
1180
1310
|
# Temporal: a healthy server proves only that it answers. Workflows run in the
|
|
@@ -1193,29 +1323,14 @@ smoke_temporal() {
|
|
|
1193
1323
|
VIRTUAL_ENV="$work/.venv" uv pip install -q "temporalio==1.34.0" >/dev/null 2>&1 \
|
|
1194
1324
|
|| fail "could not install the temporalio SDK"
|
|
1195
1325
|
|
|
1196
|
-
#
|
|
1197
|
-
#
|
|
1198
|
-
# guide tells the reader to do.
|
|
1326
|
+
# The worker step as the guide writes it, then the Runnable Example, which
|
|
1327
|
+
# adds the Approval workflow and its signal to the same script.
|
|
1199
1328
|
snippet temporal-workers/steps.py worker > "$work/greet.py"
|
|
1200
|
-
snippet temporal-workers/steps.py approval-workflow > "$work/approval.part"
|
|
1201
|
-
snippet temporal-workers/steps.py approval-run > "$work/run.part"
|
|
1202
|
-
python3 - "$work" <<'PYEOF' || fail "could not combine the guide's approval steps"
|
|
1203
|
-
import pathlib, sys, textwrap
|
|
1204
|
-
work = pathlib.Path(sys.argv[1])
|
|
1205
|
-
script = (work / "greet.py").read_text()
|
|
1206
|
-
head, tail = script.split("async def main() -> None:\n")
|
|
1207
|
-
body, rest = tail.split("\n\n", 1)
|
|
1208
|
-
connect = body.splitlines()[0]
|
|
1209
|
-
run = textwrap.indent((work / "run.part").read_text().rstrip(), " ")
|
|
1210
|
-
approval = (work / "approval.part").read_text()
|
|
1211
|
-
(work / "approval.py").write_text(
|
|
1212
|
-
f"{head}{approval}\n\nasync def main() -> None:\n{connect}\n{run}\n\n{rest}")
|
|
1213
|
-
PYEOF
|
|
1214
1329
|
local got
|
|
1215
1330
|
got=$(cd "$work" && "$work/.venv/bin/python" greet.py 2>&1 | tail -1)
|
|
1216
1331
|
[ "$got" = "hello odctl" ] || fail "the guide's worker printed '$got', expected hello odctl"
|
|
1217
|
-
got=$(cd "$work" && "$work/.venv/bin/python"
|
|
1218
|
-
[ "$got" = "approved by alice" ] || fail "the guide's
|
|
1332
|
+
got=$(cd "$work" && "$work/.venv/bin/python" "$SNIPPETS/temporal-workers/example.py" 2>&1 | tail -1)
|
|
1333
|
+
[ "$got" = "approved by alice" ] || fail "the guide's example printed '$got', expected approved by alice"
|
|
1219
1334
|
pass "the guide's worker retried its activity, and its approval workflow completed on a signal"
|
|
1220
1335
|
|
|
1221
1336
|
cat > "$work/workflows.py" <<'PYEOF'
|
|
@@ -1390,9 +1505,11 @@ SPARK_MASTER=http://spark-master:8080/metrics/master/prometheus/
|
|
|
1390
1505
|
|
|
1391
1506
|
case "$PROFILE" in
|
|
1392
1507
|
kafka-lite)
|
|
1393
|
-
guide_kafka; connect_plugins
|
|
1508
|
+
guide_kafka; connect_plugins; connect_datagen; getting_started
|
|
1394
1509
|
assert_metrics http://kafka:9404/metrics "${KAFKA_SIDE[@]}"
|
|
1395
|
-
smoke_iceberg_sink
|
|
1510
|
+
smoke_iceberg_sink
|
|
1511
|
+
run_example kafka 'Processed a total of 5 messages'
|
|
1512
|
+
run_example kafka-connect 'Processed a total of 3 messages' ;;
|
|
1396
1513
|
kafka-full)
|
|
1397
1514
|
smoke_kafka kafka-1; connect_plugins
|
|
1398
1515
|
assert_metrics http://kafka-{1,2,3}:9404/metrics "${KAFKA_SIDE[@]}" ;;
|
|
@@ -1400,31 +1517,42 @@ case "$PROFILE" in
|
|
|
1400
1517
|
smoke_flink
|
|
1401
1518
|
smoke_flink_kafka_jdbc
|
|
1402
1519
|
smoke_flink_http
|
|
1403
|
-
assert_metrics http://jobmanager:9249/metrics http://taskmanager-a:9249/metrics
|
|
1520
|
+
assert_metrics http://jobmanager:9249/metrics http://taskmanager-a:9249/metrics
|
|
1521
|
+
run_example flink-sql 'b +\| +1 +\| +7\.5'
|
|
1522
|
+
run_example rolling-windows '20 +\| +20'
|
|
1523
|
+
run_example iceberg-ingestion 'records written by the Kafka Connect sink' ;;
|
|
1404
1524
|
flink-full)
|
|
1405
1525
|
smoke_flink
|
|
1406
1526
|
assert_metrics http://jobmanager:9249/metrics http://taskmanager-{a,b,c}:9249/metrics ;;
|
|
1407
1527
|
spark-lite)
|
|
1408
1528
|
smoke_spark
|
|
1409
|
-
assert_metrics "$SPARK_MASTER" http://spark-worker-1:8081/metrics/prometheus/
|
|
1529
|
+
assert_metrics "$SPARK_MASTER" http://spark-worker-1:8081/metrics/prometheus/
|
|
1530
|
+
run_example spark '"n":3' ;;
|
|
1410
1531
|
spark-full)
|
|
1411
1532
|
smoke_spark
|
|
1412
1533
|
assert_metrics "$SPARK_MASTER" http://spark-worker-{1,2,3}:8081/metrics/prometheus/ ;;
|
|
1413
|
-
ch-lite) smoke_ch_lite ;;
|
|
1534
|
+
ch-lite) smoke_ch_lite; run_example clickhouse 'ch-12 holds 50 rows' ;;
|
|
1414
1535
|
ch-full) smoke_ch_full ;;
|
|
1415
|
-
trino) smoke_trino ;;
|
|
1536
|
+
trino) smoke_trino; how_it_works_trino ;;
|
|
1416
1537
|
deps) smoke_deps ;;
|
|
1417
1538
|
postgres|storage|catalog|valkey) smoke_infra ;;
|
|
1418
|
-
metabase) smoke_metabase ;;
|
|
1419
|
-
airflow) smoke_airflow ;;
|
|
1539
|
+
metabase) smoke_metabase; run_example trino-metabase '"status":"ok"' ;;
|
|
1540
|
+
airflow) smoke_airflow; run_example airflow-dags 'success'; how_it_works_airflow ;;
|
|
1420
1541
|
temporal) smoke_temporal ;;
|
|
1421
|
-
mlflow)
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1542
|
+
mlflow)
|
|
1543
|
+
smoke_mlflow
|
|
1544
|
+
how_it_works_edit mlops-deps .odctl/.env mlflow '^0\.9\.0$'
|
|
1545
|
+
how_it_works_edit timezone .odctl/.env mlflow '^AE[DS]T$'
|
|
1546
|
+
run_example mlflow '"predictions": \[0, 0\]' "$(mlflow_runner) bash" ;;
|
|
1547
|
+
lineage) smoke_lineage; run_example lineage 'COMPLETED' ;;
|
|
1548
|
+
telemetry) smoke_telemetry; run_example telemetry '"42"' ;;
|
|
1549
|
+
metadata) smoke_metadata; run_example openmetadata 'removed demo_pg' ;;
|
|
1550
|
+
fluss) smoke_fluss; run_example fluss '\| +3 +\| +c +\|' ;;
|
|
1551
|
+
feast)
|
|
1552
|
+
smoke_feast
|
|
1553
|
+
run_example feast-iceberg '0.8999999761581421' "env PATH=${TMPDIR:-/tmp}/odctl-feast-smoke/.venv/bin:$PATH bash"
|
|
1554
|
+
rm -rf "${TMPDIR:-/tmp}/odctl-feast-smoke" ;;
|
|
1555
|
+
evidently) smoke_evidently; run_example evidently-reports 'report pushed to project' "$(evidently_runner)" ;;
|
|
1428
1556
|
*)
|
|
1429
1557
|
echo "ℹ️ $PROFILE: no functional assertion defined, checking containers only"
|
|
1430
1558
|
[ "$(docker ps -q | wc -l)" -ge 1 ] || fail "no containers running"
|
|
@@ -8,14 +8,17 @@ A CLI that starts a local open data stack with Docker Compose profiles: Kafka, F
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `src/odctl/main.py` | the Typer CLI (`list`, `explain`, `ps`, `info`, `init`, `pull`, `up`, `down`, `logs`, `restart`, `recreate`) |
|
|
10
10
|
| `src/odctl/registry.py`, `resources/registry.yml` | profiles, the services in each, dependencies, ports and usage text |
|
|
11
|
-
| `src/odctl/planner.py`, `docker.py`, `workspace.py`, `config.py`, `ui.py` | startup order, Docker Compose calls, the `.odctl` workspace, settings, terminal output |
|
|
11
|
+
| `src/odctl/planner.py`, `docker.py`, `projects.py`, `logs.py`, `workspace.py`, `config.py`, `ui.py` | startup order, Docker Compose calls, `restart` and `recreate`, and `logs`, each with one Compose project per file as `up` runs them, the `.odctl` workspace, settings, terminal output |
|
|
12
12
|
| `src/odctl/resources/compose-*.yml` | one compose file per area (kafka, flink, spark, store, analytics, mlops, orch, obsv, metadata, infra, deps) |
|
|
13
13
|
| `src/odctl/resources/docker/` | odctl's own images (postgres, mlflow, airflow, spark), published to `ghcr.io/jaehyeon-kim/odctl/<name>:<version>` |
|
|
14
14
|
| `src/odctl/resources/deps/` | the `deps` image: connector and engine jars built or downloaded into the `odctl-shared-deps` volume; versions in `versions.env` |
|
|
15
|
-
| `scripts/gen_pages.py` | builds a page per technology and the CLI reference at docs build time, from `registry.yml`, the compose files and `--help`; run directly, it writes `skills/odctl/SKILL.md`, which a test keeps in step |
|
|
16
|
-
| `scripts/
|
|
17
|
-
|
|
|
18
|
-
| `docs/
|
|
15
|
+
| `scripts/gen_pages.py` | builds a page per technology and the CLI reference at docs build time, from `registry.yml`, the compose files and `--help`; run directly, it writes `skills/odctl/SKILL.md`, with every command, option and `.env` key, which a test keeps in step |
|
|
16
|
+
| `scripts/local_links.py`, `scripts/llmstxt_sections.py`, `scripts/release_links.py` | MkDocs hooks: clickable local addresses, the `llms.txt` sections from the nav, and links to files on `main` pointed at the release tag `v<version>` |
|
|
17
|
+
| `scripts/diagrams.py`, `docs/assets/diagrams/*.drawio` | exports each draw.io diagram to the PNG, or GIF for steps, that the docs use, and writes a still PNG of each GIF's last frame beside it; `_legend.drawio` holds the shared shapes; `tests/test_diagrams.py` checks the exports and the stills |
|
|
18
|
+
| `docs/images/` | screenshots of the services' web UIs and the terminal |
|
|
19
|
+
| `image/` | the README's diagram and recording, with the recording's still; they stay here because the PyPI page of every released version links them on `main` |
|
|
20
|
+
| `.github/scripts/smoke.sh` | the e2e assertions per profile group, which run the guide snippets and each guide's Runnable Example; `select-profiles.py` chooses the groups |
|
|
21
|
+
| `docs/snippets/` | the code each guide, Getting Started and How It Works show, one folder per page |
|
|
19
22
|
| `skills/odctl/SKILL.md` | the odctl skill for AI assistants, generated by `scripts/gen_pages.py` |
|
|
20
23
|
| `docs/` | MkDocs Material site, versioned with mike; `docs/about/` holds the public Roadmap and the Changelog |
|
|
21
24
|
| `scripts/release_notes.py` | prints one version's Changelog section, the text of its GitHub release |
|
|
@@ -40,7 +43,9 @@ Run the unit suite, `pytest tests/`, in full once before each commit, never afte
|
|
|
40
43
|
- **One profile per tool**, with `-lite` and `-full` variants where a tool has a small and a clustered form (`kafka-lite`, `kafka-full`).
|
|
41
44
|
- **Fixed container names.** Every service has a `container_name`, so `odctl up` cannot scale a service; several copies are separate named services (Flink's `taskmanager-a` and `taskmanager-b`).
|
|
42
45
|
- **Docs pages that list versions or ports are generated** by `scripts/gen_pages.py`; do not copy those values into hand-written guides.
|
|
43
|
-
- **Guide code lives in `docs/snippets/<guide>/`**, one section per step (`# --8<-- [start:name]`). A
|
|
46
|
+
- **Guide code lives in `docs/snippets/<guide>/`**, one section per step (`# --8<-- [start:name]`), and Getting Started and How It Works follow the same rule. A block labelled `title="path:section"` or `title="path"` is a copy of that section or file, checked by `tests/test_docs.py`; use copies, not `--8<--` includes, so the Markdown reads correctly on GitHub. Every guide ends with a Runnable Example, `example.sh` or `example.py` in its folder. `smoke.sh` runs the same sections and examples, and `select-profiles.py` maps each snippet folder to the e2e groups that run it.
|
|
47
|
+
- **A feature merges only with its docs.** Each command, option, profile and `.env` key needs a tested example on a guide, Getting Started or How It Works, a reference entry (the CLI reference, a profile page or How It Works), and a place in the skill; a command also needs a line in the README's Commands section. `test_every_feature_has_its_docs` checks this, and its short list of exceptions gives a reason for each.
|
|
48
|
+
- **Each guide says which pages it builds on**, the output of every step, and where a value it quotes is set. A page has at most 10 `##` headings and 15 in all, and headings use title case, keeping product names such as `odctl` as they are.
|
|
44
49
|
- **e2e runs only on tags and manual dispatch**, because a PR's images for an unreleased version do not exist yet. A PR gets lint, unit tests, image builds when image inputs change, and the strict docs build.
|
|
45
50
|
- **Renovate PRs** open on Mondays, image updates grouped by compose file. Show any Renovate config change to the owner before making it.
|
|
46
51
|
- **Writing.** Plain English for non-native readers. No em or en dashes. Headings do not start with "The". Markdown paragraphs are one line each, never hard-wrapped.
|