Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use order key in query header if defined #149

Merged
merged 35 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8050e57
Add integration test for widget with config header
JCZuurmond Jun 12, 2024
1386c01
Add unit test for custom sized widget
JCZuurmond Jun 12, 2024
88550c2
Add flow for fallback metadata
JCZuurmond Jun 12, 2024
b0e59f4
Rename method
JCZuurmond Jun 12, 2024
1b6fe94
Format
JCZuurmond Jun 12, 2024
c58b3d8
Parse flags from query header
JCZuurmond Jun 12, 2024
0182a15
Add unit test for replace WidgetMetadata from arguments
JCZuurmond Jun 12, 2024
957dc23
Test for multiple headers
JCZuurmond Jun 12, 2024
7e319cf
Handle invalid header
JCZuurmond Jun 12, 2024
069eddc
Add test for comment being on other line
JCZuurmond Jun 12, 2024
9e66a9f
Test first line command only processed
JCZuurmond Jun 12, 2024
4a9112c
Test non-header comment
JCZuurmond Jun 12, 2024
c32de8b
Update docs on the widget arguments
JCZuurmond Jun 12, 2024
1875107
Rename variable
JCZuurmond Jun 12, 2024
635a3c7
Format
JCZuurmond Jun 12, 2024
2780a9d
Add as dict method
JCZuurmond Jun 12, 2024
58e6f02
Format
JCZuurmond Jun 12, 2024
0f9c09b
Use make dashboard fixture
JCZuurmond Jun 12, 2024
da72b6b
Set back parse dashboard metadata
JCZuurmond Jun 12, 2024
f0f5b15
Fix wording
JCZuurmond Jun 12, 2024
2ff3432
Fix unused argument
JCZuurmond Jun 12, 2024
3f8d257
Add integration test for widget with config header
JCZuurmond Jun 12, 2024
8dd76df
Add unit test for custom sized widget
JCZuurmond Jun 12, 2024
c1f958f
Add flow for fallback metadata
JCZuurmond Jun 12, 2024
f6999d6
Rename method
JCZuurmond Jun 12, 2024
c7f88fc
Parse flags from query header
JCZuurmond Jun 12, 2024
c1a5a3b
Add unit test for replace WidgetMetadata from arguments
JCZuurmond Jun 12, 2024
4ab4b48
Handle invalid header
JCZuurmond Jun 12, 2024
307c77b
Add test for comment being on other line
JCZuurmond Jun 12, 2024
c2614de
Test non-header comment
JCZuurmond Jun 12, 2024
604acce
Implement widget ordering
JCZuurmond Jun 12, 2024
f14af88
Fix unit tests
JCZuurmond Jun 12, 2024
b2682e7
Add integration test for widget ordering
JCZuurmond Jun 12, 2024
989d2fe
Fix rebase problems
JCZuurmond Jun 12, 2024
2753d39
Rename variable
JCZuurmond Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions src/databricks/labs/lsql/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import shlex
from argparse import ArgumentParser
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import TypeVar
Expand Down Expand Up @@ -50,6 +51,7 @@ def as_dict(self) -> dict[str, str]:

@dataclass
class WidgetMetadata:
order: int
width: int
height: int
JCZuurmond marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -59,6 +61,7 @@ def as_dict(self) -> dict[str, str]:
@staticmethod
def _get_arguments_parser() -> ArgumentParser:
parser = ArgumentParser("WidgetMetadata", add_help=False, exit_on_error=False)
parser.add_argument("-o", "--order", type=int)
parser.add_argument("-w", "--width", type=int)
parser.add_argument("-h", "--height", type=int)
return parser
Expand All @@ -72,6 +75,7 @@ def replace_from_arguments(self, arguments: list[str]) -> "WidgetMetadata":
return dataclasses.replace(self)
return dataclasses.replace(
self,
order=args.order or self.order,
width=args.width or self.width,
height=args.height or self.height,
)
Expand Down Expand Up @@ -125,19 +129,32 @@ def _format_query(query: str) -> str:
return formatted_query

def create_dashboard(self, dashboard_folder: Path) -> Dashboard:
"""Create a dashboard from code, i.e. metadata and queries."""
"""Create a dashboard from code, i.e. configuration and queries."""
dashboard_metadata = self._parse_dashboard_metadata(dashboard_folder)
datasets = self._get_datasets(dashboard_folder)
widgets = self._get_widgets(dashboard_folder.iterdir(), datasets)
layouts = self._get_layouts(widgets)
page = Page(
name=dashboard_metadata.display_name,
display_name=dashboard_metadata.display_name,
layout=layouts,
)
lakeview_dashboard = Dashboard(datasets=datasets, pages=[page])
return lakeview_dashboard

position = Position(0, 0, 0, 0) # First widget position
datasets, layouts = [], []
@staticmethod
def _get_datasets(dashboard_folder: Path) -> list[Dataset]:
datasets = []
for query_path in sorted(dashboard_folder.glob("*.sql")):
with query_path.open("r") as query_file:
raw_query = query_file.read()
dataset = Dataset(name=query_path.stem, display_name=query_path.stem, query=raw_query)
datasets.append(dataset)
return datasets

dataset_index = 0
for path in sorted(dashboard_folder.iterdir()):
def _get_widgets(self, files: Iterable[Path], datasets: list[Dataset]) -> list[tuple[Widget, WidgetMetadata]]:
dataset_index, widgets = 0, []
for order, path in enumerate(sorted(files)):
if path.suffix not in {".sql", ".md"}:
continue
if path.suffix == ".sql":
Expand All @@ -151,18 +168,17 @@ def create_dashboard(self, dashboard_folder: Path) -> Dashboard:
continue
else:
widget = self._get_text_widget(path)
widget_metadata = self._parse_widget_metadata(path, widget)
widget_metadata = self._parse_widget_metadata(path, widget, order)
widgets.append((widget, widget_metadata))
return widgets

