From 992a889b958d728139cea88afb069c0e323998c1 Mon Sep 17 00:00:00 2001 From: Siddhant Sadangi Date: Tue, 29 Oct 2024 11:45:50 +0100 Subject: [PATCH 1/2] feat: Added include_plotlyjs keyword-only parameter to upload() and File.as_html() methods --- CHANGELOG.md | 5 ++++ src/neptune/handler.py | 28 ++++++++++++++---- src/neptune/internal/utils/images.py | 14 ++++----- src/neptune/types/atoms/file.py | 29 +++++++++++++++---- tests/e2e/standard/test_files.py | 2 ++ .../neptune/new/internal/utils/test_images.py | 12 ++++++++ 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a317be09d..ea295d8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/neptune/handler.py b/src/neptune/handler.py index 567802ccd..a7fd658f2 100644 --- a/src/neptune/handler.py +++ b/src/neptune/handler.py @@ -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}(). @@ -217,7 +224,7 @@ 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: @@ -225,7 +232,11 @@ def upload(self, value, *, wait: bool = False) -> None: 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 overriding the default value of `include_plotlyjs` to `"cdn"` + to reduce the size of uploaded Plotly charts when using Neptune SaaS. Examples: >>> import neptune >>> run = neptune.init_run() @@ -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) @@ -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: diff --git a/src/neptune/internal/utils/images.py b/src/neptune/internal/utils/images.py index e928a9280..cd8619fbe 100644 --- a/src/neptune/internal/utils/images.py +++ b/src/neptune/internal/utils/images.py @@ -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 @@ -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)) @@ -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) @@ -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() diff --git a/src/neptune/types/atoms/file.py b/src/neptune/types/atoms/file.py index f6677c55b..7d76b0135 100644 --- a/src/neptune/types/atoms/file.py +++ b/src/neptune/types/atoms/file.py @@ -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 @@ -224,6 +227,11 @@ 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 overriding the default value of `include_plotlyjs` to `"cdn"` + to reduce the size of uploaded Plotly charts when using Neptune SaaS. Returns: ``File``: value object with converted object. @@ -251,7 +259,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 @@ -286,13 +294,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): @@ -317,10 +325,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) diff --git a/tests/e2e/standard/test_files.py b/tests/e2e/standard/test_files.py index e9e290f52..adf8f6243 100644 --- a/tests/e2e/standard/test_files.py +++ b/tests/e2e/standard/test_files.py @@ -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): @@ -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): diff --git a/tests/unit/neptune/new/internal/utils/test_images.py b/tests/unit/neptune/new/internal/utils/test_images.py index 4de88dfe2..f1229056d 100644 --- a/tests/unit/neptune/new/internal/utils/test_images.py +++ b/tests/unit/neptune/new/internal/utils/test_images.py @@ -182,6 +182,12 @@ def test_get_html_from_matplotlib_figure(self): # then self.assertTrue(result.startswith('\n')) + # when + result = get_html_content(fig, include_plotlyjs="cdn") + + # then + self.assertTrue(result.startswith('\n')) + def test_get_html_from_plotly(self): # given df = px.data.tips() @@ -200,6 +206,12 @@ def test_get_html_from_plotly(self): # then self.assertTrue(result.startswith('\n')) + # when + result = get_html_content(fig, include_plotlyjs="cdn") + + # then + self.assertTrue(result.startswith('\n')) + def test_get_html_from_altair(self): # given source = data.cars() From fbae62f33b58a4e0dde0550e5eff1526201e53f7 Mon Sep 17 00:00:00 2001 From: Siddhant Sadangi Date: Tue, 29 Oct 2024 11:59:06 +0100 Subject: [PATCH 2/2] docs: Rephrased docstring --- src/neptune/handler.py | 4 ++-- src/neptune/types/atoms/file.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/neptune/handler.py b/src/neptune/handler.py index a7fd658f2..3baafe4a3 100644 --- a/src/neptune/handler.py +++ b/src/neptune/handler.py @@ -235,8 +235,8 @@ def upload(self, value, *, wait: bool = False, **kwargs) -> None: **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 overriding the default value of `include_plotlyjs` to `"cdn"` - to reduce the size of uploaded Plotly charts when using Neptune SaaS. + We recommend passing `include_plotlyjs="cdn"` for better performance when using Neptune SaaS. + Examples: >>> import neptune >>> run = neptune.init_run() diff --git a/src/neptune/types/atoms/file.py b/src/neptune/types/atoms/file.py index 7d76b0135..5419ea8d2 100644 --- a/src/neptune/types/atoms/file.py +++ b/src/neptune/types/atoms/file.py @@ -230,8 +230,7 @@ def as_html( **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 overriding the default value of `include_plotlyjs` to `"cdn"` - to reduce the size of uploaded Plotly charts when using Neptune SaaS. + We recommend passing `include_plotlyjs="cdn"` for better performance when using Neptune SaaS. Returns: ``File``: value object with converted object.