From 523f5c2d3e061fdcd97f923ff97e5c04e4e04078 Mon Sep 17 00:00:00 2001 From: isaacrobinson2000 Date: Sun, 22 Sep 2024 19:44:29 -0400 Subject: [PATCH 01/15] Working with latest matplotlib, scale_lines also now scales markers. --- matplotview/_transform_renderer.py | 70 +++++++++++++++++++++++ matplotview/tests/test_view_rendering.py | 73 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/matplotview/_transform_renderer.py b/matplotview/_transform_renderer.py index a61d1ba..6732636 100644 --- a/matplotview/_transform_renderer.py +++ b/matplotview/_transform_renderer.py @@ -251,6 +251,76 @@ def _draw_text_as_path( # checked above... (Above case causes error) super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath) + def draw_markers( + self, + gc, + marker_path, + marker_trans, + path, + trans, + rgbFace = None, + ): + # If the markers need to be scaled accurately (such as in log scale), just use the fallback as each will need + # to be scaled separately. + if(self.__scale_widths): + super().draw_markers(gc, marker_path, marker_trans, path, trans, rgbFace) + return + + # Otherwise we transform just the marker offsets (not the marker patch), so they stay the same size. + path = path.deepcopy() + path.vertices = self._get_transfer_transform(trans).transform(path.vertices) + bbox = self._get_axes_display_box() + + # Change the clip to the sub-axes box + gc.set_clip_rectangle(bbox) + if (not isinstance(self.__bounding_axes.patch, Rectangle)): + gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) + + rgbFace = tuple(rgbFace) if (rgbFace is not None) else None + self.__renderer.draw_markers(gc, marker_path, marker_trans, path, IdentityTransform(), rgbFace) + + def draw_path_collection( + self, + gc, + master_transform, + paths, + all_transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + ): + # If we want accurate scaling for each marker (such as in log scale), just use superclass implementation... + if(self.__scale_widths): + super().draw_path_collection( + gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, + edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position + ) + return + + # Otherwise we transform just the offsets, and pass them to the backend. + print(offsets) + if(np.any(np.isnan(offsets))): + raise ValueError("???") + offsets = self._get_transfer_transform(offset_trans).transform(offsets) + print(offsets) + bbox = self._get_axes_display_box() + + # Change the clip to the sub-axes box + gc.set_clip_rectangle(bbox) + if (not isinstance(self.__bounding_axes.patch, Rectangle)): + gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) + + self.__renderer.draw_path_collection( + gc, master_transform, paths, all_transforms, offsets, IdentityTransform(), facecolors, + edgecolors, linewidths, linestyles, antialiaseds, urls, None + ) + def draw_gouraud_triangle( self, gc: GraphicsContextBase, diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 03b9547..bb9b744 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -240,3 +240,76 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.plot(data) ax1_ref.text(0.5, 0.5, "Hello") + + +@check_figures_equal() +def test_log_line(fig_test, fig_ref): + data = [i for i in range(10)] + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.set(xscale="log", yscale="log") + ax1_test.plot(data, "-o") + + view(ax2_test, ax1_test, scale_lines=False) + ax2_test.set_xlim(-1, 10) + ax2_test.set_ylim(-1, 10) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.set(xscale="log", yscale="log") + ax1_ref.plot(data, "-o") + ax2_ref.plot(data, "-o") + ax2_ref.set_xlim(-1, 10) + ax2_ref.set_ylim(-1, 10) + + +@check_figures_equal() +def test_log_scatter(fig_test, fig_ref): + data = [i for i in range(1, 11)] + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.set(xscale="log", yscale="log") + ax1_test.scatter(data, data) + + view(ax2_test, ax1_test, scale_lines=False) + ax2_test.set_xlim(-5, 15) + ax2_test.set_ylim(-5, 15) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.set(xscale="log", yscale="log") + ax1_ref.scatter(data, data) + ax2_ref.scatter(data, data) + ax2_ref.set_xlim(-5, 15) + ax2_ref.set_ylim(-5, 15) + + +@check_figures_equal() +def test_log_scatter_with_colors(fig_test, fig_ref): + data = [i for i in range(1, 11)] + colors = list("rgbrgbrgbr") + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.set(xscale="log", yscale="log") + ax1_test.scatter(data, data, color=colors) + + view(ax2_test, ax1_test, scale_lines=False) + ax2_test.set_xlim(-5, 15) + ax2_test.set_ylim(-5, 15) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.set(xscale="log", yscale="log") + ax1_ref.scatter(data, data, color=colors) + ax2_ref.scatter(data, data, color=colors) + ax2_ref.set_xlim(-5, 15) + ax2_ref.set_ylim(-5, 15) From 9c059cb2aa09c17dcc441da47a6132c912488fbe Mon Sep 17 00:00:00 2001 From: isaacrobinson2000 Date: Sun, 22 Sep 2024 19:54:47 -0400 Subject: [PATCH 02/15] Minor adjustment so we don't have a marker at 0 on the log scale. --- matplotview/tests/test_view_rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index bb9b744..79c3eea 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -244,7 +244,7 @@ def test_stop_viewing(fig_test, fig_ref): @check_figures_equal() def test_log_line(fig_test, fig_ref): - data = [i for i in range(10)] + data = [i for i in range(1, 10)] # Test case... Create a view and stop it... ax1_test, ax2_test = fig_test.subplots(1, 2) From c260e6d9ad55ae5614c56ccf04501c04ba1ff92c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 23:08:39 -0400 Subject: [PATCH 03/15] CI: auto-fix via zizmor May include: - Avoids risky string interpolation. - Prevents checkout premissions from leaking --- .github/workflows/codeql.yml | 2 ++ .github/workflows/pytest.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6135e19..f070eb6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 83075c8..ffa8201 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: @@ -65,6 +67,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: From 947dfa04b1bf9156a16b5e9beab309f078c913b6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 23:24:56 -0400 Subject: [PATCH 04/15] CI: Restrict default permissions Reduces risk of arbitrary code is run by attacker. --- .github/workflows/pytest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ffa8201..c211b71 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,5 +1,7 @@ name: Validate Python Code +permissions: + contents: read on: push: From 3a2fcc1ea1c1e381ad66f4d7dc01b4b242bebef9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 18 Jul 2025 11:31:15 -0400 Subject: [PATCH 05/15] CI: add dependabot config file for GHA --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fc9f855 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of your workflow files + schedule: + interval: "weekly" # Options: daily, weekly, monthly From 426821d0cfa8054d4c30d365b91806ae3ec0e0cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:10:55 +0000 Subject: [PATCH 06/15] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f070eb6..5e6e053 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,15 +29,15 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" From 806f86ea1813d5ab209bc6851545e92ba88a694a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:10:57 +0000 Subject: [PATCH 07/15] Bump actions/setup-python from 3 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pytest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c211b71..3d10f55 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -26,7 +26,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -72,7 +72,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From 2b02497ab60ed78ab801c785e09d02e57a5034ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:11:00 +0000 Subject: [PATCH 08/15] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pytest.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f070eb6..91ae017 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c211b71..2026133 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -22,7 +22,7 @@ jobs: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} @@ -68,7 +68,7 @@ jobs: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} From a8cf1b415b75204f58c636839aa4812c11889053 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 12:52:39 -0600 Subject: [PATCH 09/15] Add artifact upload on failure. --- .github/workflows/pytest.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 685d7b0..e8f7712 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -57,9 +57,18 @@ jobs: flake8 matplotview --count --select=E9,F63,F7,F82 --show-source --statistics flake8 matplotview --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest + id: pytest run: | pytest + - name: Upload images on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.pytest.conclusion == 'failure' }} + with: + name: test-result-images + retention-days: 1 + path: result_images/ + test-windows: runs-on: windows-latest @@ -82,5 +91,14 @@ jobs: pip install pytest pip install -r requirements.txt - name: Test with pytest + id: pytest run: | - pytest \ No newline at end of file + pytest + + - name: Upload images on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.pytest.conclusion == 'failure' }} + with: + name: test-result-images + retention-days: 1 + path: result_images/ From 351342b346f12074b26364e403531e2f3e941493 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:12:36 -0600 Subject: [PATCH 10/15] Add small tolerance for macos --- .github/workflows/pytest.yml | 4 ++-- matplotview/tests/test_view_rendering.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e8f7712..f8c31fa 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.11", "3.12", "3.13", "3.13t"] steps: - uses: actions/checkout@v4 @@ -74,7 +74,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.11", "3.12", "3.13", "3.13t"] steps: - uses: actions/checkout@v4 diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 79c3eea..32dbb14 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,3 +1,5 @@ +import sys + import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal @@ -241,8 +243,8 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.plot(data) ax1_ref.text(0.5, 0.5, "Hello") - -@check_figures_equal() +# On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... +@check_figures_equal(tol=0 if sys.platform.startswith("darwin") else 0.2) def test_log_line(fig_test, fig_ref): data = [i for i in range(1, 10)] From ad031e681330c3a95592aae205e5b857e0ea1515 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:15:23 -0600 Subject: [PATCH 11/15] Add small tolerance for macos --- matplotview/tests/test_view_rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 32dbb14..b348ce8 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -244,7 +244,7 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.text(0.5, 0.5, "Hello") # On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... -@check_figures_equal(tol=0 if sys.platform.startswith("darwin") else 0.2) +@check_figures_equal(tol=0.02) def test_log_line(fig_test, fig_ref): data = [i for i in range(1, 10)] From 3e94fa2b6d970642b022cc16c552ea87c37d0df5 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:15:33 -0600 Subject: [PATCH 12/15] Add small tolerance for macos --- matplotview/tests/test_view_rendering.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index b348ce8..6e3ab02 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,5 +1,3 @@ -import sys - import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal From 0e560d958d84f07c9172e499c893efce8a68ab56 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:16:42 -0600 Subject: [PATCH 13/15] Add small tolerance for macos --- matplotview/tests/test_view_rendering.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 6e3ab02..5cafa7c 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,3 +1,5 @@ +import sys + import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal @@ -242,7 +244,7 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.text(0.5, 0.5, "Hello") # On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... -@check_figures_equal(tol=0.02) +@check_figures_equal(tol=0.02 if sys.platform.startswith("darwin") else 0) def test_log_line(fig_test, fig_ref): data = [i for i in range(1, 10)] From 4ca387b637474f5677f0ab7162cbee9c39000e4f Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:21:36 -0600 Subject: [PATCH 14/15] Fix some linting issues. --- matplotview/_transform_renderer.py | 8 ++++---- matplotview/tests/test_view_rendering.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/matplotview/_transform_renderer.py b/matplotview/_transform_renderer.py index 6732636..4bfd180 100644 --- a/matplotview/_transform_renderer.py +++ b/matplotview/_transform_renderer.py @@ -258,11 +258,11 @@ def draw_markers( marker_trans, path, trans, - rgbFace = None, + rgbFace=None, ): # If the markers need to be scaled accurately (such as in log scale), just use the fallback as each will need # to be scaled separately. - if(self.__scale_widths): + if (self.__scale_widths): super().draw_markers(gc, marker_path, marker_trans, path, trans, rgbFace) return @@ -296,7 +296,7 @@ def draw_path_collection( offset_position, ): # If we want accurate scaling for each marker (such as in log scale), just use superclass implementation... - if(self.__scale_widths): + if (self.__scale_widths): super().draw_path_collection( gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position @@ -305,7 +305,7 @@ def draw_path_collection( # Otherwise we transform just the offsets, and pass them to the backend. print(offsets) - if(np.any(np.isnan(offsets))): + if (np.any(np.isnan(offsets))): raise ValueError("???") offsets = self._get_transfer_transform(offset_trans).transform(offsets) print(offsets) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 5cafa7c..05d44e1 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -243,6 +243,7 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.plot(data) ax1_ref.text(0.5, 0.5, "Hello") + # On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... @check_figures_equal(tol=0.02 if sys.platform.startswith("darwin") else 0) def test_log_line(fig_test, fig_ref): From 2e833f4875abb2c12ee4a02fc9ff4bd46c71f8eb Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:48:21 -0600 Subject: [PATCH 15/15] Version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2f0e9c4..9e53965 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -VERSION = "1.0.1" +VERSION = "1.0.2" with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy