odctl 1.2.1__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {odctl-1.2.1 → odctl-1.3.0}/.github/renovate.json +5 -0
  2. {odctl-1.2.1 → odctl-1.3.0}/.github/scripts/select-profiles.py +1 -1
  3. {odctl-1.2.1 → odctl-1.3.0}/.github/scripts/smoke.sh +243 -20
  4. {odctl-1.2.1 → odctl-1.3.0}/.github/workflows/ci.yml +1 -1
  5. {odctl-1.2.1 → odctl-1.3.0}/.github/workflows/e2e-test.yml +1 -1
  6. {odctl-1.2.1 → odctl-1.3.0}/.github/workflows/pipeline.yml +3 -3
  7. {odctl-1.2.1 → odctl-1.3.0}/.github/workflows/publish.yml +1 -1
  8. odctl-1.3.0/CLAUDE.md +20 -0
  9. odctl-1.3.0/CONTRIBUTING.md +99 -0
  10. {odctl-1.2.1 → odctl-1.3.0}/PKG-INFO +4 -25
  11. {odctl-1.2.1 → odctl-1.3.0}/README.md +3 -24
  12. {odctl-1.2.1 → odctl-1.3.0}/pyproject.toml +1 -1
  13. {odctl-1.2.1 → odctl-1.3.0}/scripts/gen_pages.py +2 -0
  14. {odctl-1.2.1 → odctl-1.3.0}/skills/odctl/SKILL.md +25 -16
  15. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-analytics.yml +2 -2
  16. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-infra.yml +1 -1
  17. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-kafka.yml +1 -1
  18. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-metadata.yml +79 -9
  19. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-obsv.yml +1 -1
  20. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/build.gradle +1 -1
  21. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/fetch_standalone_plugins.sh +1 -1
  22. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/clickhouse.json +1 -1
  23. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/flink.json +1 -1
  24. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/fluss.json +1 -1
  25. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/kafka-connect.json +1 -1
  26. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/kafka.json +1 -1
  27. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/karapace.json +1 -1
  28. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/marquez.json +1 -1
  29. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/openmetadata.json +1 -1
  30. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/seaweedfs.json +1 -1
  31. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/temporal.json +1 -1
  32. odctl-1.3.0/src/odctl/resources/openmetadata/autopilot_init.py +158 -0
  33. odctl-1.3.0/src/odctl/resources/openmetadata/connector_sync.py +178 -0
  34. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/registry.yml +10 -4
  35. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/workspace.py +14 -0
  36. odctl-1.3.0/tests/test_autopilot_init.py +117 -0
  37. odctl-1.3.0/tests/test_connector_sync.py +183 -0
  38. odctl-1.3.0/tests/test_metadata_profile.py +119 -0
  39. {odctl-1.2.1 → odctl-1.3.0}/tests/test_workspace.py +4 -0
  40. {odctl-1.2.1 → odctl-1.3.0}/uv.lock +1 -1
  41. odctl-1.2.1/CLAUDE.md +0 -64
  42. {odctl-1.2.1 → odctl-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {odctl-1.2.1 → odctl-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {odctl-1.2.1 → odctl-1.3.0}/.github/scripts/spark_avro_producer.py +0 -0
  45. {odctl-1.2.1 → odctl-1.3.0}/.github/workflows/build-deps.yml +0 -0
  46. {odctl-1.2.1 → odctl-1.3.0}/.github/workflows/build-platform-images.yml +0 -0
  47. {odctl-1.2.1 → odctl-1.3.0}/.gitignore +0 -0
  48. {odctl-1.2.1 → odctl-1.3.0}/.pre-commit-config.yaml +0 -0
  49. {odctl-1.2.1 → odctl-1.3.0}/.vscode/settings.json +0 -0
  50. {odctl-1.2.1 → odctl-1.3.0}/LICENSE +0 -0
  51. {odctl-1.2.1 → odctl-1.3.0}/image/demo.gif +0 -0
  52. {odctl-1.2.1 → odctl-1.3.0}/image/demo.png +0 -0
  53. {odctl-1.2.1 → odctl-1.3.0}/image/diagram.drawio +0 -0
  54. {odctl-1.2.1 → odctl-1.3.0}/image/diagram.png +0 -0
  55. {odctl-1.2.1 → odctl-1.3.0}/mkdocs.yml +0 -0
  56. {odctl-1.2.1 → odctl-1.3.0}/scripts/diagrams.py +0 -0
  57. {odctl-1.2.1 → odctl-1.3.0}/scripts/llmstxt_sections.py +0 -0
  58. {odctl-1.2.1 → odctl-1.3.0}/scripts/local_links.py +0 -0
  59. {odctl-1.2.1 → odctl-1.3.0}/scripts/release_links.py +0 -0
  60. {odctl-1.2.1 → odctl-1.3.0}/scripts/release_notes.py +0 -0
  61. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/__init__.py +0 -0
  62. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/config.py +0 -0
  63. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/docker.py +0 -0
  64. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/logs.py +0 -0
  65. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/main.py +0 -0
  66. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/planner.py +0 -0
  67. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/projects.py +0 -0
  68. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/registry.py +0 -0
  69. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/airflow/odctl_log_config.py +0 -0
  70. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/config.xml +0 -0
  71. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/init.sql +0 -0
  72. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/keeper_config.xml +0 -0
  73. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-11-lite.xml +0 -0
  74. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-11.xml +0 -0
  75. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-12-lite.xml +0 -0
  76. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-12.xml +0 -0
  77. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-21.xml +0 -0
  78. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/macros-22.xml +0 -0
  79. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/clickhouse/users.xml +0 -0
  80. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-deps.yml +0 -0
  81. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-flink.yml +0 -0
  82. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-mlops.yml +0 -0
  83. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-orch.yml +0 -0
  84. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-spark.yml +0 -0
  85. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/compose-store.yml +0 -0
  86. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/Dockerfile +0 -0
  87. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/build_connectors.sh +0 -0
  88. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/fetch_flink_dependencies.sh +0 -0
  89. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/fetch_spark_dependencies.sh +0 -0
  90. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/deps/versions.env +0 -0
  91. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/docker/airflow/Dockerfile +0 -0
  92. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/docker/mlflow/Dockerfile +0 -0
  93. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/docker/postgres/Dockerfile +0 -0
  94. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/docker/spark/Dockerfile +0 -0
  95. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/evidently/config.yaml +0 -0
  96. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/feast/feature_store.yaml +0 -0
  97. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/flink/flink-conf.yml +0 -0
  98. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/airflow.json +0 -0
  99. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/feast.json +0 -0
  100. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/iceberg-catalog.json +0 -0
  101. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/metabase.json +0 -0
  102. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/mlflow.json +0 -0
  103. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/postgresql.json +0 -0
  104. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/spark.json +0 -0
  105. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/trino.json +0 -0
  106. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/dashboards/valkey.json +0 -0
  107. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/odctl-dashboards.yaml +0 -0
  108. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/grafana/sources.yml +0 -0
  109. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/jmx/jmx-exporter.yml +0 -0
  110. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/marquez/marquez.yml +0 -0
  111. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/otelcol/odctl-receivers.yaml +0 -0
  112. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/postgres/01-init-databases.sh +0 -0
  113. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/prometheus/prometheus.yml +0 -0
  114. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/seaweed/s3.json +0 -0
  115. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/spark/log4j2.properties +0 -0
  116. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/spark/metrics.properties +0 -0
  117. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/spark/spark-defaults.conf +0 -0
  118. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/access-control.properties +0 -0
  119. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/catalog/clickhouse.properties +0 -0
  120. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/catalog/iceberg.properties +0 -0
  121. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/catalog/kafka.properties +0 -0
  122. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/catalog/postgres.properties +0 -0
  123. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/catalog/valkey.properties +0 -0
  124. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/group-provider.properties +0 -0
  125. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/groups.txt +0 -0
  126. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/resources/trino/rules.json +0 -0
  127. {odctl-1.2.1 → odctl-1.3.0}/src/odctl/ui.py +0 -0
  128. {odctl-1.2.1 → odctl-1.3.0}/tests/conftest.py +0 -0
  129. {odctl-1.2.1 → odctl-1.3.0}/tests/docs_helpers.py +0 -0
  130. {odctl-1.2.1 → odctl-1.3.0}/tests/test_airflow_profile.py +0 -0
  131. {odctl-1.2.1 → odctl-1.3.0}/tests/test_cli.py +0 -0
  132. {odctl-1.2.1 → odctl-1.3.0}/tests/test_cli_extended.py +0 -0
  133. {odctl-1.2.1 → odctl-1.3.0}/tests/test_compose_profiles.py +0 -0
  134. {odctl-1.2.1 → odctl-1.3.0}/tests/test_config.py +0 -0
  135. {odctl-1.2.1 → odctl-1.3.0}/tests/test_confirmed_pages.py +0 -0
  136. {odctl-1.2.1 → odctl-1.3.0}/tests/test_diagrams.py +0 -0
  137. {odctl-1.2.1 → odctl-1.3.0}/tests/test_docker.py +0 -0
  138. {odctl-1.2.1 → odctl-1.3.0}/tests/test_docs.py +0 -0
  139. {odctl-1.2.1 → odctl-1.3.0}/tests/test_evidently_profile.py +0 -0
  140. {odctl-1.2.1 → odctl-1.3.0}/tests/test_feast_profile.py +0 -0
  141. {odctl-1.2.1 → odctl-1.3.0}/tests/test_feature_docs.py +0 -0
  142. {odctl-1.2.1 → odctl-1.3.0}/tests/test_grafana_dashboards.py +0 -0
  143. {odctl-1.2.1 → odctl-1.3.0}/tests/test_logs.py +0 -0
  144. {odctl-1.2.1 → odctl-1.3.0}/tests/test_metrics_collector.py +0 -0
  145. {odctl-1.2.1 → odctl-1.3.0}/tests/test_metrics_jvm.py +0 -0
  146. {odctl-1.2.1 → odctl-1.3.0}/tests/test_metrics_python_services.py +0 -0
  147. {odctl-1.2.1 → odctl-1.3.0}/tests/test_planner.py +0 -0
  148. {odctl-1.2.1 → odctl-1.3.0}/tests/test_postgres_image.py +0 -0
  149. {odctl-1.2.1 → odctl-1.3.0}/tests/test_projects.py +0 -0
  150. {odctl-1.2.1 → odctl-1.3.0}/tests/test_registry.py +0 -0
  151. {odctl-1.2.1 → odctl-1.3.0}/tests/test_release_links.py +0 -0
  152. {odctl-1.2.1 → odctl-1.3.0}/tests/test_release_notes.py +0 -0
  153. {odctl-1.2.1 → odctl-1.3.0}/tests/test_telemetry_profile.py +0 -0
  154. {odctl-1.2.1 → odctl-1.3.0}/tests/test_temporal_profile.py +0 -0
@@ -77,6 +77,11 @@
77
77
  }
78
78
  ],
