Skip to content

Commit 4c34211

Browse files
authored
chore(deps): Address dependabot and pip-audit findings (#111)
* chore(deps): Address dependabot and pip-audit findings * fix(chart): handle UUID columns for VegaFusion across pandas and polars * ci: bump codecov-action to v5.5.5 (fix keybase GPG verification)
1 parent 3967dd5 commit 4c34211

6 files changed

Lines changed: 478 additions & 297 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ jobs:
411411
coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
412412
413413
- name: Upload combined coverage to Codecov
414-
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
414+
uses: codecov/codecov-action@5975040f7f7d40edaff8d784b576fd65ae95c073 # v5.5.5
415415
with:
416416
token: ${{ secrets.CODECOV_TOKEN }}
417417
slug: ${{ github.repository }}

deepnote_toolkit/chart/deepnote_chart.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from deepnote_toolkit.chart.types import CHART_ROW_LIMIT, VEGA_5_MIME_TYPE, ChartError
1818
from deepnote_toolkit.chart.utils import (
1919
sanitize_dataframe_for_chart,
20+
sanitize_polars_dataframe_for_chart,
2021
serialize_values_list_for_json,
2122
)
2223
from deepnote_toolkit.logging import LoggerManager
@@ -141,11 +142,18 @@ def __init__(
141142
if filtered_df.native_type == "pandas":
142143
sanitized_pandas = sanitize_dataframe_for_chart(filtered_df.to_native())
143144
oc_sanitized_df = oc.DataFrame.from_native(sanitized_pandas)
144-
elif filtered_df.native_type in ("pyspark", "polars-eager"):
145-
# We don't need to sanitize Spark DFs because they will processed by Spark itself and it can handle
146-
# all data types by itself
147-
# Polars is powered by Arrow, which is same format used internally by VegaFusion so there is no need
148-
# to do any additional sanitization for it either
145+
elif filtered_df.native_type == "polars-eager":
146+
# Polars is Arrow-backed, so most types pass through to VegaFusion
147+
# untouched. The exception is Object columns (e.g. uuid.UUID values),
148+
# which convert to an Arrow type VegaFusion can't serialize, so they
149+
# still need sanitizing.
150+
sanitized_polars = sanitize_polars_dataframe_for_chart(
151+
filtered_df.to_native()
152+
)
153+
oc_sanitized_df = oc.DataFrame.from_native(sanitized_polars)
154+
elif filtered_df.native_type == "pyspark":
155+
# Spark processes the data itself and handles all data types, so no
156+
# sanitization is needed here.
149157
oc_sanitized_df = filtered_df
150158
else:
151159
raise TypeError(

deepnote_toolkit/chart/utils.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
1-
from typing import Any, List, Optional
1+
import uuid
2+
from typing import TYPE_CHECKING, Any, List, Optional
23

34
import pandas as pd
45

56
import deepnote_toolkit.ocelots as oc
67

8+
if TYPE_CHECKING:
9+
import polars as pl
10+
711

812
def sanitize_dataframe_for_chart(pd_df: pd.DataFrame):
913
sanitized_dataframe = pd_df.copy()
1014

1115
oc.pandas.utils.deduplicate_columns(sanitized_dataframe)
1216
_convert_timedelta_columns_to_seconds(sanitized_dataframe)
17+
_convert_uuid_columns_to_string(sanitized_dataframe)
1318
_convert_column_names_to_string(sanitized_dataframe)
1419

1520
return sanitized_dataframe
1621

1722

23+
def sanitize_polars_dataframe_for_chart(pl_df: "pl.DataFrame") -> "pl.DataFrame":
24+
"""
25+
Coerce polars columns that VegaFusion cannot serialize into chart-friendly
26+
types, returning a new DataFrame.
27+
28+
polars stores values it has no native type for (e.g. ``uuid.UUID`` objects)
29+
in an ``Object`` column, which converts to an opaque Arrow ``FixedSizeBinary``
30+
that VegaFusion cannot serialize to JSON. Such columns are not meaningfully
31+
chartable as-is, so we stringify them -- the polars analogue of the UUID
32+
handling in :func:`sanitize_dataframe_for_chart` for the pandas path.
33+
"""
34+
import polars as pl
35+
36+
object_columns = [
37+
name for name, dtype in zip(pl_df.columns, pl_df.dtypes) if dtype == pl.Object
38+
]
39+
if not object_columns:
40+
return pl_df
41+
42+
return pl_df.with_columns(
43+
pl.col(name).map_elements(str, return_dtype=pl.String)
44+
for name in object_columns
45+
)
46+
47+
1848
def _convert_column_names_to_string(pd_df: pd.DataFrame):
1949
"""
2050
Converts dataframe column names to strings.
@@ -24,6 +54,32 @@ def _convert_column_names_to_string(pd_df: pd.DataFrame):
2454
pd_df.columns = pd_df.columns.astype(str)
2555

2656

57+
def _convert_uuid_columns_to_string(pd_df: pd.DataFrame):
58+
"""
59+
Converts columns of ``uuid.UUID`` objects to strings.
60+
61+
Starting with pyarrow 24.0.0, Arrow conversion infers the canonical
62+
``arrow.uuid`` extension type (backed by ``FixedSizeBinary(16)``) for object
63+
columns holding ``uuid.UUID`` values; pyarrow <= 23 produced a serializable
64+
result for the same data. VegaFusion's Arrow runtime cannot serialize
65+
``FixedSizeBinary(16)`` to JSON (``Unsupported datatype for JSON
66+
serialization: FixedSizeBinary(16)``), so we stringify such columns to keep
67+
charting working across pyarrow versions.
68+
69+
WARNING: This function modifies the DataFrame in-place.
70+
"""
71+
for column in pd_df.columns:
72+
col = pd_df[column]
73+
if not pd.api.types.is_object_dtype(col.dtype):
74+
continue
75+
non_null = col.dropna()
76+
if non_null.empty or not isinstance(non_null.iloc[0], uuid.UUID):
77+
continue
78+
pd_df[column] = col.map(
79+
lambda value: str(value) if isinstance(value, uuid.UUID) else value
80+
)
81+
82+
2783
def _convert_timedelta_columns_to_seconds(pd_sanitized_df: pd.DataFrame):
2884
"""
2985
Converts timedelta columns to seconds.

0 commit comments

Comments
 (0)