Files
tracer-cloud--opensre/integrations/dagster/client.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

175 lines
5.8 KiB
Python

"""Dagster GraphQL client.
All methods return ``{"data": <parsed>}`` on success or ``{"error": "<message>"}``
on transport, HTTP, or GraphQL-level errors, so callers don't branch on exceptions.
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from integrations.dagster.queries import (
GET_RUN_LOGS,
LIST_ASSETS,
LIST_RUNS,
LIST_SCHEDULE_TICKS,
LIST_SENSOR_TICKS,
)
logger = logging.getLogger(__name__)
_API_TOKEN_HEADER = "Dagster-Cloud-Api-Token"
class DagsterClient:
"""GraphQL client targeting a Dagster instance.
Auth token, when set, is sent as ``Dagster-Cloud-Api-Token`` (Cloud's
canonical header, also accepted by OSS instances behind matching proxies).
"""
def __init__(
self,
endpoint: str,
api_token: str = "",
timeout_s: int = 10,
http_client: httpx.Client | None = None,
) -> None:
# Normalise the endpoint so callers can paste any of:
# https://<host>/<deployment>
# https://<host>/<deployment>/
# https://<host>/<deployment>/graphql
# All collapse to the canonical base; the client appends /graphql itself.
self.endpoint = endpoint.rstrip("/").removesuffix("/graphql")
self.api_token = api_token
self.timeout_s = timeout_s
self._graphql_url = f"{self.endpoint}/graphql"
# When http_client is None we build a default httpx.Client with our
# auth + content-type headers and timeout. Tests inject their own
# client (typically backed by an httpx.MockTransport) to mock
# network behaviour.
self._client = http_client or httpx.Client(
headers=self._default_headers(),
timeout=self.timeout_s,
follow_redirects=True,
)
def _default_headers(self) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
if self.api_token:
headers[_API_TOKEN_HEADER] = self.api_token
return headers
def close(self) -> None:
self._client.close()
def __enter__(self) -> DagsterClient:
return self
def __exit__(self, *exc: object) -> None:
self.close()
def _post(self, query: str, variables: dict[str, Any]) -> dict[str, Any]:
"""POST a GraphQL query and return either ``{"data": ...}`` or ``{"error": ...}``."""
try:
response = self._client.post(
self._graphql_url,
json={"query": query, "variables": variables},
)
except httpx.RequestError as exc:
return {"error": f"Request to Dagster failed: {exc}"}
if response.status_code != 200:
body_preview = response.text[:200] if response.text else ""
return {"error": f"HTTP {response.status_code}: {body_preview}"}
try:
payload = response.json()
except ValueError as exc:
return {"error": f"Invalid JSON in Dagster response: {exc}"}
if "errors" in payload:
messages = [str(err.get("message", err)) for err in payload["errors"]]
return {"error": "; ".join(messages)}
data = payload.get("data") or {}
return {"data": data}
def ping(self) -> dict[str, Any]:
"""Lightweight version query to confirm the endpoint is a live Dagster."""
return self._post("query DagsterPing { version }", {})
def list_runs(
self,
*,
limit: int = 25,
status: str | None = None,
job_name: str | None = None,
) -> dict[str, Any]:
"""Issue the ListRuns query.
``status`` is a single ``RunStatus`` (e.g. ``"FAILURE"``), sent as a
single-element ``RunsFilter.statuses``. ``job_name`` is sent as
``RunsFilter.pipelineName`` (Dagster's back-compat alias for jobName).
"""
variables: dict[str, Any] = {"limit": limit}
if status:
variables["statuses"] = [status]
if job_name:
variables["pipelineName"] = job_name
return self._post(LIST_RUNS, variables)
def get_run_logs(
self, *, run_id: str, limit: int = 250, cursor: str | None = None
) -> dict[str, Any]:
"""Issue the GetRunLogs query for a specific run id (single page)."""
variables: dict[str, Any] = {"runId": run_id, "limit": limit}
if cursor is not None:
variables["afterCursor"] = cursor
return self._post(GET_RUN_LOGS, variables)
def list_assets_with_materialization(self, *, limit: int = 25) -> dict[str, Any]:
"""Issue the ListAssets query and return the parsed payload."""
return self._post(LIST_ASSETS, {"limit": limit})
def list_sensor_ticks(
self,
*,
repository_name: str,
repository_location_name: str,
sensor_name: str,
limit: int = 25,
) -> dict[str, Any]:
"""Issue the SensorTicks query for a fully-qualified sensor coordinate."""
sensor_selector = {
"repositoryName": repository_name,
"repositoryLocationName": repository_location_name,
"sensorName": sensor_name,
}
return self._post(
LIST_SENSOR_TICKS,
{"sensorSelector": sensor_selector, "limit": limit},
)
def list_schedule_ticks(
self,
*,
repository_name: str,
repository_location_name: str,
schedule_name: str,
limit: int = 25,
) -> dict[str, Any]:
"""Issue the ScheduleTicks query for a fully-qualified schedule coordinate."""
schedule_selector = {
"repositoryName": repository_name,
"repositoryLocationName": repository_location_name,
"scheduleName": schedule_name,
}
return self._post(
LIST_SCHEDULE_TICKS,
{"scheduleSelector": schedule_selector, "limit": limit},
)