def _get_layouts(self, widgets: list[tuple[Widget, WidgetMetadata]]) -> list[Layout]:
layouts, position = [], Position(0, 0, 0, 0) # First widget position
for widget, widget_metadata in sorted(widgets, key=lambda w: (w[1].order, w[0].name)):
position = self._get_position(widget_metadata, position)
layout = Layout(widget=widget, position=position)
layouts.append(layout)

page = Page(
name=dashboard_metadata.display_name,
display_name=dashboard_metadata.display_name,
layout=layouts,
)
lakeview_dashboard = Dashboard(datasets=datasets, pages=[page])
return lakeview_dashboard
return layouts

@staticmethod
def _parse_dashboard_metadata(dashboard_folder: Path) -> DashboardMetadata:
Expand All @@ -183,9 +199,13 @@ def _parse_dashboard_metadata(dashboard_folder: Path) -> DashboardMetadata:
logger.warning(f"Parsing {dashboard_metadata_path}: {e}")
return fallback_metadata

def _parse_widget_metadata(self, path: Path, widget: Widget) -> WidgetMetadata:
def _parse_widget_metadata(self, path: Path, widget: Widget, order: int) -> WidgetMetadata:
width, height = self._get_width_and_height(widget)
fallback_metadata = WidgetMetadata(width, height)
fallback_metadata = WidgetMetadata(
order=order,
width=width,
height=height,
)

try:
parsed_query = sqlglot.parse_one(path.read_text(), dialect=sqlglot.dialects.Databricks)
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/test_dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,23 @@ def test_dashboard_deploys_dashboard_with_big_widget(ws, make_dashboard, tmp_pat
sdk_dashboard = dashboards.deploy_dashboard(lakeview_dashboard, dashboard_id=sdk_dashboard.dashboard_id)

assert ws.lakeview.get(sdk_dashboard.dashboard_id)


def test_dashboards_deploys_dashboard_with_order_overwrite(ws, make_dashboard, tmp_path):
sdk_dashboard = make_dashboard()

for query_name in range(6):
with (tmp_path / f"{query_name}.sql").open("w") as f:
f.write(f"SELECT {query_name} AS count")

# Move the '4' inbetween '1' and '2' query. Note that the order 1 puts '4' on the same position as '1', but with an
# order tiebreaker the query name decides the final order.
with (tmp_path / "4.sql").open("w") as f:
f.write("-- --order 1\nSELECT 4 AS count")

dashboards = Dashboards(ws)
lakeview_dashboard = dashboards.create_dashboard(tmp_path)

sdk_dashboard = dashboards.deploy_dashboard(lakeview_dashboard, dashboard_id=sdk_dashboard.dashboard_id)

assert ws.lakeview.get(sdk_dashboard.dashboard_id)
29 changes: 24 additions & 5 deletions tests/unit/test_dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ def test_dashboard_configuration_from_and_as_dict_is_a_unit_function():


def test_widget_metadata_replaces_arguments():
widget_metadata = WidgetMetadata(1, 1)
widget_metadata = WidgetMetadata(1, 1, 1)
updated_metadata = widget_metadata.replace_from_arguments(["--width", "10", "--height", "10"])
assert updated_metadata.width == 10
assert updated_metadata.height == 10


@pytest.mark.parametrize("attribute", ["width", "height"])
@pytest.mark.parametrize("attribute", ["order", "width", "height"])
def test_widget_metadata_replaces_one_attribute(attribute: str):
widget_metadata = WidgetMetadata(1, 1)
widget_metadata = WidgetMetadata(1, 1, 1)
updated_metadata = widget_metadata.replace_from_arguments([f"--{attribute}", "10"])

other_fields = [field for field in dataclasses.fields(updated_metadata) if field.name != attribute]
Expand All @@ -59,8 +59,8 @@ def test_widget_metadata_replaces_one_attribute(attribute: str):


def test_widget_metadata_as_dict():
raw = {"width": 10, "height": 10}
widget_metadata = WidgetMetadata(10, 10)
raw = {"order": 10, "width": 10, "height": 10}
widget_metadata = WidgetMetadata(10, 10, 10)
assert widget_metadata.as_dict() == raw


Expand Down Expand Up @@ -324,6 +324,25 @@ def test_dashboards_creates_dashboards_with_widgets_sorted_alphanumerically(tmp_
ws.assert_not_called()


def test_dashboards_creates_dashboards_with_widgets_order_overwrite(tmp_path):
ws = create_autospec(WorkspaceClient)

for query_name in "abcdf":
with (tmp_path / f"{query_name}.sql").open("w") as f:
f.write("SELECT 1 AS count")

# Move the 'e' inbetween 'b' and 'c' query. Note that the order 1 puts 'e' on the same position as 'b', but with an
# order tiebreaker the query name decides the final order.
with (tmp_path / "e.sql").open("w") as f:
f.write("-- --order 1\nSELECT 1 AS count")

lakeview_dashboard = Dashboards(ws).create_dashboard(tmp_path)
widget_names = [layout.widget.name for layout in lakeview_dashboard.pages[0].layout]

assert "".join(widget_names) == "abecdf"
ws.assert_not_called()


@pytest.mark.parametrize("query, width, height", [("SELECT 1 AS count", 1, 3)])
def test_dashboards_creates_dashboards_where_widget_has_expected_width_and_height(tmp_path, query, width, height):
ws = create_autospec(WorkspaceClient)
Expand Down
Loading