From 12185d7a8a85047cfa5a14293db64b142a183a9f Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 18:31:56 +0100 Subject: [PATCH 01/10] Create utils.py --- easychart/utils.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 easychart/utils.py diff --git a/easychart/utils.py b/easychart/utils.py new file mode 100644 index 0000000..3b65fa3 --- /dev/null +++ b/easychart/utils.py @@ -0,0 +1,3 @@ +def deduplicate(lst): + seen = set() + return [x for x in lst if not (x in seen or seen.add(x))] \ No newline at end of file From e8ba8c118506f32eabf1caefd21ead42a6404265 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:03:21 +0100 Subject: [PATCH 02/10] feat: annotate and test deduplicate function --- easychart/utils.py | 24 +++++++++++++++++++++--- tests/unit-tests/utils.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/unit-tests/utils.py diff --git a/easychart/utils.py b/easychart/utils.py index 3b65fa3..94467cc 100644 --- a/easychart/utils.py +++ b/easychart/utils.py @@ -1,3 +1,21 @@ -def deduplicate(lst): - seen = set() - return [x for x in lst if not (x in seen or seen.add(x))] \ No newline at end of file +from typing import Iterable, List, TypeVar, Hashable + +T = TypeVar("T", bound=Hashable) + + +def deduplicate(lst: Iterable[T]) -> List[T]: + """ + Return a deduplicated list of items, in the order of the + original list + + Parameters + ---------- + lst : iterable + Iterable of hashable values + + Returns + ------- + list + """ + seen: set[T] = set() + return [x for x in lst if not (x in seen or seen.add(x))] diff --git a/tests/unit-tests/utils.py b/tests/unit-tests/utils.py new file mode 100644 index 0000000..8940966 --- /dev/null +++ b/tests/unit-tests/utils.py @@ -0,0 +1,28 @@ +import pytest + +from easychart.utils import deduplicate + + +def test_deduplicate_ints(): + data = [1, 2, 2, 3, 1] + assert deduplicate(data) == [1, 2, 3] + + +def test_deduplicate_tuples_and_order(): + data = [(1, 2), (1, 2), (2,)] + assert deduplicate(data) == [(1, 2), (2,)] + + +def test_deduplicate_generator_preserves_order(): + gen = (x for x in [3, 1, 3, 2]) + assert deduplicate(gen) == [3, 1, 2] + + +def test_deduplicate_empty(): + assert deduplicate([]) == [] + + +def test_deduplicate_unhashable_raises_typeerror(): + # Passing unhashable elements (lists) should raise at runtime + with pytest.raises(TypeError): + deduplicate([[1], [1]]) From 0cf92f5192120913ad74b19cc93c566a9e56cf35 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:08:12 +0100 Subject: [PATCH 03/10] chore: ruff format --- easychart/models/chart.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/easychart/models/chart.py b/easychart/models/chart.py index bca9986..894295a 100644 --- a/easychart/models/chart.py +++ b/easychart/models/chart.py @@ -1265,11 +1265,13 @@ def export(self, filename, *, theme=None, scale=2, throttle=None, **kwargs): ) if res.status_code == 429: - raise Exception(textwrap.dedent(""" + raise Exception( + textwrap.dedent(""" The export server responded with HTTP code 429, which means you are making too many export requests in too short a period of time. Please increase the throttle value, or manually set a time.sleep between each export request. - """)) + """) + ) if res.status_code != 200: raise Exception( From 66e9368a414e3224694c05396d74b28358cae25c Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:08:23 +0100 Subject: [PATCH 04/10] chore: remove unused import --- tests/unit-tests/extensions/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit-tests/extensions/__init__.py b/tests/unit-tests/extensions/__init__.py index c248a82..c3c8aff 100644 --- a/tests/unit-tests/extensions/__init__.py +++ b/tests/unit-tests/extensions/__init__.py @@ -1,4 +1,3 @@ -import pytest import easychart From 96423889efa1e7e3136643be1da0f2ff9cf67a9c Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:08:36 +0100 Subject: [PATCH 05/10] chore: fix ruff errors --- easychart/extensions/__init__.py | 2 ++ easychart/models/__init__.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/easychart/extensions/__init__.py b/easychart/extensions/__init__.py index a073521..067b8c6 100644 --- a/easychart/extensions/__init__.py +++ b/easychart/extensions/__init__.py @@ -1 +1,3 @@ from .racechart import racechart + +__all__ = ["racechart"] diff --git a/easychart/models/__init__.py b/easychart/models/__init__.py index d27b034..fa7feac 100644 --- a/easychart/models/__init__.py +++ b/easychart/models/__init__.py @@ -2,3 +2,5 @@ from .grid import Grid from .plot import Plot from .series import Series + +__all__ = ["Chart", "Grid", "Plot", "Series"] From 46ac2ec0c635694ee51850e2ba191e8fb5c6a040 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:10:07 +0100 Subject: [PATCH 06/10] feat: deduplicate scripts and stylesheets while rendering --- easychart/rendering.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/easychart/rendering.py b/easychart/rendering.py index a3007f1..89689df 100644 --- a/easychart/rendering.py +++ b/easychart/rendering.py @@ -7,6 +7,8 @@ import easychart.themes import easychart.internals +from . import utils + from jinja2 import Environment, FileSystemLoader # create the environment @@ -33,8 +35,8 @@ def render(charts) -> str: **{ "plots": simplejson.dumps([plot.serialize() for plot in grid.plots]), "theme": simplejson.dumps(easychart.themes.get(grid.theme)), - "scripts": easychart.config.scripts, - "stylesheets": easychart.config.stylesheets, + "scripts": utils.deduplicate(easychart.config.scripts), + "stylesheets": utils.deduplicate(easychart.config.stylesheets), } ) From c6de0d048c1cb34f791b22995e20c4de0769eea0 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:10:42 +0100 Subject: [PATCH 07/10] chore: bump version --- easychart/__init__.py | 17 ++++++++++++++++- setup.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/easychart/__init__.py b/easychart/__init__.py index 7633670..4b43ba9 100644 --- a/easychart/__init__.py +++ b/easychart/__init__.py @@ -12,7 +12,22 @@ import easychart.extensions as ext import easychart.rendering -__version__ = "0.1.32" +__version__ = "0.1.33" + +__all__ = [ + "new", + "heatmap", + "plot", + "render", + "Chart", + "Plot", + "Grid", + "config", + "datasets", + "themes", + "colormaps", + "ext", +] def new( diff --git a/setup.py b/setup.py index caebec6..a9a6448 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="easychart", - version="0.1.32", + version="0.1.33", author="david.schenck@outlook.com", author_email="david.schenck@outlook.com", description="Highcharts meets python in your Jupyter notebook", From 0c2511fd5c1d72501cb81bb9fc2016a7830c33e7 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:12:24 +0100 Subject: [PATCH 08/10] fix: blacken source code --- easychart/extensions/racechart/__init__.py | 6 ++++-- easychart/models/chart.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/easychart/extensions/racechart/__init__.py b/easychart/extensions/racechart/__init__.py index 214bf05..221b5b4 100644 --- a/easychart/extensions/racechart/__init__.py +++ b/easychart/extensions/racechart/__init__.py @@ -83,10 +83,12 @@ def render(self, *, theme=None): } ) - return IPython.display.HTML(f""" + return IPython.display.HTML( + f""" - """) + """ + ) diff --git a/easychart/models/chart.py b/easychart/models/chart.py index 894295a..4a53089 100644 --- a/easychart/models/chart.py +++ b/easychart/models/chart.py @@ -1266,11 +1266,13 @@ def export(self, filename, *, theme=None, scale=2, throttle=None, **kwargs): if res.status_code == 429: raise Exception( - textwrap.dedent(""" + textwrap.dedent( + """ The export server responded with HTTP code 429, which means you are making too many export requests in too short a period of time. Please increase the throttle value, or manually set a time.sleep between each export request. - """) + """ + ) ) if res.status_code != 200: From 9aa598f4d1f38ad3a7083e0dbc044f2dd21793d8 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:17:27 +0100 Subject: [PATCH 09/10] feat: replace black with ruff --- .github/workflows/testing.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 9d36b7e..b4542a1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,16 +24,9 @@ jobs: python -m pip install --upgrade pip python -m pip install flake8 pytest coverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Check format is Black - uses: psf/black@stable + - uses: astral-sh/ruff-action@v3 with: src: "./easychart" - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics test: runs-on: ubuntu-latest strategy: From 76f93cf33b18afe9071a8410b212c5f883ce0705 Mon Sep 17 00:00:00 2001 From: David Schenck Date: Wed, 25 Mar 2026 22:28:11 +0100 Subject: [PATCH 10/10] feat: replace highcharts CDN with jsdelivr CDN --- easychart/config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/easychart/config.py b/easychart/config.py index 9040bac..3512faa 100644 --- a/easychart/config.py +++ b/easychart/config.py @@ -17,14 +17,14 @@ class Config(easytree.dict): defaults = { "stylesheets": [], "scripts": [ - "https://code.highcharts.com/stock/highstock.js", - "https://code.highcharts.com/highcharts-more.js", - "https://code.highcharts.com/modules/heatmap.js", - "https://code.highcharts.com/modules/exporting.js", - "https://code.highcharts.com/modules/offline-exporting.js", - "https://code.highcharts.com/modules/export-data.js", - "https://code.highcharts.com/modules/annotations.js", - "https://code.highcharts.com/modules/accessibility.js", + "https://cdn.jsdelivr.net/npm/highcharts/highstock.js", + "https://cdn.jsdelivr.net/npm/highcharts/highcharts-more.js", + "https://cdn.jsdelivr.net/npm/highcharts/modules/heatmap.js", + "https://cdn.jsdelivr.net/npm/highcharts/modules/exporting.js", + "https://cdn.jsdelivr.net/npm/highcharts/modules/offline-exporting.js", + "https://cdn.jsdelivr.net/npm/highcharts/modules/export-data.js", + "https://cdn.jsdelivr.net/npm/highcharts/modules/annotations.js", + "https://cdn.jsdelivr.net/npm/highcharts/modules/accessibility.js", ], "theme": None, "rendering": {