79
79
  "packageRules": [
80
+ {
81
+ "description": "Kafka stays below 4.4: Kafka 4.4.0 moved AppInfoParser to org.apache.kafka.common.utils.internals, and the Iceberg Kafka Connect sink still calls the old class, so its task fails on a 4.4 Connect worker. Raise this when an Iceberg release no longer uses it. See issue #161.",
82
+ "matchPackageNames": ["apache/kafka"],
83
+ "allowedVersions": "<4.4"
84
+ },
80
85
  {
81
86
  "description": "Flink stays on 2.2: it is the newest minor with Kafka and JDBC connector releases. Raise this when both connectors publish a build for the next minor, and move FLINK_MINOR in versions.env with it. See issue #9.",
82
87
  "matchPackageNames": ["flink", "org.apache.flink:{/,}**"],
@@ -85,7 +85,7 @@ ALSO_RUNS = {
85
85
  SNIPPET_PREFIX = "docs/snippets/"
86
86
  SNIPPET_GROUPS = {
87
87
  "getting-started": ["kafka-lite kafka-full"],
88
- "how-it-works": ["storage", "catalog", "trino", "airflow"],
88
+ "how-it-works": ["storage", "catalog", "trino", "airflow", "metadata"],
89
89
  "kafka": ["kafka-lite kafka-full"],
90
90
  "kafka-connect": ["kafka-lite kafka-full"],
91
91
  "iceberg-ingestion": ["kafka-lite kafka-full", "flink-lite flink-full"],
@@ -1094,7 +1094,7 @@ smoke_fluss() {
1094
1094
  pass "Flink wrote 3 rows to a Fluss table and read them back"
1095
1095
  }
1096
1096
 
1097
- # OpenMetadata: the version endpoint answers through every failure the 2.0.1
1097
+ # OpenMetadata: the version endpoint answers through every failure an
1098
1098
  # upgrade could cause. The ingestion container runs its own Airflow 3, whose
1099
1099
  # health path moved to /api/v2/monitor/health, and a stale image or a failed
1100
1100
  # migration both leave a server that still reports a version. Drive the whole
@@ -1110,14 +1110,23 @@ smoke_metadata() {
1110
1110
  pass "schema migration completed"
1111
1111
  assert_metrics http://openmetadata-server:8586/prometheus
1112
1112
 
1113
- # The principal domain odctl sets is open-data.local, not the upstream
1113
+ # The principal domain odctl sets is odctl.io, not the upstream
1114
1114
  # default. A wrong domain fails with a misleading invalid password error.
1115
1115
  local token
1116
1116
  token=$(om_step login 'echo "$TOKEN"' 2>/dev/null | tail -1)
1117
- [ -n "$token" ] || fail "admin@open-data.local could not log in"
1117
+ [ -n "$token" ] || fail "admin@odctl.io could not log in"
1118
1118
  pass "admin logged in and received a JWT"
1119
1119
  local auth="Authorization: Bearer $token"
1120
1120
 
1121
+ # user is the second admin. Its entry in AUTHORIZER_ADMIN_PRINCIPALS,
1122
+ # user:password, sets its first password.
1123
+ local code
1124
+ code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
1125
+ -d "{\"email\":\"user@odctl.io\",\"password\":\"$(printf password | base64)\"}" \
1126
+ http://127.0.0.1:8585/api/v1/users/login)
1127
+ [ "$code" = "200" ] || fail "user@odctl.io could not log in with the password password (HTTP $code)"
1128
+ pass "user@odctl.io logged in with the password password"
1129
+
1121
1130
  # Airflow 3 moved this path. The old /health returns 404 with a message saying so.
1122
1131
  retry 60 5 http_ok "http://127.0.0.1:8087/api/v2/monitor/health" \
1123
1132
  || fail "ingestion Airflow never answered on 8087"
@@ -1126,6 +1135,20 @@ smoke_metadata() {
1126
1135
  || fail "the server cannot reach the ingestion container, so no pipeline can deploy"
1127
1136
  pass "ingestion Airflow healthy and reachable from the server"
1128
1137
 
1138
+ # AIRFLOW_ADMIN_USER and AIRFLOW_ADMIN_PASSWORD set this login. A new token
1139
+ # answers 201, and a wrong login 401.
1140
+ code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' \
1141
+ -d '{"username":"user","password":"password"}' http://127.0.0.1:8087/auth/token)
1142
+ [ "$code" = "201" ] || fail "the ingestion Airflow refused user / password (HTTP $code)"
1143
+ pass "ingestion Airflow accepted user / password"
1144
+
1145
+ # openmetadata-init sets AutoPilot from METADATA_AUTOPILOT, false by default.
1146
+ retry 24 5 om_autopilot_is "$token" False \
1147
+ || fail "AutoPilot does not report active: false after the start"
1148
+ [ "$(docker inspect -f '{{.State.ExitCode}}' openmetadata-init)" = "0" ] \
1149
+ || fail "openmetadata-init did not exit with code 0"
1150
+ pass "AutoPilot reports active: false and openmetadata-init exited 0"
1151
+
1129
1152
  # A real ingestion, with the guide's steps. Postgres is already running, so it
1130
1153
  # is its own source.
1131
1154
  local svc
@@ -1142,23 +1165,11 @@ smoke_metadata() {
1142
1165
  # Airflow's dag_processor has to parse the new file before it can be
1143
1166
  # triggered, so the guide's trigger can come too early. Trigger again here.
1144
1167
  if ! grep -q 'has been triggered' <<<"$out"; then
1145
- local triggered=no
1146
- for _ in $(seq 1 24); do
1147
- if curl -fsS --max-time 90 -X POST -H "$auth" \
1148
- "http://127.0.0.1:8585/api/v1/services/ingestionPipelines/trigger/$pipe" 2>/dev/null \
1149
- | grep -q 'has been triggered'; then triggered=yes; break; fi
1150
- sleep 5
1151
- done
1152
- [ "$triggered" = "yes" ] || fail "the pipeline never triggered"
1168
+ om_trigger "$token" "$pipe" || fail "the pipeline never triggered"
1153
1169
  fi
1154
1170
 
1155
- local state=""
1156
- for _ in $(seq 1 30); do
1157
- state=$(TOKEN="$token" om_step status 2>/dev/null \
1158
- | python3 -c 'import json,sys; s=json.load(sys.stdin).get("pipelineStatuses") or []; print(s[0]["pipelineState"] if s else "")' 2>/dev/null)
1159
- case "$state" in success|failed|partialSuccess) break ;; esac
1160
- sleep 10
1161
- done
1171
+ local state
1172
+ state=$(om_run_state "$token" om_step "")
1162
1173
  [ "$state" = "success" ] || fail "ingestion run finished in state ${state:-none}, not success"
1163
1174
  pass "ingestion run reached success"
1164
1175
 
@@ -1191,8 +1202,214 @@ smoke_metadata() {
1191
1202
  done
1192
1203
  pass "Context Center pages and memories both answer"
1193
1204
 
1205
+ smoke_metadata_connector "$token"
1206
+ smoke_metadata_autopilot "$token"
1207
+
1194
1208
  TOKEN="$token" om_step remove >/dev/null 2>&1 || fail "the guide's remove step failed"
1195
- pass "the guide's remove step deleted the service and its pipeline"
1209
+ code=$(curl -s -o /dev/null -w '%{http_code}' -H "$auth" \
1210
+ http://127.0.0.1:8585/api/v1/services/databaseServices/name/demo_connector)
1211
+ [ "$code" = "404" ] || fail "the guide's remove step left the demo_connector service (HTTP $code)"
1212
+ retry 12 5 bash -c '! docker exec openmetadata-ingestion test -e /opt/airflow/connectors/demo_connector.py' \
1213
+ || fail "the ingestion container kept the connector after the remove step deleted it from S3"
1214
+ pass "the guide's remove step deleted both services, their pipelines and the uploaded connector"
1215
+ }
1216
+
1217
+ # Trigger an ingestion pipeline until the server accepts it: Airflow has to parse
1218
+ # a new DAG file first. Usage: om_trigger <token> <pipeline id>
1219
+ om_trigger() {
1220
+ local _
1221
+ for _ in $(seq 1 24); do
1222
+ if curl -fsS --max-time 90 -X POST -H "Authorization: Bearer $1" \
1223
+ "http://127.0.0.1:8585/api/v1/services/ingestionPipelines/trigger/$2" 2>/dev/null \
1224
+ | grep -q 'has been triggered'; then return 0; fi
1225
+ sleep 5
1226
+ done
1227
+ return 1
1228
+ }
1229
+
1230
+ # The latest run of the pipeline a guide's status step reads, as "<run id> <state>",
1231
+ # or nothing when there is no run yet. The step is the guide's own: om_step for
1232
+ # demo_pg, om_connector_step for the custom connector.
1233
+ # Usage: om_latest_run <token> <om_step|om_connector_step>
1234
+ om_latest_run() {
1235
+ TOKEN="$1" "$2" status 2>/dev/null | python3 -c '
1236
+ import json, sys
1237
+ runs = json.load(sys.stdin).get("pipelineStatuses") or []
1238
+ if runs:
1239
+ print(runs[0]["runId"], runs[0]["pipelineState"])' 2>/dev/null
1240
+ }
1241
+
1242
+ # Wait for the first run whose id is not <previous run id> to finish, and print its
1243
+ # state. Usage: om_run_state <token> <om_step|om_connector_step> <previous run id>
1244
+ om_run_state() {
1245
+ local _ run state
1246
+ for _ in $(seq 1 30); do
1247
+ read -r run state < <(om_latest_run "$1" "$2")
1248
+ if [ -n "${run:-}" ] && [ "$run" != "$3" ]; then
1249
+ case "$state" in success|failed|partialSuccess) echo "$state"; return ;; esac
1250
+ fi
1251
+ sleep 10
1252
+ done
1253
+ echo ""
1254
+ }
1255
+
1256
+ # Whether the guide's autopilot step prints <value>. Usage: om_autopilot_is <token> <True|False>
1257
+ om_autopilot_is() {
1258
+ [ "$(TOKEN="$1" om_step autopilot 2>/dev/null | tail -1)" = "$2" ]
1259
+ }
1260
+
1261
+ # Add a service the way the UI does: create it, then trigger AutoPilotApplication with
1262
+ # the service's entityLink. Usage: om_add_service_like_the_ui <token> <service name>
1263
+ om_add_service_like_the_ui() {
1264
+ local auth="Authorization: Bearer $1"
1265
+ curl -fsS -X PUT -H "$auth" -H 'Content-Type: application/json' \
1266
+ -d "{\"name\":\"$2\",\"serviceType\":\"CustomDatabase\",\"connection\":{\"config\":{\"type\":\"CustomDatabase\",\"sourcePythonClass\":\"demo_connector.DemoSource\"}}}" \
1267
+ http://127.0.0.1:8585/api/v1/services/databaseServices >/dev/null || fail "could not create the $2 service"
1268
+ curl -fsS -X POST -H "$auth" -H 'Content-Type: application/json' \
1269
+ -d "{\"entityLink\":\"<#E::databaseService::$2>\"}" \
1270
+ http://127.0.0.1:8585/api/v1/apps/trigger/AutoPilotApplication >/dev/null \
1271
+ || fail "the server refused the AutoPilot trigger the UI sends"
1272
+ }
1273
+
1274
+ # The number of agents a service has. Usage: om_agent_count <token> <service name>
1275
+ om_agent_count() {
1276
+ curl -fsS -H "Authorization: Bearer $1" "http://127.0.0.1:8585/api/v1/services/ingestionPipelines?service=$2" \
1277
+ | python3 -c 'import json,sys; print(json.load(sys.stdin)["paging"]["total"])'
1278
+ }
1279
+
1280
+ om_delete_service() {
1281
+ curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $1" \
1282
+ "http://127.0.0.1:8585/api/v1/services/databaseServices/name/$2?hardDelete=true&recursive=true"
1283
+ }
1284
+
1285
+ # With AutoPilot off, a service added the way the UI adds one gets no agent. The
1286
+ # check waits 60 seconds, as long as the on-case below allows AutoPilot to create its agent.
1287
+ smoke_metadata_autopilot() {
1288
+ local agents
1289
+ om_add_service_like_the_ui "$1" demo_ui
1290
+ sleep 60
1291
+ agents=$(om_agent_count "$1" demo_ui)
1292
+ om_delete_service "$1" demo_ui
1293
+ [ "$agents" = "0" ] || fail "a service added the way the UI adds one got $agents agents with AutoPilot off"
1294
+ pass "a service added the way the UI adds one, with its AutoPilot trigger, got no agent"
1295
+ }
1296
+
1297
+ # The same two calls with AutoPilot on create at least one agent. It proves
1298
+ # METADATA_AUTOPILOT=true works and that the off-case above could have failed.
1299
+ smoke_metadata_autopilot_on() {
1300
+ local agents=0 _
1301
+ om_add_service_like_the_ui "$1" demo_ui_on
1302
+ for _ in $(seq 1 12); do
1303
+ agents=$(om_agent_count "$1" demo_ui_on)
1304
+ [ "${agents:-0}" -ge 1 ] && break
1305
+ sleep 5
1306
+ done
1307
+ om_delete_service "$1" demo_ui_on
1308
+ [ "${agents:-0}" -ge 1 ] || fail "a service added the way the UI adds one got no agent with AutoPilot on"
1309
+ pass "with METADATA_AUTOPILOT true, a service added the way the UI adds one got $agents agent(s)"
1310
+ }
1311
+
1312
+ # The How It Works example turns AutoPilot on and recreates metadata. While it is on,
1313
+ # check the UI's calls create an agent, then put the key back and check it is off.
1314
+ how_it_works_autopilot() {
1315
+ local out
1316
+ out=$(run_snippet how-it-works/commands.sh metadata-autopilot 2>&1) || { echo "$out" | tail -20; fail "the metadata-autopilot example failed"; }
1317
+ grep -qE -- 'AutoPilot active: true' <<<"$out" || { echo "$out" | tail -20; fail "the metadata-autopilot example printed no 'AutoPilot active: true'"; }
1318
+ pass "How It Works' metadata-autopilot example printed 'AutoPilot active: true'"
1319
+ smoke_metadata_autopilot_on "$(om_token)"
1320
+ mv .odctl/.env.bak .odctl/.env || fail "the metadata-autopilot example left no .env.bak"
1321
+ odctl recreate metadata >/dev/null 2>&1 || fail "could not recreate metadata after the metadata-autopilot example"
1322
+ retry 24 5 om_autopilot_is "$(om_token)" False \
1323
+ || fail "AutoPilot is not off again after the key was put back"
1324
+ pass "AutoPilot is off again after the key was put back"
1325
+ }
1326
+
1327
+ # Run a connector section until its output has <text>. Usage: om_connector_says <section> <text>
1328
+ om_connector_says() {
1329
+ om_connector_step "$1" 2>/dev/null | grep -qF "$2"
1330
+ }
1331
+
1332
+ # The guide's custom connector steps, run from the folder that holds the file, as
1333
+ # the guide tells the reader. Usage: om_connector_step <section> [extra commands]
1334
+ om_connector_step() {
1335
+ local code
1336
+ code=$(snippet openmetadata/connector/commands.sh "$1")
1337
+ (cd "$CONNECTOR_DIR" && bash -o pipefail -c 'AUTH="Authorization: Bearer ${TOKEN:-}"
1338
+ '"$code"'
1339
+ '"${2:-}")
1340
+ }
1341
+
1342
+ # A custom connector uploaded to s3://openmetadata/connectors is importable in the
1343
+ # ingestion container, and a service and an agent made through the API run it.
1344
+ # Then a changed connector is used by the next run with no restart, which is why
1345
+ # nothing here restarts the container: its start time must not move.
1346
+ smoke_metadata_connector() {
1347
+ local token="$1" auth="Authorization: Bearer $1" started svc pipe out run state agents
1348
+ CONNECTOR_DIR=$(mktemp -d)
1349
+ cp "$SNIPPETS/openmetadata/connector/demo_connector.py" "$CONNECTOR_DIR/"
1350
+ started=$(docker inspect -f '{{.State.StartedAt}}' openmetadata-ingestion)
1351
+
1352
+ om_connector_step upload >/dev/null 2>&1 || fail "could not upload the connector to s3://openmetadata/connectors"
1353
+ retry 12 5 om_connector_says copied demo_connector.py \
1354
+ || fail "the ingestion container never copied the connector from S3"
1355
+ pass "the connector reached /opt/airflow/connectors in the ingestion container"
1356
+
1357
+ svc=$(TOKEN="$token" om_connector_step service 'echo "$SVC"' 2>/dev/null | tail -1)
1358
+ [ -n "$svc" ] || fail "could not create the CustomDatabase service through the API"
1359
+ out=$(TOKEN="$token" SVC="$svc" om_connector_step agent 'echo; echo "$PIPE"' 2>/dev/null)
1360
+ pipe=$(tail -1 <<<"$out")
1361
+ [ -n "$pipe" ] || fail "could not create the custom connector's agent"
1362
+ grep -q 'has been created' <<<"$out" || { echo "$out"; fail "the custom connector's agent did not deploy a DAG"; }
1363
+ grep -q 'has been triggered' <<<"$out" || om_trigger "$token" "$pipe" || fail "the custom connector's agent never triggered"
1364
+ state=$(om_run_state "$token" om_connector_step "")
1365
+ [ "$state" = "success" ] || fail "the custom connector's run finished in state ${state:-none}, not success"
1366
+ out=$(TOKEN="$token" om_connector_step table 2>/dev/null)
1367
+ grep -q '"fullyQualifiedName":"demo_connector.demo_db.public.orders"' <<<"$out" \
1368
+ && grep -q '"name":"id"' <<<"$out" && grep -q '"name":"amount"' <<<"$out" \
1369
+ || fail "the custom connector's table orders, with its columns id and amount, is not in the catalogue"
1370
+ pass "the custom connector's agent ran and its table orders is in the catalogue"
1371
+
1372
+ # A service made through the API starts no AutoPilot agents. The UI does.
1373
+ agents=$(curl -fsS -H "$auth" "http://127.0.0.1:8585/api/v1/services/ingestionPipelines?service=demo_connector" \
1374
+ | python3 -c 'import json,sys; print(json.load(sys.stdin)["paging"]["total"])')
1375
+ [ "$agents" = "1" ] || fail "the demo_connector service has $agents agents, not only its own"
1376
+ pass "the demo_connector service has only its own agent"
1377
+
1378
+ # Change the connector, upload it again and trigger the same agent.
1379
+ read -r run _ < <(om_latest_run "$token" om_connector_step)
1380
+ om_connector_step change >/dev/null 2>&1 || fail "could not change the connector"
1381
+ om_connector_step upload >/dev/null 2>&1 || fail "could not upload the changed connector"
1382
+ retry 12 5 om_connector_says copied-change 1 \
1383
+ || fail "the ingestion container never copied the changed connector"
1384
+ TOKEN="$token" PIPE="$pipe" om_connector_step retrigger 2>/dev/null | grep -q 'has been triggered' \
1385
+ || fail "the custom connector's agent did not trigger again"
1386
+ state=$(om_run_state "$token" om_connector_step "$run")
1387
+ [ "$state" = "success" ] || fail "the second run finished in state ${state:-none}, not success"
1388
+ TOKEN="$token" om_connector_step changed-table 2>/dev/null \
1389
+ | grep -q '"name":"orders_v2"' || fail "the changed connector's table orders_v2 is not in the catalogue"
1390
+ [ "$(docker inspect -f '{{.State.StartedAt}}' openmetadata-ingestion)" = "$started" ] \
1391
+ || fail "the ingestion container restarted while the connector changed"
1392
+ pass "the changed connector was used by the next run, with no restart"
1393
+ rm -rf "$CONNECTOR_DIR"
1394
+ }
1395
+
1396
+ # The guide's runnable example needs demo_connector.py in the folder it runs in.
1397
+ smoke_metadata_example() {
1398
+ local out
1399
+ cp "$SNIPPETS/openmetadata/connector/demo_connector.py" .
1400
+ out=$(bash "$SNIPPETS/openmetadata/example.sh" 2>&1) || { rm -f demo_connector.py; echo "$out" | tail -20; fail "the openmetadata example failed"; }
1401
+ rm -f demo_connector.py
1402
+ local want
1403
+ for want in 'autopilot active: False' 'demo_pg ingestion: success' 'demo_connector ingestion: success' \
1404
+ '^custom table: orders$' 'removed demo_pg and demo_connector'; do
1405
+ grep -qE -- "$want" <<<"$out" || { echo "$out" | tail -20; fail "the openmetadata example printed no '$want'"; }
1406
+ done
1407
+ pass "the openmetadata guide's runnable example ran both services and printed its results"
1408
+ }
1409
+
1410
+ # An admin token, from the guide's login step.
1411
+ om_token() {
1412
+ om_step login 'echo "$TOKEN"' 2>/dev/null | tail -1
1196
1413
  }
1197
1414
 
1198
1415
  # The OpenMetadata guide's steps share shell variables: login sets TOKEN, the
@@ -1546,7 +1763,13 @@ case "$PROFILE" in
1546
1763
  run_example mlflow '"predictions": \[0, 0\]' "$(mlflow_runner) bash" ;;
1547
1764
  lineage) smoke_lineage; run_example lineage 'COMPLETED' ;;
1548
1765
  telemetry) smoke_telemetry; run_example telemetry '"42"' ;;
1549
- metadata) smoke_metadata; run_example openmetadata 'removed demo_pg' ;;
1766
+ metadata)
1767
+ smoke_metadata
1768
+ smoke_metadata_example
1769
+ how_it_works_edit metadata-deps .odctl/.env metadata '^1\.4\.3$'
1770
+ how_it_works_autopilot
1771
+ retry 60 5 http_ok "http://127.0.0.1:8087/api/v2/monitor/health" \
1772
+ || fail "ingestion Airflow did not answer after the package was put back" ;;
1550
1773
  fluss) smoke_fluss; run_example fluss '\| +3 +\| +c +\|' ;;
1551
1774
  feast)
1552
1775
  smoke_feast
@@ -20,7 +20,7 @@ jobs:
20
20
  - name: Install uv
21
21
  # Pinned to a full version on purpose: setup-uv stopped publishing
22
22
  # moving major tags at v8, so astral-sh/setup-uv@v10 does not exist.
23
- uses: astral-sh/setup-uv@v10.2.0
23
+ uses: astral-sh/setup-uv@v10.3.2
24
24
  with:
25
25
  enable-cache: true
26
26
  # The default key holds the runner's Python, not the matrix one, so
@@ -67,7 +67,7 @@ jobs:
67
67
  - name: Install uv
68
68
  # Pinned to a full version on purpose: setup-uv stopped publishing
69
69
  # moving major tags at v8, so astral-sh/setup-uv@v10 does not exist.
70
- uses: astral-sh/setup-uv@v10.2.0
70
+ uses: astral-sh/setup-uv@v10.3.2
71
71
 
72
72
  - name: Set up Python
73
73
  run: uv python install 3.10
@@ -48,7 +48,7 @@ jobs:
48
48
  # A tag runs every profile. A pull request runs only the groups its
49
49
  # changed files affect, because the full matrix pulls roughly 10 GB per
50
50
  # entry and paying that on every push is not worth the signal.
51
- - uses: astral-sh/setup-uv@v10.2.0
51
+ - uses: astral-sh/setup-uv@v10.3.2
52
52
  - name: Choose profile groups
53
53
  id: profiles
54
54
  run: |
@@ -109,7 +109,7 @@ jobs:
109
109
  runs-on: ubuntu-latest
110
110
  steps:
111
111
  - uses: actions/checkout@v7
112
- - uses: astral-sh/setup-uv@v10.2.0
112
+ - uses: astral-sh/setup-uv@v10.3.2
113
113
  with:
114
114
  enable-cache: true
115
115
  cache-suffix: docs
@@ -132,7 +132,7 @@ jobs:
132
132
  - uses: actions/checkout@v7
133
133
  with:
134
134
  fetch-depth: 0 # mike needs the full history to update the gh-pages branch
135
- - uses: astral-sh/setup-uv@v10.2.0
135
+ - uses: astral-sh/setup-uv@v10.3.2
136
136
  - name: Deploy the versioned docs
137
137
  run: |
138
138
  # Commit as the owner, so the gh-pages history adds no bot contributor.
@@ -125,7 +125,7 @@ jobs:
125
125
  - name: Install uv
126
126
  # Pinned to a full version on purpose: setup-uv stopped publishing
127
127
  # moving major tags at v8, so astral-sh/setup-uv@v10 does not exist.
128
- uses: astral-sh/setup-uv@v10.2.0
128
+ uses: astral-sh/setup-uv@v10.3.2
129
129
 
130
130
  - name: Set up Python
131
131
  run: uv python install 3.10
odctl-1.3.0/CLAUDE.md ADDED
@@ -0,0 +1,20 @@
1
+ # odctl
2
+
3
+ A CLI that starts a local open data stack with Docker Compose profiles: Kafka, Flink, Spark, Iceberg, Trino, ClickHouse, MLflow, Feast, Airflow, Temporal, Marquez, OpenMetadata, Grafana and more.
4
+
5
+ `CONTRIBUTING.md` holds the set-up, the code map, the test commands, the rules for code and docs, how to add a profile or a command, and the release steps. Read it before a change. This file holds only what applies to an agent.
6
+
7
+ ## Running the Tests
8
+
9
+ - Run the unit suite, `pytest tests/`, in full once before each commit, never after each step.
10
+ - Run the strict build at most once per commit.
11
+
12
+ ## Owner Decisions
13
+
14
+ - Show any Renovate config change to the owner before making it.
15
+ - Real projects find defects the profile e2e tests miss, so run the benchtop projects on a released odctl from time to time, when the owner asks. It is not a release step: each project starts a full compose stack, which takes too long to do at every release.
16
+
17
+ ## Git
18
+
19
+ - Commit or push only when asked. Do not create branches unless asked.
20
+ - Merge a bot PR with the owner as author: `gh pr merge <n> --squash --author-email <maintainer email>`. A plain squash merge adds the bot to the contributors list. If a bot commit lands on `main`, rewrite it with the maintainer as author and rename the default branch and back so GitHub recounts contributors. Do this only with the owner's approval: the rewrite changes pushed history and needs a force push, and each force push is approved on its own.
@@ -0,0 +1,99 @@
1
+ # Contributing to odctl
2
+
3
+ This file is for people who change odctl itself: the CLI, its compose files and images, its tests and its documentation. To use odctl, start with the [documentation](https://jaehyeon.me/odctl/). For a change larger than a fix, such as a new profile, open an issue first, so the design is agreed before the work starts. The [roadmap](https://jaehyeon.me/odctl/latest/about/roadmap/) shows what is planned, and [How It Works](https://jaehyeon.me/odctl/latest/how-it-works/) shows how the parts fit together.
4
+
5
+ ## Set Up
6
+
7
+ odctl uses [uv](https://docs.astral.sh/uv/) and needs Docker. Clone the repository, install the dependencies and the project in development mode, and install the pre-commit hooks, which run the formatting and type checks on each commit:
8
+
9
+ ```bash
10
+ git clone https://github.com/jaehyeon-kim/odctl.git
11
+ cd odctl
12
+ uv sync
13
+ uvx pre-commit install
14
+ ```
15
+
16
+ Exporting the diagrams also needs draw.io, Chrome and ffmpeg.
17
+
18
+ ## Code Map
19
+
20
+ | Path | Contents |
21
+ |---|---|
22
+ | `src/odctl/main.py` | the Typer CLI (`list`, `explain`, `ps`, `info`, `init`, `pull`, `up`, `down`, `logs`, `restart`, `recreate`) |
23
+ | `src/odctl/registry.py`, `resources/registry.yml` | profiles, the services in each, dependencies, ports and usage text |
24
+ | `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 |
25
+ | `src/odctl/resources/compose-*.yml` | one compose file per area (kafka, flink, spark, store, analytics, mlops, orch, obsv, metadata, infra, deps) |
26
+ | `src/odctl/resources/docker/` | odctl's own images (postgres, mlflow, airflow, spark), published to `ghcr.io/jaehyeon-kim/odctl/<name>:<version>` |
27
+ | `src/odctl/resources/deps/` | the `deps` image: connector and engine jars built or downloaded into the `odctl-shared-deps` volume; versions in `versions.env` |
28
+ | `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 |
29
+ | `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>` |
30
+ | `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 |
31
+ | `docs/images/` | screenshots of the services' web UIs and the terminal |
32
+ | `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` |
33
+ | `.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 |
34
+ | `docs/snippets/` | the code each guide, Getting Started and How It Works show, one folder per page |
35
+ | `skills/odctl/SKILL.md` | the odctl skill for AI assistants, generated by `scripts/gen_pages.py` |
36
+ | `docs/` | MkDocs Material site, versioned with mike; `docs/about/` holds the public Roadmap and the Changelog |
37
+ | `scripts/release_notes.py` | prints one version's Changelog section, the text of its GitHub release |
38
+
39
+ ## Tests
40
+
41
+ ```bash
42
+ uv run pytest tests/ -q
43
+ uvx pre-commit run --all-files # ruff, ruff format, mypy, whitespace, YAML, TOML
44
+ DISABLE_MKDOCS_2_WARNING=true uv run mkdocs build --strict
45
+ uv run mkdocs serve -a 127.0.0.1:8000 # docs at http://127.0.0.1:8000/odctl/
46
+ uv run python scripts/gen_pages.py # regenerate skills/odctl/SKILL.md
47
+ uv run python scripts/diagrams.py [name ...] # export diagrams; needs draw.io, Chrome and ffmpeg
48
+ ```
49
+
50
+ Each layer checks one thing:
51
+
52
+ - `tests/` checks the CLI, the registry, the compose files, the docs and the generated pages, with no container running.
53
+ - The strict docs build fails on a broken link or anchor.
54
+ - The e2e tests start each profile group and run `.github/scripts/smoke.sh` on it. A running container is not enough: the script checks that the profile does its job, such as a Kafka consumer reading a message or a Flink job committing to Iceberg. It also runs each guide's snippets and Runnable Example.
55
+
56
+ Run the unit suite in full once before each commit. While fixing, run only the failed tests (`pytest --lf`) and the tests of the files you changed. Run the strict build when `docs/`, `mkdocs.yml` or an input of `scripts/gen_pages.py` changed: `registry.yml`, a compose file, or a command's `--help` text.
57
+
58
+ A pull request runs pre-commit, the unit tests on each supported Python version, the strict docs build, and the image builds when an image input changed. e2e runs only on a tag and on a manual run, because odctl runs images tagged with its own version, and the images of an unreleased version do not exist yet. To check a profile on your machine, do what the e2e job does: install the CLI with `uv tool install .`, run `odctl init`, start the profile with `odctl up <profile>`, then run `.github/scripts/smoke.sh <profile>`. Run one `odctl up` at a time. When no image definition changed since the last release, you can give the last release's images the new version's tag for the test, and remove those tags afterwards.
59
+
60
+ ## Rules
61
+
62
+ - **Images follow the CLI version.** odctl runs its own images at its own version (`TAG`), never `latest`: Docker does not pull a tag it already has, so a cached `latest` keeps running old code. Third-party images are pinned to a version and updated by Renovate.
63
+ - **Bind to 127.0.0.1.** Every published port binds to `127.0.0.1`, not all interfaces.
64
+ - **One profile per tool**, with `-lite` and `-full` variants where a tool has a small and a clustered form (`kafka-lite`, `kafka-full`).
65
+ - **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`).
66
+ - **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.
67
+ - **Renovate** opens its pull requests on Mondays, with image updates grouped by compose file. Agree a change to its configuration (`.github/renovate.json`) with the maintainer first.
68
+
69
+ ## Add a Profile
70
+
71
+ 1. Add the services to the compose file of their area in `src/odctl/resources/`, following the Rules: a pinned image, a `container_name`, and ports bound to `127.0.0.1`.
72
+ 2. Add the profile to `src/odctl/resources/registry.yml`: its compose file, description, profiles, dependencies, role and usage text. `scripts/gen_pages.py` builds its page from this entry and the compose file.
73
+ 3. Add its checks to `.github/scripts/smoke.sh`, and its group to `GROUPS` in `.github/scripts/select-profiles.py`.
74
+ 4. Add the technology to the README diagram, `image/diagram.drawio`, then export it. `test_readme_diagram_names_every_technology` fails until you do.
75
+ 5. Give it a tested example on a guide, Getting Started or How It Works, then run `scripts/gen_pages.py` to update the skill.
76
+
77
+ ## Add a Command or Option
78
+
79
+ Add it to `src/odctl/main.py` with its `--help` text, which the CLI reference is built from. Give it a tested example on a guide, Getting Started or How It Works, and a command a line in the README's Commands section. Then run `scripts/gen_pages.py` to update the skill.
80
+
81
+ ## Write the Docs
82
+
83
+ - **Generated pages.** The profile pages and the CLI reference are generated by `scripts/gen_pages.py` during the build, from `registry.yml`, the compose files and the commands. Change those, not the pages, and do not copy versions or ports into hand-written guides.
84
+ - **Code blocks are copies.** 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`. Pages 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.
85
+ - **Guides.** 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.
86
+ - **Writing.** Plain English for readers whose first language is not English. Headings use title case, keeping product names such as `odctl` as they are, and do not start with "The". No em or en dashes. Each Markdown paragraph is one line, never hard-wrapped.
87
+ - **Diagrams.** Copy shapes from `docs/assets/diagrams/_legend.drawio`, and export with `scripts/diagrams.py`.
88
+
89
+ ## Pull Requests
90
+
91
+ Work on a branch from `main` and open a pull request against `main`. Before you push, run pre-commit, the unit tests, and the strict build when the docs changed. Describe each change to ports, profiles, `.env` keys, volumes or workspace files in the pull request, so that the release can add an Upgrading line to the Changelog (`docs/about/changelog.md`).
92
+
93
+ ## Releases
94
+
95
+ The maintainer releases. These steps are written down so that each release is the same:
96
+
97
+ 1. Bump `version` in `pyproject.toml`, run `uv lock`, and add the release to `docs/about/changelog.md`, with an Upgrading line for any change to ports, profiles, `.env` keys, volumes or workspace files. When it reaches `main`, the docs deploy as that version, aliased `latest`.
98
+ 2. Once the `main` pipeline for that commit is green, push tag `vX.Y.Z` on it. A tag on a commit whose checks fail builds no images and cannot be released until it is moved. The tag pipeline builds and pushes the `X.Y.Z` images, then runs e2e for every profile group (about an hour).
99
+ 3. Publish the GitHub release with that version's Changelog section as its text, so the two never differ: `python scripts/release_notes.py X.Y.Z > /tmp/notes.md`, then `gh release create vX.Y.Z --title vX.Y.Z --notes-file /tmp/notes.md`. `publish.yml` waits for a green tag pipeline and checks that every `X.Y.Z` image is in ghcr, then uploads to PyPI.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: odctl
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: Run an open data stack on your machine, with its services already connected, from one command.
5
5
  Project-URL: Homepage, https://github.com/jaehyeon-kim/odctl
6
6
  Project-URL: Repository, https://github.com/jaehyeon-kim/odctl
@@ -106,30 +106,9 @@ Projects that use odctl:
106
106
  - [desflow](https://github.com/jaehyeon-kim/desflow) ([docs](https://jaehyeon.me/desflow/)): a SimPy library for simulations that stream to Kafka, PostgreSQL, Redis and Iceberg; its examples and integration tests run their infrastructure with odctl.
107
107
  - [agentic-analytics-system](https://github.com/jaehyeon-kim/agentic-analytics-system): conversational analytics over an Iceberg lakehouse, with Trino, Iceberg and object storage started by odctl.
108
108
 
109
- ## Development
110
-
111
- Pull requests are welcome. To work on the CLI:
112
-
113
- 1. Clone the repository and install [uv](https://docs.astral.sh/uv/).
114
- 2. Install the dependencies and the project in development mode:
115
- ```bash
116
- uv sync
117
- ```
118
- 3. Install the pre-commit hooks, which run the formatting and type checks:
119
- ```bash
120
- uvx pre-commit install
121
- ```
122
- 4. Run the tests:
123
- ```bash
124
- uv run pytest tests/
125
- ```
126
- 5. Build the documentation site, then serve it while you edit. It is served at `http://127.0.0.1:8000/odctl/`:
127
- ```bash
128
- uv run mkdocs build --strict
129
- uv run mkdocs serve
130
- ```
131
-
132
- The profile pages and the CLI reference are generated by `scripts/gen_pages.py` during the build, so edit `registry.yml`, the compose files or the commands rather than those pages.
109
+ ## Contributing
110
+
111
+ Pull requests are welcome. [CONTRIBUTING.md](https://github.com/jaehyeon-kim/odctl/blob/main/CONTRIBUTING.md) explains how to set up the repository, run the tests, add a profile or a command, and write the docs.
133
112
 
134
113
  ## License
135
114
 
@@ -69,30 +69,9 @@ Projects that use odctl:
69
69
  - [desflow](https://github.com/jaehyeon-kim/desflow) ([docs](https://jaehyeon.me/desflow/)): a SimPy library for simulations that stream to Kafka, PostgreSQL, Redis and Iceberg; its examples and integration tests run their infrastructure with odctl.
70
70
  - [agentic-analytics-system](https://github.com/jaehyeon-kim/agentic-analytics-system): conversational analytics over an Iceberg lakehouse, with Trino, Iceberg and object storage started by odctl.
71
71
 
72
- ## Development
73
-
74
- Pull requests are welcome. To work on the CLI:
75
-
76
- 1. Clone the repository and install [uv](https://docs.astral.sh/uv/).
77
- 2. Install the dependencies and the project in development mode:
78
- ```bash
79
- uv sync
80
- ```
81
- 3. Install the pre-commit hooks, which run the formatting and type checks:
82
- ```bash
83
- uvx pre-commit install
84
- ```
85
- 4. Run the tests:
86
- ```bash
87
- uv run pytest tests/
88
- ```
89
- 5. Build the documentation site, then serve it while you edit. It is served at `http://127.0.0.1:8000/odctl/`:
90
- ```bash
91
- uv run mkdocs build --strict
92
- uv run mkdocs serve
93
- ```
94
-
95
- The profile pages and the CLI reference are generated by `scripts/gen_pages.py` during the build, so edit `registry.yml`, the compose files or the commands rather than those pages.
72
+ ## Contributing
73
+
74
+ Pull requests are welcome. [CONTRIBUTING.md](https://github.com/jaehyeon-kim/odctl/blob/main/CONTRIBUTING.md) explains how to set up the repository, run the tests, add a profile or a command, and write the docs.
96
75
 
97
76
  ## License
98
77
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "odctl"
3
- version = "1.2.1"
3
+ version = "1.3.0"
4
4
  description = "Run an open data stack on your machine, with its services already connected, from one command."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -418,6 +418,8 @@ ENV_KEYS = {
418
418
  "TZ": "The time zone of the services that read it, `UTC` by default.",
419
419
  "_AIRFLOW_PIP_DEPS": "Extra Python packages installed in every new Airflow container.",
420
420
  "_MLOPS_PIP_DEPS": "Extra Python packages installed in every new `mlflow` and `mlflow-serve` container.",
421
+ "_METADATA_PIP_DEPS": "Extra Python packages installed in every new `openmetadata-ingestion` container.",
422
+ "METADATA_AUTOPILOT": "`true` turns on OpenMetadata's AutoPilot, which creates and runs default agents for a service added in the UI; it is `false` by default.",
421
423
  "MODEL_URI": "The model `mlflow-serve` serves, such as `models:/<name>@<alias>`; empty means it waits idle.",
422
424
  }
423
425