Skip to content

Commit 43072d5

Browse files
fix chardEdges destination type validation in DashboardPatchBuilder + add missing case for dashboards
1 parent 6479072 commit 43072d5

File tree

3 files changed

+183
-3
lines changed

3 files changed

+183
-3
lines changed

metadata-ingestion/src/datahub/ingestion/api/incremental_lineage_helper.py

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ def convert_dashboard_info_to_patch(
102102
if aspect.datasets:
103103
patch_builder.add_datasets(aspect.datasets)
104104

105+
if aspect.dashboards:
106+
for dashboard in aspect.dashboards:
107+
patch_builder.add_dashboard(dashboard)
108+
105109
if aspect.access:
106110
patch_builder.set_access(aspect.access)
107111

metadata-ingestion/src/datahub/specific/dashboard.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def add_chart_edge(self, chart: Union[Edge, Urn, str]) -> "DashboardPatchBuilder
161161
lastModified=self._mint_auditstamp(),
162162
)
163163

164-
self._ensure_urn_type("dataset", [chart_edge], "add_chart_edge")
164+
self._ensure_urn_type("chart", [chart_edge], "add_chart_edge")
165165
self._add_patch(
166166
DashboardInfo.ASPECT_NAME,
167167
"add",
@@ -271,6 +271,48 @@ def add_datasets(
271271

272272
return self
273273

274+
def add_dashboard(
275+
self, dashboard: Union[Edge, Urn, str]
276+
) -> "DashboardPatchBuilder":
277+
"""
278+
Adds an dashboard to the DashboardPatchBuilder.
279+
280+
Args:
281+
dashboard: The dashboard, which can be an Edge object, Urn object, or a string.
282+
283+
Returns:
284+
The DashboardPatchBuilder instance.
285+
286+
Raises:
287+
ValueError: If the dashboard is not a Dashboard urn.
288+
289+
Notes:
290+
If `dashboard` is an Edge object, it is used directly. If `dashboard` is a Urn object or string,
291+
it is converted to an Edge object and added with default audit stamps.
292+
"""
293+
if isinstance(dashboard, Edge):
294+
dashboard_urn: str = dashboard.destinationUrn
295+
dashboard_edge: Edge = dashboard
296+
elif isinstance(dashboard, (Urn, str)):
297+
dashboard_urn = str(dashboard)
298+
if not dashboard_urn.startswith("urn:li:dashboard:"):
299+
raise ValueError(f"Input {dashboard} is not a Dashboard urn")
300+
301+
dashboard_edge = Edge(
302+
destinationUrn=dashboard_urn,
303+
created=self._mint_auditstamp(),
304+
lastModified=self._mint_auditstamp(),
305+
)
306+
307+
self._ensure_urn_type("dashboard", [dashboard_edge], "add_dashboard")
308+
self._add_patch(
309+
DashboardInfo.ASPECT_NAME,
310+
"add",
311+
path=("dashboards", dashboard_urn),
312+
value=dashboard_edge,
313+
)
314+
return self
315+
274316
def set_dashboard_url(
275317
self, dashboard_url: Optional[str]
276318
) -> "DashboardPatchBuilder":

metadata-ingestion/tests/unit/api/source_helpers/test_incremental_lineage_helper.py

+136-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
from typing import List
1+
import re
2+
import unittest
3+
from typing import List, Optional
4+
5+
import pytest
26

37
import datahub.emitter.mce_builder as builder
48
import datahub.metadata.schema_classes as models
59
from datahub.emitter.mce_builder import make_dataset_urn, make_schema_field_urn
610
from datahub.emitter.mcp import MetadataChangeProposalWrapper
7-
from datahub.ingestion.api.incremental_lineage_helper import auto_incremental_lineage
11+
from datahub.ingestion.api.incremental_lineage_helper import (
12+
auto_incremental_lineage,
13+
convert_dashboard_info_to_patch,
14+
)
815
from datahub.ingestion.api.source_helpers import auto_workunit
16+
from datahub.ingestion.api.workunit import MetadataWorkUnit
917
from datahub.ingestion.sink.file import write_metadata_file
1018
from tests.test_helpers import mce_helpers
1119

@@ -183,3 +191,129 @@ def test_incremental_lineage_pass_through(tmp_path, pytestconfig):
183191
mce_helpers.check_golden_file(
184192
pytestconfig=pytestconfig, output_path=test_file, golden_path=golden_file
185193
)
194+
195+
196+
class TestConvertDashboardInfoToPatch(unittest.TestCase):
197+
def setUp(self):
198+
self.urn = builder.make_dashboard_urn(
199+
platform="platform", name="test", platform_instance="instance"
200+
)
201+
self.system_metadata = models.SystemMetadataClass()
202+
self.aspect = models.DashboardInfoClass(
203+
title="Test Dashboard",
204+
description="This is a test dashboard",
205+
lastModified=models.ChangeAuditStampsClass(),
206+
)
207+
208+
def _validate_dashboard_info_patch(
209+
self, result: Optional[MetadataWorkUnit], expected_aspect_value: bytes
210+
) -> None:
211+
assert (
212+
result
213+
and result.metadata
214+
and isinstance(result.metadata, models.MetadataChangeProposalClass)
215+
)
216+
mcp: models.MetadataChangeProposalClass = result.metadata
217+
assert (
218+
mcp.entityUrn == self.urn
219+
and mcp.entityType == "dashboard"
220+
and mcp.aspectName == "dashboardInfo"
221+
and mcp.changeType == "PATCH"
222+
)
223+
assert mcp.aspect and isinstance(mcp.aspect, models.GenericAspectClass)
224+
aspect: models.GenericAspectClass = mcp.aspect
225+
assert aspect.value == expected_aspect_value
226+
227+
def test_convert_dashboard_info_to_patch_with_no_additional_values(self):
228+
result = convert_dashboard_info_to_patch(
229+
self.urn, self.aspect, self.system_metadata
230+
)
231+
self._validate_dashboard_info_patch(
232+
result=result,
233+
expected_aspect_value=b'[{"op": "add", "path": "/title", "value": "Test Dashboard"}, {"op": "add", "path": "/description", "value": "This is a test dashboard"}, {"op": "add", "path": "/lastModified", "value": {"created": {"time": 0, "actor": "urn:li:corpuser:unknown"}, "lastModified": {"time": 0, "actor": "urn:li:corpuser:unknown"}}}]',
234+
)
235+
236+
def test_convert_dashboard_info_to_patch_with_custom_properties(self):
237+
self.aspect.customProperties = {
238+
"key1": "value1",
239+
"key2": "value2",
240+
}
241+
result = convert_dashboard_info_to_patch(
242+
self.urn, self.aspect, self.system_metadata
243+
)
244+
self._validate_dashboard_info_patch(
245+
result=result,
246+
expected_aspect_value=b'[{"op": "add", "path": "/customProperties/key1", "value": "value1"}, {"op": "add", "path": "/customProperties/key2", "value": "value2"}, {"op": "add", "path": "/title", "value": "Test Dashboard"}, {"op": "add", "path": "/description", "value": "This is a test dashboard"}, {"op": "add", "path": "/lastModified", "value": {"created": {"time": 0, "actor": "urn:li:corpuser:unknown"}, "lastModified": {"time": 0, "actor": "urn:li:corpuser:unknown"}}}]',
247+
)
248+
249+
def test_convert_dashboard_info_to_patch_with_chart_edges(self):
250+
self.aspect.chartEdges = [
251+
models.EdgeClass(
252+
destinationUrn=builder.make_chart_urn(
253+
platform="platform",
254+
name="target-test",
255+
platform_instance="instance",
256+
),
257+
)
258+
]
259+
result = convert_dashboard_info_to_patch(
260+
self.urn, self.aspect, self.system_metadata
261+
)
262+
self._validate_dashboard_info_patch(
263+
result,
264+
b'[{"op": "add", "path": "/chartEdges/urn:li:chart:(platform,instance.target-test)", "value": {"destinationUrn": "urn:li:chart:(platform,instance.target-test)"}}, {"op": "add", "path": "/title", "value": "Test Dashboard"}, {"op": "add", "path": "/description", "value": "This is a test dashboard"}, {"op": "add", "path": "/lastModified", "value": {"created": {"time": 0, "actor": "urn:li:corpuser:unknown"}, "lastModified": {"time": 0, "actor": "urn:li:corpuser:unknown"}}}]',
265+
)
266+
267+
def test_convert_dashboard_info_to_patch_with_chart_edges_no_chart(self):
268+
self.aspect.chartEdges = [
269+
models.EdgeClass(
270+
destinationUrn=builder.make_dashboard_urn(
271+
platform="platform",
272+
name="target-test",
273+
platform_instance="instance",
274+
),
275+
)
276+
]
277+
with pytest.raises(
278+
ValueError,
279+
match=re.escape(
280+
"add_chart_edge: urn:li:dashboard:(platform,instance.target-test) is not of type chart"
281+
),
282+
):
283+
convert_dashboard_info_to_patch(self.urn, self.aspect, self.system_metadata)
284+
285+
def test_convert_dashboard_info_to_patch_with_dashboards(self):
286+
self.aspect.dashboards = [
287+
models.EdgeClass(
288+
destinationUrn=builder.make_dashboard_urn(
289+
platform="platform",
290+
name="target-test",
291+
platform_instance="instance",
292+
),
293+
)
294+
]
295+
result = convert_dashboard_info_to_patch(
296+
self.urn, self.aspect, self.system_metadata
297+
)
298+
self._validate_dashboard_info_patch(
299+
result,
300+
b'[{"op": "add", "path": "/title", "value": "Test Dashboard"}, {"op": "add", "path": "/description", "value": "This is a test dashboard"}, {"op": "add", "path": "/dashboards/urn:li:dashboard:(platform,instance.target-test)", "value": {"destinationUrn": "urn:li:dashboard:(platform,instance.target-test)"}}, {"op": "add", "path": "/lastModified", "value": {"created": {"time": 0, "actor": "urn:li:corpuser:unknown"}, "lastModified": {"time": 0, "actor": "urn:li:corpuser:unknown"}}}]',
301+
)
302+
303+
def test_convert_dashboard_info_to_patch_with_dashboards_no_dashboard(self):
304+
self.aspect.dashboards = [
305+
models.EdgeClass(
306+
destinationUrn=builder.make_chart_urn(
307+
platform="platform",
308+
name="target-test",
309+
platform_instance="instance",
310+
),
311+
)
312+
]
313+
with pytest.raises(
314+
ValueError,
315+
match=re.escape(
316+
"add_dashboard: urn:li:chart:(platform,instance.target-test) is not of type dashboard"
317+
),
318+
):
319+
convert_dashboard_info_to_patch(self.urn, self.aspect, self.system_metadata)

0 commit comments

Comments
 (0)