dex-connector-datadog 0.1.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.
- dex_connector_datadog-0.1.0/.gitignore +6 -0
- dex_connector_datadog-0.1.0/PKG-INFO +53 -0
- dex_connector_datadog-0.1.0/README.md +42 -0
- dex_connector_datadog-0.1.0/examples/datadog_lookup/__init__.py +3 -0
- dex_connector_datadog-0.1.0/examples/datadog_lookup/flow.py +76 -0
- dex_connector_datadog-0.1.0/examples/datadog_lookup/worker.py +51 -0
- dex_connector_datadog-0.1.0/pyproject.toml +37 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/__init__.py +50 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/__init__.py +2 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/config.py +26 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/connection.py +134 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/definitions.py +55 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/mock.py +107 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/models.py +104 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/_generated/steps.py +128 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/client.py +383 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/connection.py +46 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/datadogmock/__init__.py +54 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/dex-connector.json +77 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/models.py +27 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/py.typed +0 -0
- dex_connector_datadog-0.1.0/src/dex_connectors/datadog/steps.py +37 -0
- dex_connector_datadog-0.1.0/tests/test_conformance.py +35 -0
- dex_connector_datadog-0.1.0/tests/test_manifest_contract.py +15 -0
- dex_connector_datadog-0.1.0/tests/test_operations.py +193 -0
- dex_connector_datadog-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-datadog
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Datadog connector for Dex Flows
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: dex-connectors-sdk==0.1.0
|
|
8
|
+
Requires-Dist: dex-python-sdk>=1.28.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Datadog connector for Python
|
|
13
|
+
|
|
14
|
+
Install with `pip install dex-connector-datadog==0.1.0`. The package requires
|
|
15
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
16
|
+
manifest Queries `queryMetrics` and `listMonitors` with the same branches,
|
|
17
|
+
defaults, and failure classification as the Go module.
|
|
18
|
+
|
|
19
|
+
## Operations
|
|
20
|
+
|
|
21
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `queryMetrics` | `new_query_metrics_step` | `queried` | `providerRejected`, `invalidResponse`, `defect` |
|
|
24
|
+
| `listMonitors` | `new_list_monitors_step` | `listed` | `providerRejected`, `invalidResponse`, `defect` |
|
|
25
|
+
|
|
26
|
+
`queryMetrics` requires a positive `from_seconds`, a later `to_seconds`, and a
|
|
27
|
+
non-blank Datadog metric query, and accepts only a response whose `status` is
|
|
28
|
+
`ok`. `listMonitors` reads a zero-based `page` with `page_size` defaulting to
|
|
29
|
+
100, at most 1000. 408, 409, 429, 5xx, and transport failures retry; other
|
|
30
|
+
non-200 responses select `providerRejected` with authentication,
|
|
31
|
+
authorization, not-found, or validation failure kinds.
|
|
32
|
+
Metric point numbers retain integer JSON form when exact. A point number that
|
|
33
|
+
cannot be represented exactly by Go's float64 output selects `invalidResponse`
|
|
34
|
+
and identifies its `series[].pointlist[][]` location in the failure.
|
|
35
|
+
|
|
36
|
+
## Authorization
|
|
37
|
+
|
|
38
|
+
Create a Dex Web connection named `datadog` as described in the
|
|
39
|
+
[connector README](../README.md#authorization). Set `baseUrl` to your Datadog
|
|
40
|
+
site's API host. The API and application keys travel only in `DD-API-KEY` and
|
|
41
|
+
`DD-APPLICATION-KEY` and are resolved for every attempt.
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
[`examples/datadog_lookup`](examples/datadog_lookup/flow.py) queries a metric
|
|
46
|
+
interval and records the series. Run the Worker with
|
|
47
|
+
`uv run python -m examples.datadog_lookup.worker` after `dexcli dev` has loaded
|
|
48
|
+
a project containing the `datadog` connection.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
`datadogmock.Mock` scripts both Queries and defaults to the manifest answers.
|
|
53
|
+
Run `uv run pytest tests` for provider parity and manifest contract checks.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Datadog connector for Python
|
|
2
|
+
|
|
3
|
+
Install with `pip install dex-connector-datadog==0.1.0`. The package requires
|
|
4
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
5
|
+
manifest Queries `queryMetrics` and `listMonitors` with the same branches,
|
|
6
|
+
defaults, and failure classification as the Go module.
|
|
7
|
+
|
|
8
|
+
## Operations
|
|
9
|
+
|
|
10
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `queryMetrics` | `new_query_metrics_step` | `queried` | `providerRejected`, `invalidResponse`, `defect` |
|
|
13
|
+
| `listMonitors` | `new_list_monitors_step` | `listed` | `providerRejected`, `invalidResponse`, `defect` |
|
|
14
|
+
|
|
15
|
+
`queryMetrics` requires a positive `from_seconds`, a later `to_seconds`, and a
|
|
16
|
+
non-blank Datadog metric query, and accepts only a response whose `status` is
|
|
17
|
+
`ok`. `listMonitors` reads a zero-based `page` with `page_size` defaulting to
|
|
18
|
+
100, at most 1000. 408, 409, 429, 5xx, and transport failures retry; other
|
|
19
|
+
non-200 responses select `providerRejected` with authentication,
|
|
20
|
+
authorization, not-found, or validation failure kinds.
|
|
21
|
+
Metric point numbers retain integer JSON form when exact. A point number that
|
|
22
|
+
cannot be represented exactly by Go's float64 output selects `invalidResponse`
|
|
23
|
+
and identifies its `series[].pointlist[][]` location in the failure.
|
|
24
|
+
|
|
25
|
+
## Authorization
|
|
26
|
+
|
|
27
|
+
Create a Dex Web connection named `datadog` as described in the
|
|
28
|
+
[connector README](../README.md#authorization). Set `baseUrl` to your Datadog
|
|
29
|
+
site's API host. The API and application keys travel only in `DD-API-KEY` and
|
|
30
|
+
`DD-APPLICATION-KEY` and are resolved for every attempt.
|
|
31
|
+
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
[`examples/datadog_lookup`](examples/datadog_lookup/flow.py) queries a metric
|
|
35
|
+
interval and records the series. Run the Worker with
|
|
36
|
+
`uv run python -m examples.datadog_lookup.worker` after `dexcli dev` has loaded
|
|
37
|
+
a project containing the `datadog` connection.
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
`datadogmock.Mock` scripts both Queries and defaults to the manifest answers.
|
|
42
|
+
Run `uv run pytest tests` for provider parity and manifest contract checks.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Runnable Datadog lookup Flow using the queryMetrics Query."""
|
|
4
|
+
|
|
5
|
+
import dex
|
|
6
|
+
from dex_connectors.sdk import QueryResult
|
|
7
|
+
|
|
8
|
+
from dex_connectors.datadog import Connection, MetricsQuery, QueryMetricsInput, new_query_metrics_step
|
|
9
|
+
|
|
10
|
+
FLOW_TYPE = "DatadogLookup"
|
|
11
|
+
CONNECTION_NAME = "datadog"
|
|
12
|
+
_OPERATION_STEP_TYPE = "QueryMetricsDatadog"
|
|
13
|
+
_REQUEST = dex.Attribute("datadog-lookup-request", QueryMetricsInput)
|
|
14
|
+
_OUTPUT = dex.Attribute("datadog-lookup-output", MetricsQuery)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def map_to_operation_input(request: QueryMetricsInput) -> QueryMetricsInput:
|
|
18
|
+
"""Query the requested metric interval unchanged."""
|
|
19
|
+
return request
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# dex:group group-id:datadog-lookup group-label:"Datadog lookup"
|
|
23
|
+
# dex:explanation text:"Record the lookup request before querying Datadog metrics."
|
|
24
|
+
class _StartLookup(dex.Step[QueryMetricsInput]):
|
|
25
|
+
def get_step_type(self) -> str:
|
|
26
|
+
return "StartDatadogLookup"
|
|
27
|
+
|
|
28
|
+
def execute(self, context: dex.Context, input: QueryMetricsInput) -> dex.StepDecision:
|
|
29
|
+
_REQUEST.set(context, input)
|
|
30
|
+
return dex.go_to(dex.StepRef(_OPERATION_STEP_TYPE), input)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# dex:group group-id:datadog-lookup group-label:"Datadog lookup"
|
|
34
|
+
# dex:explanation text:"Record the metric series and complete the Flow."
|
|
35
|
+
class _CompleteLookup(dex.Step[QueryResult[MetricsQuery]]):
|
|
36
|
+
def get_step_type(self) -> str:
|
|
37
|
+
return "CompleteDatadogLookup"
|
|
38
|
+
|
|
39
|
+
def execute(self, context: dex.Context, input: QueryResult[MetricsQuery]) -> dex.StepDecision:
|
|
40
|
+
_OUTPUT.set(context, input.value)
|
|
41
|
+
return dex.graceful_complete(input.value)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DatadogLookupFlow(dex.Flow[QueryMetricsInput]):
|
|
45
|
+
"""Query Datadog metrics through the static datadog connection."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, connection: Connection) -> None:
|
|
48
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
49
|
+
raise ValueError("Datadog lookup requires the static datadog connection")
|
|
50
|
+
self._start = _StartLookup()
|
|
51
|
+
self._complete = _CompleteLookup()
|
|
52
|
+
self._list = new_query_metrics_step(
|
|
53
|
+
step_type=_OPERATION_STEP_TYPE,
|
|
54
|
+
step_input_type=QueryMetricsInput,
|
|
55
|
+
annotations={
|
|
56
|
+
"groupId": "datadog",
|
|
57
|
+
"groupLabel": "Datadog",
|
|
58
|
+
"explanation": "Query Datadog timeseries points over an explicit Unix-second interval.",
|
|
59
|
+
},
|
|
60
|
+
connection=connection,
|
|
61
|
+
connection_name=CONNECTION_NAME,
|
|
62
|
+
map_to_operation_input=map_to_operation_input,
|
|
63
|
+
queried=self._complete,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def get_flow_type(self) -> str:
|
|
67
|
+
"""Return the stable Flow identity used in Dex Web."""
|
|
68
|
+
return FLOW_TYPE
|
|
69
|
+
|
|
70
|
+
def get_steps(self) -> dex.StepList[QueryMetricsInput]:
|
|
71
|
+
"""Register the start, provider list, and completion Steps."""
|
|
72
|
+
return dex.StepList.start_step(self._start).other_steps(self._list, self._complete)
|
|
73
|
+
|
|
74
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
75
|
+
"""Persist the request and metric query result."""
|
|
76
|
+
return dex.PersistenceSchema.of(_REQUEST, _OUTPUT)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Worker entrypoint for the Datadog lookup Flow."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import dex
|
|
11
|
+
from dex_connectors.sdk.projectconfig import load_from_environment
|
|
12
|
+
|
|
13
|
+
from dex_connectors.datadog import new_project_connection
|
|
14
|
+
|
|
15
|
+
from .flow import CONNECTION_NAME, DatadogLookupFlow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_worker() -> None:
|
|
19
|
+
"""Load the named Dex Web connection and serve Datadog lookups."""
|
|
20
|
+
project = await load_from_environment()
|
|
21
|
+
connection = None
|
|
22
|
+
cache = None
|
|
23
|
+
try:
|
|
24
|
+
connection = new_project_connection(project, CONNECTION_NAME)
|
|
25
|
+
registry = dex.Registry([DatadogLookupFlow(connection)], allow_async_handlers=True)
|
|
26
|
+
cache_directory = os.environ.get(
|
|
27
|
+
"DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-datadog-lookup-blobs")
|
|
28
|
+
)
|
|
29
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(cache_directory, 1 << 30))
|
|
30
|
+
worker = dex.AsyncWorker(
|
|
31
|
+
registry,
|
|
32
|
+
cache,
|
|
33
|
+
dex.WorkerOptions(server_address=os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")),
|
|
34
|
+
)
|
|
35
|
+
async with worker:
|
|
36
|
+
await worker.start()
|
|
37
|
+
finally:
|
|
38
|
+
if connection is not None:
|
|
39
|
+
await connection.aclose()
|
|
40
|
+
if cache is not None:
|
|
41
|
+
cache.close()
|
|
42
|
+
await project.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
"""Run until the Worker stops or the process receives an interrupt."""
|
|
47
|
+
asyncio.run(run_worker())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dex-connector-datadog"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Datadog connector for Dex Flows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["dex-connectors-sdk==0.1.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "pyyaml>=6", "types-PyYAML>=6"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/dex_connectors"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
pythonpath = [".", "src"]
|
|
23
|
+
asyncio_mode = "auto"
|
|
24
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
python_version = "3.11"
|
|
28
|
+
strict = true
|
|
29
|
+
mypy_path = "src"
|
|
30
|
+
explicit_package_bases = true
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
line-length = 110
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Datadog metric and monitor Queries for Dex connector Steps."""
|
|
4
|
+
|
|
5
|
+
from .client import (
|
|
6
|
+
CONNECTOR_ID,
|
|
7
|
+
DEFECT,
|
|
8
|
+
INVALID_RESPONSE,
|
|
9
|
+
LIST_MONITORS_DEFINITION,
|
|
10
|
+
LISTED,
|
|
11
|
+
PROVIDER_REJECTED,
|
|
12
|
+
QUERIED,
|
|
13
|
+
QUERY_METRICS_DEFINITION,
|
|
14
|
+
Client,
|
|
15
|
+
Config,
|
|
16
|
+
Credentials,
|
|
17
|
+
ListMonitorsOperation,
|
|
18
|
+
QueryMetricsOperation,
|
|
19
|
+
)
|
|
20
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
21
|
+
from .models import ListMonitorsInput, MetricSeries, MetricsQuery, Monitor, MonitorsPage, QueryMetricsInput
|
|
22
|
+
from .steps import new_list_monitors_step, new_query_metrics_step
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Client",
|
|
26
|
+
"Config",
|
|
27
|
+
"Connection",
|
|
28
|
+
"Credentials",
|
|
29
|
+
"ListMonitorsInput",
|
|
30
|
+
"ListMonitorsOperation",
|
|
31
|
+
"MetricSeries",
|
|
32
|
+
"MetricsQuery",
|
|
33
|
+
"Monitor",
|
|
34
|
+
"MonitorsPage",
|
|
35
|
+
"QueryMetricsInput",
|
|
36
|
+
"QueryMetricsOperation",
|
|
37
|
+
"new_connection",
|
|
38
|
+
"new_connection_with_operations",
|
|
39
|
+
"new_list_monitors_step",
|
|
40
|
+
"new_project_connection",
|
|
41
|
+
"new_query_metrics_step",
|
|
42
|
+
"CONNECTOR_ID",
|
|
43
|
+
"DEFECT",
|
|
44
|
+
"INVALID_RESPONSE",
|
|
45
|
+
"LIST_MONITORS_DEFINITION",
|
|
46
|
+
"LISTED",
|
|
47
|
+
"PROVIDER_REJECTED",
|
|
48
|
+
"QUERIED",
|
|
49
|
+
"QUERY_METRICS_DEFINITION",
|
|
50
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import SecretString
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Config:
|
|
12
|
+
|
|
13
|
+
base_url: str = "https://api.datadoghq.com"
|
|
14
|
+
timeout_seconds: float = 15
|
|
15
|
+
max_response_bytes: int = 2097152
|
|
16
|
+
|
|
17
|
+
setattr(Config, '__module__', 'dex_connectors.datadog.client')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Credentials:
|
|
22
|
+
|
|
23
|
+
api_key: SecretString
|
|
24
|
+
application_key: SecretString
|
|
25
|
+
|
|
26
|
+
setattr(Credentials, '__module__', 'dex_connectors.datadog.client')
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from dex_connectors.sdk import ConnectionRef, Query, Call, SecretString
|
|
9
|
+
from dex_connectors.sdk.projectconfig import ConnectionKey, LoadedProject
|
|
10
|
+
|
|
11
|
+
from ..client import Client, Config, Credentials
|
|
12
|
+
from .definitions import CONNECTOR_ID, LIST_MONITORS_DEFINITION, QUERY_METRICS_DEFINITION
|
|
13
|
+
from ..models import ListMonitorsInput, MetricsQuery, MonitorsPage, QueryMetricsInput
|
|
14
|
+
|
|
15
|
+
class Connection:
|
|
16
|
+
|
|
17
|
+
__slots__ = ('_query_metrics', '_list_monitors', '_client', 'reference')
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
reference: ConnectionRef,
|
|
21
|
+
query_metrics: Query[QueryMetricsInput, MetricsQuery],
|
|
22
|
+
list_monitors: Query[ListMonitorsInput, MonitorsPage],
|
|
23
|
+
client: Client | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
reference.validate()
|
|
26
|
+
if reference.provider != CONNECTOR_ID or query_metrics is None or list_monitors is None:
|
|
27
|
+
raise ValueError("Datadog connection provider and operations are required")
|
|
28
|
+
if query_metrics.definition().operation != QUERY_METRICS_DEFINITION.operation:
|
|
29
|
+
raise ValueError("Datadog connection operation identity differs")
|
|
30
|
+
if list_monitors.definition().operation != LIST_MONITORS_DEFINITION.operation:
|
|
31
|
+
raise ValueError("Datadog connection operation identity differs")
|
|
32
|
+
self.reference = reference
|
|
33
|
+
self._query_metrics = query_metrics
|
|
34
|
+
self._list_monitors = list_monitors
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
def query_metrics(self) -> Query[QueryMetricsInput, MetricsQuery]:
|
|
38
|
+
return self._query_metrics
|
|
39
|
+
|
|
40
|
+
def list_monitors(self) -> Query[ListMonitorsInput, MonitorsPage]:
|
|
41
|
+
return self._list_monitors
|
|
42
|
+
|
|
43
|
+
async def aclose(self) -> None:
|
|
44
|
+
if self._client is not None:
|
|
45
|
+
await self._client.aclose()
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return "datadog.Connection([REDACTED])"
|
|
49
|
+
|
|
50
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
51
|
+
raise TypeError("Datadog connections cannot be serialized")
|
|
52
|
+
|
|
53
|
+
setattr(Connection, '__module__', 'dex_connectors.datadog.connection')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def new_connection(client: Client, reference: ConnectionRef) -> Connection:
|
|
57
|
+
if client is None:
|
|
58
|
+
raise ValueError("Datadog client is required")
|
|
59
|
+
return Connection(reference, client.query_metrics(), client.list_monitors(), client)
|
|
60
|
+
|
|
61
|
+
setattr(new_connection, '__module__', 'dex_connectors.datadog.connection')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def new_connection_with_operations(
|
|
65
|
+
query_metrics: Query[QueryMetricsInput, MetricsQuery],
|
|
66
|
+
list_monitors: Query[ListMonitorsInput, MonitorsPage],
|
|
67
|
+
reference: ConnectionRef,
|
|
68
|
+
) -> Connection:
|
|
69
|
+
return Connection(reference, query_metrics, list_monitors)
|
|
70
|
+
|
|
71
|
+
setattr(new_connection_with_operations, '__module__', 'dex_connectors.datadog.connection')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _ProjectCredentialSource:
|
|
75
|
+
|
|
76
|
+
def __init__(self, project: LoadedProject, key: ConnectionKey) -> None:
|
|
77
|
+
self.project = project
|
|
78
|
+
self.key = key
|
|
79
|
+
|
|
80
|
+
async def resolve(self, call: Call) -> Credentials:
|
|
81
|
+
material = await self.project.resolve_credential(self.key, str(call.id))
|
|
82
|
+
api_key = material.get("api_key")
|
|
83
|
+
if not isinstance(api_key, str) or not api_key:
|
|
84
|
+
raise ValueError("Datadog credential api_key is required")
|
|
85
|
+
application_key = material.get("application_key")
|
|
86
|
+
if not isinstance(application_key, str) or not application_key:
|
|
87
|
+
raise ValueError("Datadog credential application_key is required")
|
|
88
|
+
return Credentials(SecretString(api_key), SecretString(application_key))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def new_project_connection(
|
|
92
|
+
project: LoadedProject, connection_name: str, http_client: httpx.AsyncClient | None = None
|
|
93
|
+
) -> Connection:
|
|
94
|
+
if project is None:
|
|
95
|
+
raise ValueError("Datadog connection requires loaded project configuration")
|
|
96
|
+
reference = ConnectionRef(CONNECTOR_ID, connection_name)
|
|
97
|
+
reference.validate()
|
|
98
|
+
key = ConnectionKey(CONNECTOR_ID, connection_name)
|
|
99
|
+
matching = [
|
|
100
|
+
entry
|
|
101
|
+
for entry in project.configuration.connections
|
|
102
|
+
if entry.connector_id == CONNECTOR_ID and entry.connection_name == connection_name
|
|
103
|
+
]
|
|
104
|
+
if len(matching) != 1:
|
|
105
|
+
raise ValueError("Datadog project connection is missing or duplicated")
|
|
106
|
+
if matching[0].provider != CONNECTOR_ID:
|
|
107
|
+
raise ValueError("Datadog project connection provider differs")
|
|
108
|
+
config = _decode_config(matching[0].configuration)
|
|
109
|
+
client = Client(config, _ProjectCredentialSource(project, key), http_client)
|
|
110
|
+
return new_connection(client, reference)
|
|
111
|
+
|
|
112
|
+
setattr(new_project_connection, '__module__', 'dex_connectors.datadog.connection')
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _decode_config(value: dict[str, Any]) -> Config:
|
|
116
|
+
if set(value) - {"baseUrl", "timeout", "maxResponseBytes"}:
|
|
117
|
+
raise ValueError("Datadog configuration has unknown fields")
|
|
118
|
+
config_arguments: dict[str, Any] = {}
|
|
119
|
+
if "baseUrl" in value:
|
|
120
|
+
base_url = value["baseUrl"]
|
|
121
|
+
if not (isinstance(base_url, str)):
|
|
122
|
+
raise ValueError("Datadog configuration baseUrl has an invalid type")
|
|
123
|
+
config_arguments['base_url'] = base_url
|
|
124
|
+
if "timeout" in value:
|
|
125
|
+
timeout = value["timeout"]
|
|
126
|
+
if not (type(timeout) is int):
|
|
127
|
+
raise ValueError("Datadog configuration timeout has an invalid type")
|
|
128
|
+
config_arguments['timeout_seconds'] = timeout / 1_000_000_000
|
|
129
|
+
if "maxResponseBytes" in value:
|
|
130
|
+
max_response_bytes = value["maxResponseBytes"]
|
|
131
|
+
if not (type(max_response_bytes) is int):
|
|
132
|
+
raise ValueError("Datadog configuration maxResponseBytes has an invalid type")
|
|
133
|
+
config_arguments['max_response_bytes'] = max_response_bytes
|
|
134
|
+
return Config(**config_arguments)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import (
|
|
8
|
+
BranchDefinition,
|
|
9
|
+
MutationDefinition,
|
|
10
|
+
OperationRef,
|
|
11
|
+
QueryDefinition,
|
|
12
|
+
RetryPolicy,
|
|
13
|
+
StepDefaults,
|
|
14
|
+
default_connector_step_execute_retry_policy,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CONNECTOR_ID = "datadog"
|
|
18
|
+
PROVIDER = "datadog"
|
|
19
|
+
QUERY_METRICS_OPERATION_ID = "queryMetrics"
|
|
20
|
+
LIST_MONITORS_OPERATION_ID = "listMonitors"
|
|
21
|
+
DEFECT = "defect"
|
|
22
|
+
INVALID_RESPONSE = "invalidResponse"
|
|
23
|
+
LISTED = "listed"
|
|
24
|
+
PROVIDER_REJECTED = "providerRejected"
|
|
25
|
+
QUERIED = "queried"
|
|
26
|
+
|
|
27
|
+
QUERY_METRICS_DEFINITION = QueryDefinition(
|
|
28
|
+
OperationRef(CONNECTOR_ID, QUERY_METRICS_OPERATION_ID),
|
|
29
|
+
(
|
|
30
|
+
BranchDefinition(QUERIED, "Query Datadog timeseries points over an explicit Unix-second interval.", False),
|
|
31
|
+
BranchDefinition(PROVIDER_REJECTED, "The provider rejected the request.", True),
|
|
32
|
+
BranchDefinition(INVALID_RESPONSE, "The provider returned malformed or oversized JSON.", True),
|
|
33
|
+
BranchDefinition(DEFECT, "Local input or connection credentials are invalid.", True),
|
|
34
|
+
),
|
|
35
|
+
StepDefaults(
|
|
36
|
+
timedelta(seconds=30),
|
|
37
|
+
2,
|
|
38
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
LIST_MONITORS_DEFINITION = QueryDefinition(
|
|
43
|
+
OperationRef(CONNECTOR_ID, LIST_MONITORS_OPERATION_ID),
|
|
44
|
+
(
|
|
45
|
+
BranchDefinition(LISTED, "List one page of Datadog monitors.", False),
|
|
46
|
+
BranchDefinition(PROVIDER_REJECTED, "The provider rejected the request.", True),
|
|
47
|
+
BranchDefinition(INVALID_RESPONSE, "The provider returned malformed or oversized JSON.", True),
|
|
48
|
+
BranchDefinition(DEFECT, "Local input or connection credentials are invalid.", True),
|
|
49
|
+
),
|
|
50
|
+
StepDefaults(
|
|
51
|
+
timedelta(seconds=30),
|
|
52
|
+
2,
|
|
53
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import ConnectionRef, Failure, FailureKind
|
|
8
|
+
from dex_connectors.sdk.connectormock import Case, Query
|
|
9
|
+
|
|
10
|
+
from ..connection import Connection, new_connection_with_operations
|
|
11
|
+
from ..models import ListMonitorsInput, MetricSeries, MetricsQuery, Monitor, MonitorsPage, QueryMetricsInput
|
|
12
|
+
from .definitions import CONNECTOR_ID, DEFECT, INVALID_RESPONSE, LISTED, LIST_MONITORS_DEFINITION, PROVIDER_REJECTED, QUERIED, QUERY_METRICS_DEFINITION
|
|
13
|
+
|
|
14
|
+
def query_metrics_queried(value: MetricsQuery, failure: Failure | None = None) -> Case[MetricsQuery]:
|
|
15
|
+
return Case.branch_result(QUERIED, value, failure)
|
|
16
|
+
|
|
17
|
+
setattr(query_metrics_queried, '__module__', "dex_connectors.datadog.datadogmock")
|
|
18
|
+
|
|
19
|
+
def query_metrics_provider_rejected(value: MetricsQuery, failure: Failure | None = None) -> Case[MetricsQuery]:
|
|
20
|
+
return Case.branch_result(PROVIDER_REJECTED, value, failure)
|
|
21
|
+
|
|
22
|
+
setattr(query_metrics_provider_rejected, '__module__', "dex_connectors.datadog.datadogmock")
|
|
23
|
+
|
|
24
|
+
def query_metrics_invalid_response(value: MetricsQuery, failure: Failure | None = None) -> Case[MetricsQuery]:
|
|
25
|
+
return Case.branch_result(INVALID_RESPONSE, value, failure)
|
|
26
|
+
|
|
27
|
+
setattr(query_metrics_invalid_response, '__module__', "dex_connectors.datadog.datadogmock")
|
|
28
|
+
|
|
29
|
+
def query_metrics_defect(value: MetricsQuery, failure: Failure | None = None) -> Case[MetricsQuery]:
|
|
30
|
+
return Case.branch_result(DEFECT, value, failure)
|
|
31
|
+
|
|
32
|
+
setattr(query_metrics_defect, '__module__', "dex_connectors.datadog.datadogmock")
|
|
33
|
+
|
|
34
|
+
def query_metrics_retry(failure: Failure, after_seconds: float = 0) -> Case[MetricsQuery]:
|
|
35
|
+
return Case.retry(failure, after_seconds)
|
|
36
|
+
|
|
37
|
+
setattr(query_metrics_retry, '__module__', "dex_connectors.datadog.datadogmock")
|
|
38
|
+
|
|
39
|
+
def query_metrics_queried_default() -> Case[MetricsQuery]:
|
|
40
|
+
return Case.branch_result(QUERIED, MetricsQuery(status='ok', series=(MetricSeries(metric='system.cpu.user', pointlist=((1761436800000, 0.23),)),)))
|
|
41
|
+
|
|
42
|
+
setattr(query_metrics_queried_default, '__module__', "dex_connectors.datadog.datadogmock")
|
|
43
|
+
|
|
44
|
+
def list_monitors_listed(value: MonitorsPage, failure: Failure | None = None) -> Case[MonitorsPage]:
|
|
45
|
+
return Case.branch_result(LISTED, value, failure)
|
|
46
|
+
|
|
47
|
+
setattr(list_monitors_listed, '__module__', "dex_connectors.datadog.datadogmock")
|
|
48
|
+
|
|
49
|
+
def list_monitors_provider_rejected(value: MonitorsPage, failure: Failure | None = None) -> Case[MonitorsPage]:
|
|
50
|
+
return Case.branch_result(PROVIDER_REJECTED, value, failure)
|
|
51
|
+
|
|
52
|
+
setattr(list_monitors_provider_rejected, '__module__', "dex_connectors.datadog.datadogmock")
|
|
53
|
+
|
|
54
|
+
def list_monitors_invalid_response(value: MonitorsPage, failure: Failure | None = None) -> Case[MonitorsPage]:
|
|
55
|
+
return Case.branch_result(INVALID_RESPONSE, value, failure)
|
|
56
|
+
|
|
57
|
+
setattr(list_monitors_invalid_response, '__module__', "dex_connectors.datadog.datadogmock")
|
|
58
|
+
|
|
59
|
+
def list_monitors_defect(value: MonitorsPage, failure: Failure | None = None) -> Case[MonitorsPage]:
|
|
60
|
+
return Case.branch_result(DEFECT, value, failure)
|
|
61
|
+
|
|
62
|
+
setattr(list_monitors_defect, '__module__', "dex_connectors.datadog.datadogmock")
|
|
63
|
+
|
|
64
|
+
def list_monitors_retry(failure: Failure, after_seconds: float = 0) -> Case[MonitorsPage]:
|
|
65
|
+
return Case.retry(failure, after_seconds)
|
|
66
|
+
|
|
67
|
+
setattr(list_monitors_retry, '__module__', "dex_connectors.datadog.datadogmock")
|
|
68
|
+
|
|
69
|
+
def list_monitors_listed_default() -> Case[MonitorsPage]:
|
|
70
|
+
return Case.branch_result(LISTED, MonitorsPage(monitors=(Monitor(id=12345, name='Meridian API latency', type='metric alert', overall_state='OK'),)))
|
|
71
|
+
|
|
72
|
+
setattr(list_monitors_listed_default, '__module__', "dex_connectors.datadog.datadogmock")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Mock:
|
|
76
|
+
|
|
77
|
+
def __init__(self, connection_name: str = "datadog") -> None:
|
|
78
|
+
self.query_metrics_script: Query[QueryMetricsInput, MetricsQuery] = Query(QUERY_METRICS_DEFINITION)
|
|
79
|
+
self.query_metrics_script.default(query_metrics_queried_default())
|
|
80
|
+
self.list_monitors_script: Query[ListMonitorsInput, MonitorsPage] = Query(LIST_MONITORS_DEFINITION)
|
|
81
|
+
self.list_monitors_script.default(list_monitors_listed_default())
|
|
82
|
+
self._connection = new_connection_with_operations(
|
|
83
|
+
query_metrics=self.query_metrics_script,
|
|
84
|
+
list_monitors=self.list_monitors_script,
|
|
85
|
+
reference=ConnectionRef(CONNECTOR_ID, connection_name),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def connection(self) -> Connection:
|
|
89
|
+
return self._connection
|
|
90
|
+
|
|
91
|
+
setattr(Mock, '__module__', "dex_connectors.datadog.datadogmock")
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"Mock",
|
|
95
|
+
"query_metrics_queried",
|
|
96
|
+
"query_metrics_provider_rejected",
|
|
97
|
+
"query_metrics_invalid_response",
|
|
98
|
+
"query_metrics_defect",
|
|
99
|
+
"query_metrics_retry",
|
|
100
|
+
"query_metrics_queried_default",
|
|
101
|
+
"list_monitors_listed",
|
|
102
|
+
"list_monitors_provider_rejected",
|
|
103
|
+
"list_monitors_invalid_response",
|
|
104
|
+
"list_monitors_defect",
|
|
105
|
+
"list_monitors_retry",
|
|
106
|
+
"list_monitors_listed_default",
|
|
107
|
+
]
|