Skip to content

Commit

Permalink
Merge pull request #1881 from neptune-ai/ss/include_plotlyjs
Browse files Browse the repository at this point in the history
Added `**kwargs` to `upload()` and `File.as_html()` methods
  • Loading branch information
kgodlewski authored Oct 29, 2024
2 parents 26e08ab + fbae62f commit 7da7535
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## neptune 1.13.0

### Features
- Added optional `include_plotlyjs` keyword-only parameter to `upload()` and `File.as_html()` methods to fetch Plotly.js library from CDN ([#1881](https://github.com/neptune-ai/neptune-client/pull/1876))

## neptune 1.12.0

### Changes
Expand Down
26 changes: 21 additions & 5 deletions src/neptune/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,18 @@ def __setitem__(self, key: str, value) -> None:
self[key].assign(value)

def __getattr__(self, item: str):
run_level_methods = {"exists", "get_structure", "print_structure", "stop", "sync", "wait"}
run_level_methods = {
"exists",
"get_structure",
"print_structure",
"stop",
"sync",
"wait",
}

if item in run_level_methods:
raise AttributeError(
"You're invoking an object-level method on a handler for a namespace" "inside the object.",
"You're invoking an object-level method on a handler for a namespace inside the object.",
f"""
For example: You're trying run[{self._path}].{item}()
but you probably want run.{item}().
Expand Down Expand Up @@ -217,14 +224,18 @@ def assign(self, value, *, wait: bool = False) -> None:
attr.process_assignment(value, wait=wait)

@check_protected_paths
def upload(self, value, *, wait: bool = False) -> None:
def upload(self, value, *, wait: bool = False, **kwargs) -> None:
"""Uploads the provided file under the specified field path.
Args:
value (str or File): Path to the file to be uploaded or `File` value object.
wait (bool, optional): If `True` the client will wait to send all tracked metadata to the server.
This makes the call synchronous.
Defaults to `False`.
**kwargs: Optional keyword-only arguments.
Currently, the only accepted keyword argument is
[`include_plotlyjs`](https://plotly.com/python-api-reference/generated/plotly.io.write_html.html).
We recommend passing `include_plotlyjs="cdn"` for better performance when using Neptune SaaS.
Examples:
>>> import neptune
Expand All @@ -246,7 +257,7 @@ def upload(self, value, *, wait: bool = False) -> None:
https://docs.neptune.ai/api/field_types#upload
"""
value = FileVal.create_from(value)
value = FileVal.create_from(value, **kwargs)

with self._container.lock():
attr = self._container.get_attribute(self._path)
Expand Down Expand Up @@ -571,7 +582,12 @@ def fetch_last(self):
"""
return self._pass_call_to_attr(function_name="fetch_last")

def fetch_values(self, *, include_timestamp: Optional[bool] = True, progress_bar: Optional[ProgressBarType] = None):
def fetch_values(
self,
*,
include_timestamp: Optional[bool] = True,
progress_bar: Optional[ProgressBarType] = None,
):
"""Fetches all values stored in the series from Neptune.
Available for the following field types:
Expand Down
14 changes: 7 additions & 7 deletions src/neptune/internal/utils/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def get_image_content(image, autoscale=True) -> Optional[bytes]:
return content


def get_html_content(chart) -> Optional[str]:
content = _to_html(chart)
def get_html_content(chart, **kwargs) -> Optional[str]:
content = _to_html(chart, **kwargs)

return content

Expand Down Expand Up @@ -112,14 +112,14 @@ def _image_to_bytes(image, autoscale) -> bytes:
raise TypeError("image is {}".format(type(image)))


def _to_html(chart) -> str:
def _to_html(chart, **kwargs) -> str:
if _is_matplotlib_pyplot(chart):
chart = chart.gcf()

if is_matplotlib_figure(chart):
try:
chart = _matplotlib_to_plotly(chart)
return _export_plotly_figure(chart)
return _export_plotly_figure(chart, **kwargs)
except ImportError:
logger.warning("Plotly not installed. Logging plot as an image.")
return _image_content_to_html(_get_figure_image_data(chart))
Expand All @@ -133,7 +133,7 @@ def _to_html(chart) -> str:
return _export_pandas_dataframe_to_html(chart)

elif is_plotly_figure(chart):
return _export_plotly_figure(chart)
return _export_plotly_figure(chart, **kwargs)

elif is_altair_chart(chart):
return _export_altair_chart(chart)
Expand Down Expand Up @@ -316,9 +316,9 @@ def _export_pandas_dataframe_to_html(table):
return buffer.getvalue()


def _export_plotly_figure(image):
def _export_plotly_figure(image, **kwargs):
buffer = StringIO()
image.write_html(buffer)
image.write_html(buffer, include_plotlyjs=kwargs.get("include_plotlyjs", True))
buffer.seek(0)
return buffer.getvalue()

Expand Down
28 changes: 22 additions & 6 deletions src/neptune/types/atoms/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ def as_image(image, autoscale: bool = True) -> "File":
return File.from_content(content_bytes if content_bytes is not None else b"", extension="png")

@staticmethod
def as_html(chart) -> "File":
def as_html(
chart,
**kwargs,
) -> "File":
"""Converts an object to an HTML File value object.
This way you can upload `Altair`, `Bokeh`, `Plotly`, `Matplotlib`, `Seaborn` interactive charts
Expand All @@ -224,6 +227,10 @@ def as_html(chart) -> "File":
chart: An object to be converted.
Supported are `Altair`, `Bokeh`, `Plotly`, `Matplotlib`, `Seaborn` interactive charts,
and `Pandas` `DataFrame` objects.
**kwargs: Optional keyword-only arguments.
Currently, the only accepted keyword argument is
[`include_plotlyjs`](https://plotly.com/python-api-reference/generated/plotly.io.write_html.html).
We recommend passing `include_plotlyjs="cdn"` for better performance when using Neptune SaaS.
Returns:
``File``: value object with converted object.
Expand Down Expand Up @@ -251,7 +258,7 @@ def as_html(chart) -> "File":
.. _as_html docs page:
https://docs.neptune.ai/api/field_types#as_html
"""
content = get_html_content(chart)
content = get_html_content(chart, **kwargs)
return File.from_content(content if content is not None else "", extension="html")

@staticmethod
Expand Down Expand Up @@ -286,13 +293,13 @@ def as_pickle(obj) -> "File":
return File.from_content(content if content is not None else b"", extension="pkl")

@staticmethod
def create_from(value) -> "File":
def create_from(value, **kwargs) -> "File":
if isinstance(value, str):
return File(path=value)
elif File.is_convertable_to_image(value):
return File.as_image(value)
elif File.is_convertable_to_html(value):
return File.as_html(value)
return File.as_html(value, **kwargs)
elif is_numpy_array(value):
raise TypeError("Value of type {} is not supported. Please use File.as_image().".format(type(value)))
elif is_pandas_dataframe(value):
Expand All @@ -317,10 +324,19 @@ def is_convertable(value):

@staticmethod
def is_convertable_to_image(value):
convertable_to_img_predicates = (is_pil_image, is_matplotlib_figure, is_seaborn_figure)
convertable_to_img_predicates = (
is_pil_image,
is_matplotlib_figure,
is_seaborn_figure,
)
return any(predicate(value) for predicate in convertable_to_img_predicates)

@staticmethod
def is_convertable_to_html(value):
convertable_to_html_predicates = (is_altair_chart, is_bokeh_figure, is_plotly_figure, is_seaborn_figure)
convertable_to_html_predicates = (
is_altair_chart,
is_bokeh_figure,
is_plotly_figure,
is_seaborn_figure,
)
return any(predicate(value) for predicate in convertable_to_html_predicates)
2 changes: 2 additions & 0 deletions tests/e2e/standard/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ def test_pil_image(self, container: MetadataContainer):
def test_matplotlib_figure(self, container: MetadataContainer):
figure = generate_matplotlib_figure()
container["matplotlib_figure"] = figure
container["matplotlib_figure_cdn"] = File.as_html(figure, include_plotlyjs="cdn")

@pytest.mark.parametrize("container", ["run"], indirect=True)
def test_altair_chart(self, container: MetadataContainer):
Expand All @@ -516,6 +517,7 @@ def test_brokeh_figure(self, container: MetadataContainer):
def test_plotly_figure(self, container: MetadataContainer):
plotly_figure = generate_plotly_figure()
container["plotly_figure"] = plotly_figure
container["plotly_figure_cdn"].upload(plotly_figure, include_plotlyjs="cdn")

@pytest.mark.parametrize("container", ["run"], indirect=True)
def test_seaborn_figure(self, container: MetadataContainer):
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/neptune/new/internal/utils/test_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ def test_get_html_from_matplotlib_figure(self):
# then
self.assertTrue(result.startswith('<html>\n<head><meta charset="utf-8" />'))

# when
result = get_html_content(fig, include_plotlyjs="cdn")

# then
self.assertTrue(result.startswith('<html>\n<head><meta charset="utf-8" />'))

def test_get_html_from_plotly(self):
# given
df = px.data.tips()
Expand All @@ -200,6 +206,12 @@ def test_get_html_from_plotly(self):
# then
self.assertTrue(result.startswith('<html>\n<head><meta charset="utf-8" />'))

# when
result = get_html_content(fig, include_plotlyjs="cdn")

# then
self.assertTrue(result.startswith('<html>\n<head><meta charset="utf-8" />'))

def test_get_html_from_altair(self):
# given
source = data.cars()
Expand Down

0 comments on commit 7da7535

Please sign in to comment.