Skip to content

Commit e195f1e

Browse files
feat(qlik_compose): five components for Qlik Compose DW-automation orchestration
Ships the same shape as qlik_replicate and tm1: - qlik_compose_resource — shared Compose REST auth (session or API-token) - qlik_compose_workflow_trigger_job — run/stop a Compose workflow with optional wait_for_completion polling to terminal state - qlik_compose_workflow_status_sensor — poll workflow state; trigger Dagster job on COMPLETED / ERROR / FAILED / STOPPED transitions; cursor-deduped on (state, last_completion_time) - qlik_compose_workflow_metrics_ingestion — DataFrame per workflow with state, duration, rows_loaded, error_count - qlik_compose_workspace — StateBackedComponent that enumerates every Project × Workflow × Data Mart; per-kind Fivetran-shape selectors (workflow_selector, data_mart_selector); materializing a Workflow asset runs the workflow Also fixes README cross-links across ALL new components (qlik_replicate, tm1, qlik_compose) — strips [foo](../../path/) markdown links to plain `foo` names so Vercel UI's auto-linking (via /c/<id> route) works. The relative-path form doesn't match Vercel's routing; plain names do. Bumps version to 0.10.32.
1 parent a3c5790 commit e195f1e

41 files changed

Lines changed: 1256 additions & 37 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# QlikComposeWorkflowMetricsIngestionComponent
2+
3+
Emit a DataFrame with per-workflow metrics from **Qlik Compose**. One row per workflow per materialization. Foundation for DW-build observability dashboards.
4+
5+
Pairs with `qlik_compose_resource`.
6+
7+
## Emitted schema
8+
9+
| Column | Type | Notes |
10+
|---|---|---|
11+
| `project` | string | Compose project (DW) name |
12+
| `workflow` | string | Workflow name |
13+
| `state` | string | RUNNING / COMPLETED / STOPPED / ERROR / FAILED |
14+
| `last_run_duration_seconds` | float | Wall-clock of the last run |
15+
| `rows_loaded` | int | Total rows written by the workflow |
16+
| `error_count` | int | Non-null even at zero |
17+
| `last_completion_time` | string | ISO-8601 |
18+
| `polled_at` | float | Unix epoch seconds |
19+
20+
## Example
21+
22+
```yaml
23+
type: dagster_community_components.QlikComposeWorkflowMetricsIngestionComponent
24+
attributes:
25+
asset_key: qlik_compose_metrics
26+
projects: [FinanceDW, SalesDW]
27+
group_name: qlik_observability
28+
resource_key: qlik_compose_resource
29+
```
30+
31+
## Chaining
32+
33+
- `dataframe_to_snowflake` → BI dashboards
34+
- `dataframe_to_prometheus` → alert on error rate
35+
- `filter` (`error_count > 0`) → `dataframe_to_pagerduty` → alert on any errors
36+
37+
## Related
38+
39+
- `qlik_compose_resource`
40+
- `qlik_compose_workflow_trigger_job`
41+
- `qlik_compose_workspace`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .component import QlikComposeWorkflowMetricsIngestionComponent
2+
3+
__all__ = ["QlikComposeWorkflowMetricsIngestionComponent"]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Qlik Compose Workflow Metrics Ingestion Component.
2+
3+
Emit a DataFrame with per-workflow metrics from Qlik Compose (state, last-run
4+
duration, rows loaded, error count). One row per workflow per materialization.
5+
Feed into BI dashboards or alert-input assets.
6+
"""
7+
from typing import List, Optional
8+
9+
import dagster as dg
10+
from pydantic import Field
11+
12+
13+
class QlikComposeWorkflowMetricsIngestionComponent(dg.Component, dg.Model, dg.Resolvable):
14+
"""Emit per-workflow metrics from Qlik Compose.
15+
16+
Example:
17+
18+
```yaml
19+
type: dagster_community_components.QlikComposeWorkflowMetricsIngestionComponent
20+
attributes:
21+
asset_key: qlik_compose_metrics
22+
projects: [FinanceDW, SalesDW]
23+
group_name: qlik_observability
24+
resource_key: qlik_compose_resource
25+
```
26+
"""
27+
28+
asset_key: str = Field(description="Asset key for the emitted DataFrame.")
29+
projects: List[str] = Field(description="Compose projects to poll.")
30+
workflows: Optional[List[str]] = Field(default=None, description="Optional workflow whitelist across all listed projects.")
31+
group_name: Optional[str] = Field(default=None)
32+
resource_key: str = Field(default="qlik_compose_resource")
33+
34+
def build_defs(self, context: dg.ComponentLoadContext) -> dg.Definitions:
35+
_self = self
36+
required_resource_keys = {self.resource_key}
37+
38+
@dg.asset(
39+
name=_self.asset_key,
40+
group_name=_self.group_name,
41+
required_resource_keys=required_resource_keys,
42+
compute_kind="qlik_compose",
43+
)
44+
def _the_asset(context: dg.AssetExecutionContext):
45+
import time
46+
try:
47+
import pandas as pd
48+
import requests
49+
except ImportError as e:
50+
raise Exception("pandas or requests library not installed") from e
51+
52+
resource = getattr(context.resources, _self.resource_key)
53+
session = requests.Session()
54+
session.verify = resource.verify_ssl
55+
headers = resource.get_auth_headers()
56+
57+
login_body = resource.login_body()
58+
if login_body is not None:
59+
r = session.post(f"{resource.api_base}/login", json=login_body, timeout=30)
60+
if r.status_code >= 300:
61+
raise Exception(f"Compose login failed: {r.status_code}")
62+
63+
wanted_wfs = set(_self.workflows) if _self.workflows else None
64+
rows: list[dict] = []
65+
polled_at = time.time()
66+
67+
for project in _self.projects:
68+
try:
69+
lr = session.get(f"{resource.project_url(project)}/workflows", headers=headers, timeout=30)
70+
lr.raise_for_status()
71+
wf_list = (lr.json() or {}).get("workflows") or lr.json() or []
72+
except Exception as e:
73+
context.log.warning(f"Failed to list workflows for {project}: {e}")
74+
continue
75+
76+
if isinstance(wf_list, dict):
77+
wf_list = wf_list.get("value", [])
78+
79+
for wf_entry in wf_list:
80+
wf_name = wf_entry.get("name") if isinstance(wf_entry, dict) else str(wf_entry)
81+
if not wf_name:
82+
continue
83+
if wanted_wfs is not None and wf_name not in wanted_wfs:
84+
continue
85+
try:
86+
dr = session.get(resource.workflow_url(project, wf_name), headers=headers, timeout=30)
87+
dr.raise_for_status()
88+
detail = dr.json() or {}
89+
except Exception as e:
90+
context.log.warning(f"Detail fetch failed for {project}/{wf_name}: {e}")
91+
continue
92+
93+
wf = detail.get("workflow") or detail
94+
rows.append({
95+
"project": project,
96+
"workflow": wf_name,
97+
"state": wf.get("state"),
98+
"last_run_duration_seconds": wf.get("last_run_duration_seconds"),
99+
"rows_loaded": wf.get("rows_loaded") or wf.get("total_rows"),
100+
"error_count": wf.get("error_count") or 0,
101+
"last_completion_time": wf.get("last_completion_time") or wf.get("last_run_time"),
102+
"polled_at": polled_at,
103+
})
104+
105+
df = pd.DataFrame(rows)
106+
context.add_output_metadata({
107+
"row_count": len(df),
108+
"projects_polled": len(_self.projects),
109+
})
110+
return df
111+
112+
return dg.Definitions(assets=[_the_asset])
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type: dagster_community_components.QlikComposeWorkflowMetricsIngestionComponent
2+
attributes:
3+
asset_key: qlik_compose_metrics
4+
projects: [FinanceDW, SalesDW]
5+
group_name: qlik_observability
6+
resource_key: qlik_compose_resource
7+
# workflows: [FullBuildAndPopulate, IncrementalUpdate] # optional whitelist
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pandas>=2.0.0
2+
requests>=2.28.0
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"component_type": "dagster_community_components.QlikComposeWorkflowMetricsIngestionComponent",
3+
"description": "Emit a DataFrame with per-workflow metrics from Qlik Compose (state, last-run duration, rows loaded, error count). One row per workflow per materialization. Foundation for DW-build observability dashboards.",
4+
"category": "ingestion",
5+
"icon": "TableChart",
6+
"attributes": {
7+
"asset_key": {"type": "string", "label": "Asset Key", "required": true},
8+
"projects": {"type": "array", "label": "Projects", "required": true, "description": "Compose projects to poll."},
9+
"workflows": {"type": "array", "label": "Workflows", "required": false, "description": "Optional workflow whitelist across all listed projects."},
10+
"group_name": {"type": "string", "label": "Group Name", "required": false},
11+
"resource_key": {"type": "string", "label": "Resource Key", "required": false, "default": "qlik_compose_resource"}
12+
}
13+
}

assets/ingestion/qlik_replicate_task_metrics_ingestion/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Materialize per-task metrics from **Qlik Replicate** (via Enterprise Manager REST API) as a DataFrame asset. Each materialization polls the API and emits one row per task with its current state, latency, throughput, and error count.
44

5-
Pairs with [`qlik_replicate_resource`](../../../resources/qlik_replicate_resource/) for auth.
5+
Pairs with `qlik_replicate_resource` for auth.
66

77
## When to use
88

@@ -68,7 +68,7 @@ Or use a schedule-based partitioned asset if you want to keep historical rows in
6868

6969
## Related
7070

71-
- [`qlik_replicate_resource`](../../../resources/qlik_replicate_resource/) — shared auth
72-
- [`qlik_replicate_task_trigger_job`](../../../jobs/qlik_replicate_task_trigger_job/) — imperative task control (start/stop/reload)
73-
- [`qlik_replicate_task_status_sensor`](../../../sensors/qlik_replicate_task_status_sensor/) — event-drive on state transitions
74-
- [`dataframe_to_pagerduty`](../../sinks/dataframe_to_pagerduty/) — chain metrics → alerts on `error_count > 0` rows
71+
- `qlik_replicate_resource` — shared auth
72+
- `qlik_replicate_task_trigger_job` — imperative task control (start/stop/reload)
73+
- `qlik_replicate_task_status_sensor` — event-drive on state transitions
74+
- `dataframe_to_pagerduty` — chain metrics → alerts on `error_count > 0` rows

assets/ingestion/tm1_cube_data_ingestion/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Materialize a slice of a **TM1 cube** via MDX as a Dagster DataFrame asset. Turn multidimensional planning cube data into flat rows for downstream Dagster transforms, dbt models, or warehouse loads.
44

5-
Pairs with [`tm1_resource`](../../../resources/tm1_resource/) for auth.
5+
Pairs with `tm1_resource` for auth.
66

77
## Query modes
88

@@ -58,6 +58,6 @@ Common downstream patterns:
5858

5959
## Related
6060

61-
- [`tm1_resource`](../../../resources/tm1_resource/)
62-
- [`tm1_process_trigger_job`](../../../jobs/tm1_process_trigger_job/) — run a TI process before ingesting
63-
- [`tm1_workspace`](../../../integrations/tm1_workspace/) — auto-emit assets per cube (workspace shape)
61+
- `tm1_resource`
62+
- `tm1_process_trigger_job` — run a TI process before ingesting
63+
- `tm1_workspace` — auto-emit assets per cube (workspace shape)

dagster_community_components/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,12 @@
10131013
"TM1ProcessStatusSensorComponent": "sensors/tm1_process_status_sensor/component.py",
10141014
"TM1CubeDataIngestionComponent": "assets/ingestion/tm1_cube_data_ingestion/component.py",
10151015
"TM1WorkspaceComponent": "integrations/tm1_workspace/component.py",
1016+
"QlikComposeResource": "resources/qlik_compose_resource/component.py",
1017+
"QlikComposeResourceComponent": "resources/qlik_compose_resource/component.py",
1018+
"QlikComposeWorkflowTriggerJobComponent": "jobs/qlik_compose_workflow_trigger_job/component.py",
1019+
"QlikComposeWorkflowStatusSensorComponent": "sensors/qlik_compose_workflow_status_sensor/component.py",
1020+
"QlikComposeWorkflowMetricsIngestionComponent": "assets/ingestion/qlik_compose_workflow_metrics_ingestion/component.py",
1021+
"QlikComposeWorkspaceComponent": "integrations/qlik_compose_workspace/component.py",
10161022
}
10171023

10181024
_loaded: dict[str, object] = {}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# QlikComposeWorkspaceComponent
2+
3+
Auto-emit one Dagster asset per **Qlik Compose Workflow / Data Mart** across every Project (Data Warehouse). `StateBackedComponent` — discovery cached to disk, refreshed on explicit trigger. Materializing a Workflow asset runs the workflow to terminal state; Data Mart assets are observational.
4+
5+
The workspace-shape peer of the low-level `qlik_compose_*` components.
6+
7+
## Related components
8+
9+
- `qlik_compose_resource` — shared auth
10+
- `qlik_compose_workflow_trigger_job` — imperative workflow control
11+
- `qlik_compose_workflow_status_sensor` — event-drive on state
12+
- `qlik_compose_workflow_metrics_ingestion` — metrics DataFrame
13+
14+
## When to use which
15+
16+
| Goal | Use |
17+
|---|---|
18+
| Wrap ALL Compose workflows + data marts as Dagster assets with one YAML | `qlik_compose_workspace` (this) |
19+
| Run a specific workflow with bespoke config from a Dagster job | `qlik_compose_workflow_trigger_job` |
20+
| Kick a downstream job when a workflow completes | `qlik_compose_workflow_status_sensor` |
21+
| Feed DW-build metrics into observability dashboards | `qlik_compose_workflow_metrics_ingestion` |
22+
23+
## Selector shape
24+
25+
Same as Fivetran's `connector_selector`:
26+
27+
```yaml
28+
workflow_selector:
29+
by_name: [FullBuildAndPopulate]
30+
by_pattern: ["Incremental_*"]
31+
exclude_by_name: [test_workflow]
32+
exclude_by_pattern: ["*_deprecated"]
33+
```
34+
35+
Same shape for `data_mart_selector`. Omit → include everything.
36+
37+
## Example
38+
39+
```yaml
40+
type: dagster_community_components.QlikComposeWorkspaceComponent
41+
attributes:
42+
base_url_env_var: QLIK_COMPOSE_URL
43+
api_token_env_var: QLIK_COMPOSE_TOKEN
44+
projects: [FinanceDW, SalesDW]
45+
workflow_selector:
46+
by_pattern: ["FullBuild*", "Incremental*"]
47+
group_name: qlik_compose
48+
defs_state:
49+
management_type: LOCAL_FILESYSTEM
50+
refresh_if_dev: true
51+
```
52+
53+
## Config reference
54+
55+
| Field | Type | Default | Description |
56+
|---|---|---|---|
57+
| `base_url_env_var` | str | — | Env var with Compose base URL. |
58+
| `api_token_env_var` | str | — | Env var with API token. |
59+
| `username_env_var` / `password_env_var` | str | — | Session-based auth (dev). |
60+
| `verify_ssl` | bool | `true` | TLS verification. |
61+
| `projects` | list[str] | all | Whitelist of Compose projects. |
62+
| `workflow_selector` | object | — | Filter workflows. Fivetran-shape. |
63+
| `data_mart_selector` | object | — | Filter data marts. Fivetran-shape. |
64+
| `group_name` | str | — | Dagster asset group. |
65+
| `asset_key_prefix` | list[str] | `["qlik_compose"]` | Prefix; final key = `[...prefix, project, workflow|data_mart, name]`. |
66+
| `compute_kind` | str | `qlik_compose` | UI compute-kind label. |
67+
| `wait_for_completion` | bool | `true` | For Workflow assets: wait to terminal state. |
68+
| `poll_interval_seconds` | int | `30` | Seconds between status polls. |
69+
| `timeout_seconds` | int | `3600` | Deadline for wait_for_completion. |
70+
| `defs_state` | object | — | `LOCAL_FILESYSTEM` (dev) or `CLOUD_OBJECT_STORE` (Dagster+). |
71+
72+
[Schema](schema.json) · [Example](example.yaml)

0 commit comments

Comments
 (0)