diff --git a/docs/examples/dataset/dataset_collection.ipynb b/docs/examples/dataset/dataset_collection.ipynb index 9216fb886c..ee7fece855 100644 --- a/docs/examples/dataset/dataset_collection.ipynb +++ b/docs/examples/dataset/dataset_collection.ipynb @@ -23,10 +23,10 @@ "outputs": [], "source": [ "# NBVAL_IGNORE_OUTPUT\n", - "from cleopatra.array_glyph import FrameLabel\n", "from IPython.display import HTML\n", "\n", "from pyramids.dataset import DatasetCollection\n", + "from pyramids.plot import ColorScaling, FrameLabel\n", "\n", "%matplotlib inline" ] @@ -256,7 +256,7 @@ "cleo = m_dataset.plot(\n", " exclude_value=0,\n", " frame_label=FrameLabel(location=(1, 3)),\n", - " color_scale=\"linear\",\n", + " color=ColorScaling.linear(),\n", " vmin=1,\n", " vmax=100,\n", ")\n", diff --git a/docs/examples/netcdf/ugrid/ugrid-river-channel.ipynb b/docs/examples/netcdf/ugrid/ugrid-river-channel.ipynb index 218b4986d4..e61240dbfd 100644 --- a/docs/examples/netcdf/ugrid/ugrid-river-channel.ipynb +++ b/docs/examples/netcdf/ugrid/ugrid-river-channel.ipynb @@ -47,7 +47,8 @@ "\n", "from pyramids.netcdf.ugrid import (\n", " UgridDataset,\n", - ")" + ")\n", + "from pyramids.plot import ColorBar" ] }, { @@ -135,7 +136,7 @@ " \"bathymetry\",\n", " cmap=\"ocean_r\",\n", " title=\"River Channel Bathymetry\",\n", - " cbar_label=\"Depth (m)\",\n", + " colorbar=ColorBar(label=\"Depth (m)\"),\n", ")\n", "plt.show()" ] @@ -151,7 +152,7 @@ " \"velocity\",\n", " cmap=\"YlOrRd\",\n", " title=\"Flow Velocity (parabolic cross-section)\",\n", - " cbar_label=\"Velocity (m/s)\",\n", + " colorbar=ColorBar(label=\"Velocity (m/s)\"),\n", ")\n", "plt.show()" ] @@ -311,7 +312,7 @@ " \"bathymetry\",\n", " cmap=\"ocean_r\",\n", " title=\"Meander Bend — Bathymetry\",\n", - " cbar_label=\"Depth (m)\",\n", + " colorbar=ColorBar(label=\"Depth (m)\"),\n", ")\n", "plt.show()" ] @@ -489,7 +490,7 @@ " vmax=1.5,\n", " interval=800,\n", " title=\"Tidal Water Level\",\n", - " cbar_label=\"Water Level (m)\",\n", + " colorbar=ColorBar(label=\"Water Level (m)\"),\n", ")\n", "\n", "from IPython.display import HTML\n", diff --git a/pixi.lock b/pixi.lock index 7d82eb242e..5231b7fb45 100644 --- a/pixi.lock +++ b/pixi.lock @@ -143,10 +143,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/77/4c0456ead6871b060d024204f891546499eb1e57b8ce523671f690f24281/hpc_utils-0.1.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl linux-aarch64: @@ -256,13 +256,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/c4/77/4c0456ead6871b060d024204f891546499eb1e57b8ce523671f690f24281/hpc_utils-0.1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl osx-64: @@ -367,13 +367,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl - pypi: https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/77/4c0456ead6871b060d024204f891546499eb1e57b8ce523671f690f24281/hpc_utils-0.1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda @@ -479,10 +479,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c4/77/4c0456ead6871b060d024204f891546499eb1e57b8ce523671f690f24281/hpc_utils-0.1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl win-64: @@ -580,13 +580,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/77/4c0456ead6871b060d024204f891546499eb1e57b8ce523671f690f24281/hpc_utils-0.1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl dev: @@ -869,6 +869,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -931,7 +932,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -1229,6 +1229,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -1294,7 +1295,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -1588,6 +1588,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -1652,7 +1653,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -1945,6 +1945,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -2007,7 +2008,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -2294,6 +2294,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -2352,7 +2353,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -2660,6 +2660,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/c6/76ee9dacedcd8c67d8fa53dd975613733bdd28242a4c41518ff1c8aeaa64/jupytext-1.19.5-py3-none-any.whl @@ -2717,7 +2718,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl @@ -3012,6 +3012,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/c6/76ee9dacedcd8c67d8fa53dd975613733bdd28242a4c41518ff1c8aeaa64/jupytext-1.19.5-py3-none-any.whl @@ -3071,7 +3072,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -3362,6 +3362,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -3424,7 +3425,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl @@ -3713,6 +3713,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/c6/76ee9dacedcd8c67d8fa53dd975613733bdd28242a4c41518ff1c8aeaa64/jupytext-1.19.5-py3-none-any.whl @@ -3773,7 +3774,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl @@ -4055,6 +4055,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/c6/76ee9dacedcd8c67d8fa53dd975613733bdd28242a4c41518ff1c8aeaa64/jupytext-1.19.5-py3-none-any.whl @@ -4111,7 +4112,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -4371,6 +4371,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -4427,7 +4428,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -4675,6 +4675,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -4734,7 +4735,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -4978,6 +4978,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -5036,7 +5037,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -5279,6 +5279,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -5335,7 +5336,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -5574,6 +5574,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -5627,7 +5628,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -5933,6 +5933,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -5989,7 +5990,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -6283,6 +6283,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -6342,7 +6343,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -6632,6 +6632,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -6690,7 +6691,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -6979,6 +6979,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -7035,7 +7036,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -7318,6 +7318,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -7371,7 +7372,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -7628,6 +7628,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -7688,7 +7689,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -7934,6 +7934,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -7996,7 +7997,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -8236,6 +8236,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -8296,7 +8297,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -8540,6 +8540,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -8597,7 +8598,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -8832,6 +8832,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl @@ -8887,7 +8888,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -9144,6 +9144,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -9205,7 +9206,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -9452,6 +9452,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -9510,7 +9511,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -9750,6 +9750,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -9808,7 +9809,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -10046,6 +10046,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -10106,7 +10107,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -10341,6 +10341,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -10397,7 +10398,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -10651,6 +10651,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -10707,7 +10708,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl @@ -10960,6 +10960,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -11020,7 +11021,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -11258,6 +11258,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -11318,7 +11319,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -11558,6 +11558,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -11617,7 +11618,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -11850,6 +11850,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -11907,7 +11908,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -12165,6 +12165,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -12223,7 +12224,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -12471,6 +12471,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -12532,7 +12533,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl @@ -12775,6 +12775,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -12835,7 +12836,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -13078,6 +13078,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -13136,7 +13137,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl @@ -13373,6 +13373,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl @@ -13428,7 +13429,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl @@ -14510,7 +14510,7 @@ packages: - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/backports-zstd?source=compressed-mapping + - pkg:pypi/backports-zstd?source=hash-mapping run_exports: {} size: 239892 timestamp: 1781450817988 @@ -17265,7 +17265,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/numpy?source=compressed-mapping + - pkg:pypi/numpy?source=hash-mapping run_exports: weak: - numpy >=1.25,<3 @@ -17311,7 +17311,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/numpy?source=compressed-mapping + - pkg:pypi/numpy?source=hash-mapping run_exports: weak: - numpy >=1.25,<3 @@ -18003,7 +18003,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/scipy?source=compressed-mapping + - pkg:pypi/scipy?source=hash-mapping run_exports: {} size: 17104066 timestamp: 1781912972195 @@ -18314,7 +18314,7 @@ packages: - python_abi 3.13.* *_cp313 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 117625 timestamp: 1785422127987 @@ -18328,7 +18328,7 @@ packages: - python_abi 3.14.* *_cp314 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 119163 timestamp: 1785422126684 @@ -19438,7 +19438,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/gdal?source=compressed-mapping + - pkg:pypi/gdal?source=hash-mapping run_exports: {} size: 2447911 timestamp: 1784038283198 @@ -22627,7 +22627,7 @@ packages: - python_abi 3.11.* *_cp311 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 118914 timestamp: 1785422173475 @@ -22641,7 +22641,7 @@ packages: - python_abi 3.12.* *_cp312 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 117427 timestamp: 1785422157504 @@ -22669,7 +22669,7 @@ packages: - python_abi 3.14.* *_cp314 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 119267 timestamp: 1785422152533 @@ -22811,7 +22811,7 @@ packages: license: PSF-2.0 license_family: PSF purls: - - pkg:pypi/aiohappyeyeballs?source=compressed-mapping + - pkg:pypi/aiohappyeyeballs?source=hash-mapping run_exports: {} size: 24690 timestamp: 1782986823268 @@ -22898,8 +22898,7 @@ packages: depends: - python >=3.14 license: BSD-3-Clause AND MIT AND EPL-2.0 - purls: - - pkg:pypi/backports-zstd?source=compressed-mapping + purls: [] run_exports: {} size: 7526 timestamp: 1781450817767 @@ -22958,7 +22957,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/cached-property?source=compressed-mapping + - pkg:pypi/cached-property?source=hash-mapping run_exports: {} size: 14752 timestamp: 1783242913845 @@ -22969,7 +22968,7 @@ packages: - python >=3.10 license: ISC purls: - - pkg:pypi/certifi?source=compressed-mapping + - pkg:pypi/certifi?source=hash-mapping run_exports: {} size: 137015 timestamp: 1784717699092 @@ -22981,7 +22980,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/charset-normalizer?source=compressed-mapping + - pkg:pypi/charset-normalizer?source=hash-mapping run_exports: {} size: 61418 timestamp: 1783505332569 @@ -22996,7 +22995,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/click?source=compressed-mapping + - pkg:pypi/click?source=hash-mapping run_exports: {} size: 106227 timestamp: 1783085395110 @@ -23010,7 +23009,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/click?source=compressed-mapping + - pkg:pypi/click?source=hash-mapping run_exports: {} size: 107155 timestamp: 1783085363526 @@ -23056,7 +23055,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/dask?source=compressed-mapping + - pkg:pypi/dask?source=hash-mapping run_exports: {} size: 1074755 timestamp: 1784031128995 @@ -23188,7 +23187,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/h2?source=compressed-mapping + - pkg:pypi/h2?source=hash-mapping run_exports: {} size: 100184 timestamp: 1784898236952 @@ -23200,7 +23199,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/hpack?source=compressed-mapping + - pkg:pypi/hpack?source=hash-mapping run_exports: {} size: 32884 timestamp: 1782283986153 @@ -23225,7 +23224,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/idna?source=compressed-mapping + - pkg:pypi/idna?source=hash-mapping run_exports: {} size: 163869 timestamp: 1781620148226 @@ -23683,7 +23682,7 @@ packages: - obstore >=0.5.1 license: MIT purls: - - pkg:pypi/zarr?source=compressed-mapping + - pkg:pypi/zarr?source=hash-mapping run_exports: {} size: 471664 timestamp: 1785511642744 @@ -23708,7 +23707,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/zipp?source=compressed-mapping + - pkg:pypi/zipp?source=hash-mapping run_exports: {} size: 24190 timestamp: 1779159948016 @@ -27747,7 +27746,7 @@ packages: - python_abi 3.11.* *_cp311 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 114187 timestamp: 1785422578909 @@ -27760,7 +27759,7 @@ packages: - python_abi 3.12.* *_cp312 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 112703 timestamp: 1785422534356 @@ -27773,7 +27772,7 @@ packages: - python_abi 3.13.* *_cp313 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 113971 timestamp: 1785422522880 @@ -27786,7 +27785,7 @@ packages: - python_abi 3.14.* *_cp314 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 115207 timestamp: 1785422575221 @@ -31180,7 +31179,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/pandas?source=compressed-mapping + - pkg:pypi/pandas?source=hash-mapping run_exports: {} size: 14402418 timestamp: 1784821943837 @@ -32005,7 +32004,7 @@ packages: - python_abi 3.11.* *_cp311 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 114759 timestamp: 1785422729160 @@ -32033,7 +32032,7 @@ packages: - python_abi 3.13.* *_cp313 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 114819 timestamp: 1785422632081 @@ -32205,7 +32204,7 @@ packages: license: MIT AND Apache-2.0 license_family: Apache purls: - - pkg:pypi/aiohttp?source=compressed-mapping + - pkg:pypi/aiohttp?source=hash-mapping run_exports: {} size: 1033675 timestamp: 1784900600588 @@ -32229,7 +32228,7 @@ packages: license: MIT AND Apache-2.0 license_family: Apache purls: - - pkg:pypi/aiohttp?source=compressed-mapping + - pkg:pypi/aiohttp?source=hash-mapping run_exports: {} size: 1041930 timestamp: 1784900597803 @@ -32718,7 +32717,7 @@ packages: - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/backports-zstd?source=compressed-mapping + - pkg:pypi/backports-zstd?source=hash-mapping run_exports: {} size: 238542 timestamp: 1781450836106 @@ -34992,7 +34991,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/msgpack?source=compressed-mapping + - pkg:pypi/msgpack?source=hash-mapping run_exports: {} size: 89054 timestamp: 1782460807428 @@ -35008,7 +35007,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/msgpack?source=compressed-mapping + - pkg:pypi/msgpack?source=hash-mapping run_exports: {} size: 89413 timestamp: 1782460810590 @@ -35249,7 +35248,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/numpy?source=compressed-mapping + - pkg:pypi/numpy?source=hash-mapping run_exports: weak: - numpy >=1.25,<3 @@ -35398,7 +35397,7 @@ packages: - zstandard >=0.23.0 license: BSD-3-Clause purls: - - pkg:pypi/pandas?source=compressed-mapping + - pkg:pypi/pandas?source=hash-mapping run_exports: {} size: 14103773 timestamp: 1785276287020 @@ -35488,7 +35487,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/propcache?source=compressed-mapping + - pkg:pypi/propcache?source=hash-mapping run_exports: {} size: 49101 timestamp: 1780037831647 @@ -35946,7 +35945,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/scipy?source=compressed-mapping + - pkg:pypi/scipy?source=hash-mapping run_exports: {} size: 15248913 timestamp: 1781914321655 @@ -36094,7 +36093,7 @@ packages: license: Apache-2.0 license_family: Apache purls: - - pkg:pypi/tornado?source=compressed-mapping + - pkg:pypi/tornado?source=hash-mapping run_exports: {} size: 865801 timestamp: 1781006895319 @@ -36284,7 +36283,7 @@ packages: - vc14_runtime >=14.44.35208 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 116290 timestamp: 1785422205402 @@ -36299,7 +36298,7 @@ packages: - vc14_runtime >=14.44.35208 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 114221 timestamp: 1785422189861 @@ -36314,7 +36313,7 @@ packages: - vc14_runtime >=14.44.35208 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 115201 timestamp: 1785422193029 @@ -36329,7 +36328,7 @@ packages: - vc14_runtime >=14.44.35208 license: BSD-2-Clause purls: - - pkg:pypi/wrapt?source=compressed-mapping + - pkg:pypi/wrapt?source=hash-mapping run_exports: {} size: 115942 timestamp: 1785422199912 @@ -36419,7 +36418,7 @@ packages: license: Apache-2.0 license_family: Apache purls: - - pkg:pypi/yarl?source=compressed-mapping + - pkg:pypi/yarl?source=hash-mapping run_exports: {} size: 167471 timestamp: 1784526549850 @@ -36469,7 +36468,7 @@ packages: - pyyaml>=6.0.0 - shapely>=2.1.0 ; platform_machine != 'ARM64' or sys_platform != 'win32' - scipy>=1.10.0 - - cleopatra[tiles]>=0.28.0 ; extra == 'viz' + - cleopatra[tiles]>=0.30.0 ; extra == 'viz' - dask>=2024.1.0 ; extra == 'lazy' - distributed>=2024.1.0 ; extra == 'lazy' - zarr>=3 ; extra == 'lazy' @@ -40310,6 +40309,20 @@ packages: version: 0.13.0 sha256: 34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/78/a4/03e5f09af194fa1df1263f88e0ae7d0acb47a071ac638beaca57ed4bceff/cleopatra-0.30.0-py3-none-any.whl + name: cleopatra + version: 0.30.0 + sha256: 59624fd5e2a16cbace7829a7b22e039b2c458f6b6d3bba963badfd6a83411ec0 + requires_dist: + - hpc-utils>=0.1.4 + - imageio-ffmpeg>=0.4.9 + - matplotlib>=3.9 + - numpy>=2.0.0 + - pillow>=12.1.1 + - cmap>=0.7.2 ; extra == 'science-colors' + - pyproj>=3.7.2 ; extra == 'tiles' + - xyzservices>=2026.3.0 ; extra == 'tiles' + requires_python: '>=3.11,<4' - pypi: https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl name: scipy version: 1.18.0 @@ -43448,19 +43461,6 @@ packages: requires_dist: - typing-extensions>=4.12.0 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/dc/c6/c993e8f3129975c2a60c87c4489d432ad203390c04f606f19b9ec2ad5d58/cleopatra-0.28.0-py3-none-any.whl - name: cleopatra - version: 0.28.0 - sha256: df0434d5501c5aa85eddc2cba4466437608d325aa47d27639d442a583b79155e - requires_dist: - - hpc-utils>=0.1.4 - - imageio-ffmpeg>=0.4.9 - - matplotlib>=3.9 - - numpy>=2.0.0 - - pillow>=12.1.1 ; extra == 'tiles' - - pyproj>=3.7.2 ; extra == 'tiles' - - xyzservices>=2026.3.0 ; extra == 'tiles' - requires_python: '>=3.11,<4' - pypi: https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl name: fonttools version: 4.63.0 diff --git a/pyproject.toml b/pyproject.toml index dfb37adb05..bcf5c95c91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ dependencies = [ ] [project.optional-dependencies] -viz = ["cleopatra[tiles]>=0.28.0"] +viz = ["cleopatra[tiles]>=0.30.0"] lazy = [ "dask>=2024.1.0", "distributed>=2024.1.0", @@ -234,6 +234,24 @@ exclude = [ "planning/", ] +[[tool.mypy.overrides]] +# cleopatra ships inline types (the TypedDicts PlotKwargs/AnimateKwargs, the ColorBar / +# PointOverlay / Basemap dataclasses, ...) but no `py.typed` marker yet, so the global +# `ignore_missing_imports` would collapse them to `Any` and break `Unpack[PlotKwargs]`. +# Follow its source types instead. Drop once cleopatra ships `py.typed`. +module = "cleopatra.*" +ignore_missing_imports = false +follow_untyped_imports = true + +[[tool.mypy.overrides]] +# render_array is the single dynamic-dispatch backend: it splits **kwargs into ctor / +# render buckets at runtime (keyed on ArrayGlyph.option_keys()) and splats them into +# cleopatra's now-typed plot/animate/facet. mypy cannot verify a runtime-built **dict +# against a typed signature, so `arg-type` here is a wall of false positives (the 1367 +# plot tests prove the routing). Suppress just that code for this one module. +module = "pyramids.dataset._plot_helpers" +disable_error_code = ["arg-type", "no-any-return"] + [[tool.mypy.overrides]] module = "pyramids.dataset.ops.*" disable_error_code = [ diff --git a/src/pyramids/base/_utils.py b/src/pyramids/base/_utils.py index 2e9555aa63..3262d091b8 100644 --- a/src/pyramids/base/_utils.py +++ b/src/pyramids/base/_utils.py @@ -654,14 +654,14 @@ def require_optional(module_name: str, message: str, *, return_module: bool = Fa One nuance for dotted names: `import_basemap` previously spelled `from cleopatra import tiles`, which calls `__import__("cleopatra", ..., fromlist=("tiles",))`. Here it becomes - `__import__("cleopatra.tiles")`, so the parent package resolves internally + `__import__("cleopatra.basemap.tiles")`, so the parent package resolves internally rather than through `builtins.__import__`. A patch keyed on `name == "cleopatra"` no longer intercepts it; key on the dotted name instead. No in-tree test depends on this. Args: module_name: Dotted module path to import, e.g. ``"zarr"`` or - ``"cleopatra.tiles"``. + ``"cleopatra.basemap.tiles"``. message: The install hint raised when the import fails. Compose it with :func:`lazy_extra_hint` for the ``[lazy]`` extra. return_module: When `True` the imported module object is returned so the @@ -894,8 +894,8 @@ def import_h5py(message: str): def import_basemap(message: str): - """Import the web-tile basemap backend (``cleopatra.tiles``, the ``[tiles]`` extra).""" - return require_optional("cleopatra.tiles", message) + """Import the web-tile basemap backend (``cleopatra.basemap.tiles``, the ``[tiles]`` extra).""" + return require_optional("cleopatra.basemap.tiles", message) def ogr_ds_to_gdal_dataset(ogr_ds: ogr.DataSource) -> gdal.Dataset: diff --git a/src/pyramids/basemap/__init__.py b/src/pyramids/basemap/__init__.py index 20e54ff3af..bdae5e1425 100644 --- a/src/pyramids/basemap/__init__.py +++ b/src/pyramids/basemap/__init__.py @@ -3,7 +3,7 @@ Adds web-tile basemaps (OpenStreetMap, CartoDB, Esri, etc.) underneath data plotted on matplotlib axes. :func:`~pyramids.basemap.add_basemap` and :func:`~pyramids.basemap.get_provider` are thin wrappers over -:mod:`cleopatra.tiles` (the cleopatra C-6 helpers) — cleopatra does the +:mod:`cleopatra.basemap.tiles` (the cleopatra C-6 helpers) — cleopatra does the tile fetching, stitching, GDAL CRS warping, and rendering. The tile functions require the cleopatra ``[tiles]`` extra (which pins @@ -13,8 +13,8 @@ - conda-forge: ``conda install -c conda-forge pyramids-viz`` Natural Earth vector layers and the global relief raster have moved to -:mod:`cleopatra.reference` (the viz layer), where they belong with the rest of -the map-decoration helpers — use ``cleopatra.reference`` for those backdrops. +:mod:`cleopatra.basemap.reference` (the viz layer), where they belong with the rest of +the map-decoration helpers — use ``cleopatra.basemap.reference`` for those backdrops. """ from pyramids.basemap.basemap import add_basemap, get_provider diff --git a/src/pyramids/basemap/basemap.py b/src/pyramids/basemap/basemap.py index 1b9d02ca07..63f82de34b 100644 --- a/src/pyramids/basemap/basemap.py +++ b/src/pyramids/basemap/basemap.py @@ -1,7 +1,7 @@ -"""Web-tile basemap helpers — thin wrappers over ``cleopatra.tiles``. +"""Web-tile basemap helpers — thin wrappers over ``cleopatra.basemap.tiles``. :func:`add_basemap` and :func:`get_provider` here delegate to -:func:`cleopatra.tiles.add_tiles` and :func:`cleopatra.tiles.get_provider` +:func:`cleopatra.basemap.tiles.add_tiles` and :func:`cleopatra.basemap.tiles.get_provider` (the cleopatra C-6 helpers, shipped in ``cleopatra >= 0.8.0`` and pinned via the ``[viz]`` extra as ``cleopatra[tiles]``). All the real work — provider resolution, zoom selection, parallel tile fetching, stitching, @@ -40,7 +40,7 @@ def get_provider(name: str | None = None) -> Any: """Resolve a web-tile provider by name. - Thin wrapper over :func:`cleopatra.tiles.get_provider`. + Thin wrapper over :func:`cleopatra.basemap.tiles.get_provider`. Args: name (str or None, optional): @@ -84,10 +84,10 @@ def get_provider(name: str | None = None) -> Any: See Also: add_basemap: Uses ``get_provider`` internally (via cleopatra) when ``source`` is a string. - cleopatra.tiles.get_provider: The underlying implementation. + cleopatra.basemap.tiles.get_provider: The underlying implementation. """ import_basemap(_BASEMAP_MSG) - from cleopatra.tiles import get_provider as _get_provider + from cleopatra.basemap.tiles import get_provider as _get_provider return _get_provider(name) @@ -106,7 +106,7 @@ def add_basemap( ) -> Any: """Add a web-tile basemap underneath the data plotted on ``ax``. - Thin wrapper over :func:`cleopatra.tiles.add_tiles`: fetches XYZ web + Thin wrapper over :func:`cleopatra.basemap.tiles.add_tiles`: fetches XYZ web tiles for the axes' geographic extent, stitches them into a single image, reprojects to the data's CRS via GDAL when ``crs`` is not Web Mercator, and renders the image beneath the data layer. @@ -146,7 +146,7 @@ def add_basemap( Returns: matplotlib.axes.Axes: The axes with the basemap added (whatever - :func:`cleopatra.tiles.add_tiles` returns). + :func:`cleopatra.basemap.tiles.add_tiles` returns). Raises: OptionalPackageDoesNotExist: If the cleopatra ``[tiles]`` extra is @@ -181,10 +181,10 @@ def add_basemap( See Also: get_provider: Resolve a tile provider name to a ``TileProvider``. - cleopatra.tiles.add_tiles: The underlying implementation. + cleopatra.basemap.tiles.add_tiles: The underlying implementation. """ import_basemap(_BASEMAP_MSG) - from cleopatra.tiles import add_tiles + from cleopatra.basemap.tiles import add_tiles result = add_tiles( ax, diff --git a/src/pyramids/dataset/_plot_helpers.py b/src/pyramids/dataset/_plot_helpers.py index 4343e524ae..a05f359133 100644 --- a/src/pyramids/dataset/_plot_helpers.py +++ b/src/pyramids/dataset/_plot_helpers.py @@ -67,7 +67,6 @@ from pyproj import CRS from pyproj.exceptions import CRSError -from pyramids.base._errors import OptionalPackageDoesNotExist from pyramids.base._utils import require_cleopatra # `add_basemap` is imported at top-level so existing test patches that @@ -78,7 +77,8 @@ from pyramids.basemap import basemap as _basemap_module if TYPE_CHECKING: - from cleopatra.geo import Basemap + from cleopatra.basemap.geo import Basemap + from cleopatra.glyphs.gridded.array_glyph import ArrayGlyph # N-6 — Mesh rendering shares this module's "data in, glyph out" # contract via :func:`mesh_render`. The function lives next to @@ -161,159 +161,71 @@ def _unwrap_geographic_longitude( return result -def _guard_style_hillshade(kwargs: dict[str, Any], option_keys: Any) -> None: - """Normalise and version-gate the ``style`` / ``hillshade`` plot presets. +# Loose colour-bar kwargs that the typed ``pyramids.plot.ColorBar`` spec replaces. +# cleopatra 0.30 still accepts these natively, but pyramids standardises on ``ColorBar``, +# so passing one is a hard error pointing at the spec. ``cbar_kwargs`` (a raw matplotlib +# colorbar-kwargs passthrough) and ``add_colorbar`` (the on/off switch) are deliberately +# NOT in this set — they have no ``ColorBar`` field and remain valid. +_CBAR_REPLACED_BY_COLORBAR = frozenset( + { + "cbar_label", + "cbar_length", + "cbar_label_size", + "cbar_label_rotation", + "cbar_label_location", + "cbar_orientation", + "cbar_location", + "cbar_inside", + "cbar_box", + "cbar_label_color", + "cbar_tick_color", + "ticks_spacing", + } +) - Shared by the raster (:func:`render_array`, ``ArrayGlyph``) and mesh - (:func:`mesh_render`, ``MeshGlyph``) dispatch so both paths handle cleopatra's - data-style presets identically. - ``style`` / ``hillshade`` were added to cleopatra's glyphs in 0.24. On an older - cleopatra the kwargs would fall through to a cryptic "Unknown option" error deep - in the render call; feature-detect each key against ``option_keys`` and raise a - clear upgrade hint instead. +def _reject_replaced_cbar_kwargs(kwargs: dict[str, Any]) -> None: + """Raise if any loose colour-bar kwarg the ``ColorBar`` spec replaces is present. - A ``None`` / ``False`` ``hillshade`` requests no shading, so it is popped from - ``kwargs`` in place — making it a true no-op on ANY cleopatra version (a pre-0.24 - build that rejects the unknown kwarg included). ``{}`` is falsy but is an - affirmative "shade with default parameters", so the drop uses identity (only - ``None`` / ``False``), not truthiness — an empty-dict ``hillshade`` is still - gated/forwarded. An explicit ``style=None`` requests no preset and is likewise - popped in place, symmetric with the ``hillshade`` drop. Each remaining key is - checked against its own ``option_keys`` membership so a build shipping only one - of the two features is handled precisely. An invalid ``style`` *name* is left to - cleopatra, which already raises a ``ValueError`` listing the valid ``DATA_STYLES`` - keys. (issue #737) + Mirrors how cleopatra rejects the other loose styling keywords that moved onto typed + group objects. cleopatra itself still tolerates the ``cbar_*`` / ``ticks_spacing`` + forms, but pyramids exposes a single typed colour-bar surface — ``colorbar=ColorBar`` + — so it rejects the loose forms at the render boundary with a pointer to the spec. Args: - kwargs: The render kwargs; mutated in place to drop a no-op ``style`` / - ``hillshade``. - option_keys: The glyph's declared option set (``.option_keys()``). + kwargs: The render kwargs. Raises: - OptionalPackageDoesNotExist: A ``style`` / ``hillshade`` preset was - requested but the installed cleopatra's glyph does not support it. + ValueError: If a replaced loose colour-bar keyword is present; the message lists + the offending keys and the ``ColorBar`` replacement. """ - if kwargs.get("style") is None: - # An explicit ``style=None`` requests no preset — drop it so it never - # reaches cleopatra (a true no-op on any version), symmetric with the - # ``hillshade`` handling below. - kwargs.pop("style", None) - hillshade = kwargs.get("hillshade") - if hillshade is None or hillshade is False: - kwargs.pop("hillshade", None) - style_unsupported = "style" in kwargs and "style" not in option_keys - hillshade_unsupported = "hillshade" in kwargs and "hillshade" not in option_keys - if style_unsupported or hillshade_unsupported: - raise OptionalPackageDoesNotExist( - "`style=` / `hillshade=` plot presets require cleopatra >= 0.24. " - "Upgrade with: pip install -U 'pyramids-gis[viz]' (or " - "pip install -U 'cleopatra>=0.24')." + offending = sorted(_CBAR_REPLACED_BY_COLORBAR & kwargs.keys()) + if offending: + raise ValueError( + f"The loose colour-bar kwargs {offending} were replaced by the typed " + "pyramids.plot.ColorBar spec; pass colorbar=ColorBar(label=…, orientation=…, " + "length=…, ...) instead of the loose cbar_* / ticks_spacing keywords." ) -# Loose plot kwargs cleopatra 0.28 deprecated in favour of the typed specs -# (ColorBar / PointOverlay). ``render_array`` translates them to the spec at the -# render boundary (see ``_migrate_deprecated_plot_specs``) so cleopatra only ever -# sees the typed form — no double DeprecationWarning — and the loose forms are -# steered off in one place for every raster plot / animate path. The maps mirror -# cleopatra's own loose-kwarg -> spec-field mapping. -_CBAR_TO_COLORBAR = { - "cbar_label": "label", - "cbar_length": "length", - "cbar_label_size": "label_size", - "cbar_label_rotation": "label_rotation", - "cbar_label_location": "label_location", - "cbar_orientation": "orientation", - "ticks_spacing": "ticks_spacing", -} -_POINT_TO_OVERLAY = { - "point_color": "color", - "point_size": "size", - # Legacy ``pid_*`` aliases first, then the modern ``point_label_*`` names — the - # fold below is last-write-wins, so passing both makes ``point_label_*`` win, - # matching cleopatra's ``_resolve_point_overlay`` precedence. - "pid_color": "label_color", - "pid_size": "label_size", - "point_label_color": "label_color", - "point_label_size": "label_size", -} - - -def _migrate_deprecated_plot_specs( - kwargs: dict[str, Any], colorbar_cls: Any, point_overlay_cls: Any -) -> None: - """Fold deprecated loose ``cbar_*`` / ``point_*`` kwargs into the typed specs. - - Mutates ``kwargs`` in place: the loose keys are popped and folded into a - ``ColorBar`` / ``PointOverlay`` unless an explicit typed spec was already given. - A typed ``colorbar=ColorBar`` (or ``colorbar=False``, which hides the bar) and a - ``points=PointOverlay`` win — the loose keys are dropped with the warning. - ``colorbar=True`` / ``None`` still fold (they carry no styling of their own, so - dropping the loose kwargs would silently lose the caption). Emits a pyramids - ``DeprecationWarning`` naming the typed replacement. Cleopatra therefore never - sees the loose kwargs, so its own deprecation never fires — no double warning. - """ - cbar = { - _CBAR_TO_COLORBAR[k]: kwargs.pop(k) for k in _CBAR_TO_COLORBAR if k in kwargs - } - if cbar: - existing = kwargs.get("colorbar") - # Only a typed ColorBar (carries its own styling) or colorbar=False (bar - # hidden) can't be augmented by the loose kwargs. colorbar=True / None / - # absent carry no caption of their own, so fold the loose form in — else - # the caption that rendered before this migration would silently vanish. - if existing is False: - warnings.warn( - "The loose cbar_* / ticks_spacing kwargs are deprecated and were " - "ignored because colorbar=False hides the colour bar.", - DeprecationWarning, - stacklevel=3, - ) - elif isinstance(existing, colorbar_cls): - warnings.warn( - "The loose cbar_* / ticks_spacing kwargs are deprecated and were " - "ignored because an explicit colorbar=ColorBar was given; set the " - "styling on it instead.", - DeprecationWarning, - stacklevel=3, - ) - else: - warnings.warn( - "The loose cbar_* / ticks_spacing kwargs are deprecated; pass " - "colorbar=pyramids.plot.ColorBar(...) instead.", - DeprecationWarning, - stacklevel=3, - ) - kwargs["colorbar"] = colorbar_cls(**cbar) +def nonnull_group_kwargs(**groups: Any) -> dict[str, Any]: + """Return the cleopatra render-group kwargs that were actually set (not ``None``). - style = { - _POINT_TO_OVERLAY[k]: kwargs.pop(k) for k in _POINT_TO_OVERLAY if k in kwargs - } - if style: - points = kwargs.get("points") - if isinstance(points, point_overlay_cls): - warnings.warn( - "The loose point_* / pid_* kwargs are deprecated and were ignored " - "because points= is already a PointOverlay; set them on it instead.", - DeprecationWarning, - stacklevel=3, - ) - elif points is None: - warnings.warn( - "The loose point_* / pid_* kwargs are deprecated and have no effect " - "without points=; pass points=pyramids.plot.PointOverlay(points, ...).", - DeprecationWarning, - stacklevel=3, - ) - else: - warnings.warn( - "The loose point_* / pid_* kwargs are deprecated; pass " - "points=pyramids.plot.PointOverlay(points, ...) instead.", - DeprecationWarning, - stacklevel=3, - ) - kwargs["points"] = point_overlay_cls(points, **style) + Shared by the ``.plot`` facades: they expose the cleopatra render groups + (``color`` / ``contour`` / ``cells`` / ``data_style`` / ``classify``) as explicit + typed params and fold the set ones into the kwargs forwarded to the render backend. + Dropping the unset (``None``) groups keeps them off the render call, so an untouched + group never overrides the backend default. + + Args: + **groups: Candidate group objects keyed by their cleopatra render-method + parameter name (``color`` / ``contour`` / ``cells`` / ``data_style`` / + ``classify``). + + Returns: + A dict of only the groups whose value is not ``None``. + """ + return {name: value for name, value in groups.items() if value is not None} def render_array( @@ -335,7 +247,7 @@ def render_array( basemap: bool | str | dict[str, Any] | Basemap | None = None, basemap_epsg: int | None = None, **kwargs: Any, -): +) -> ArrayGlyph: """Build an ArrayGlyph and dispatch to the right cleopatra render path. Args: @@ -370,7 +282,7 @@ def render_array( animation_axis_values: Frame labels for the animation path. Required when ``mode == "animate"``. data_getter: Optional callable ``f(i) -> ndarray`` forwarded to - :meth:`cleopatra.array_glyph.ArrayGlyph.animate` as the + :meth:`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph.animate` as the ``data_getter`` kwarg. When set the animation streams each frame lazily through this callback instead of slicing the pre-materialised ``arr`` stack — used by @@ -388,7 +300,7 @@ def render_array( non-empty web-tile / basemap provider string adds a pyramids web-tile basemap underneath the rendered plot (tile mode is applied on ``"plot"`` and per-panel on ``"facet"``). A - :class:`cleopatra.geo.Basemap` is cleopatra's relief/features + :class:`cleopatra.basemap.geo.Basemap` is cleopatra's relief/features reference layer, forwarded to the glyph's own ``basemap=`` on the ``"plot"``/``"animate"`` render call; it is **not** supported on ``"facet"`` (raises). A ``dict`` is a deprecated alias translated to @@ -400,13 +312,13 @@ def render_array( ``basemap`` is truthy and this is ``None`` the helper raises :class:`ValueError`. **kwargs: Forwarded to the cleopatra entry point selected by - ``mode`` (including cleopatra >= 0.28 render params such as - ``colorbar=`` / ``full_bleed=``). + ``mode`` (including render params such as ``colorbar=`` / + ``full_bleed=``). Returns: The result object cleopatra returns for that mode — typically a - :class:`cleopatra.array_glyph.ArrayGlyph` for ``"plot"`` and - ``"animate"``, and a :class:`cleopatra.array_glyph.FacetGrid` + :class:`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph` for ``"plot"`` and + ``"animate"``, and a :class:`cleopatra.glyphs.gridded.array_glyph.FacetGrid` for ``"facet"``. Raises: @@ -414,8 +326,11 @@ def render_array( required mode-specific argument is missing, if ``basemap`` is truthy and ``basemap_epsg`` is ``None``, if a cleopatra ``Basemap`` (or equivalent dict) is passed on the ``"facet"`` - path, or if ``color_scale`` is not a recognised - :class:`~cleopatra.styles.ColorScale` value. + path, or if a removed loose colour-bar kwarg (``cbar_*`` / + ``ticks_spacing``) is passed. A removed loose styling kwarg + (``color_scale`` / ``style`` / ``levels`` / ``point_*`` / ...) + surfaces cleopatra's own "moved onto a grouped parameter object" + ``ValueError`` from the render call. Examples: - Single-slice plot path. Tagged ``+SKIP`` because the call @@ -531,22 +446,22 @@ def render_array( ``` """ require_cleopatra() - from cleopatra.array_glyph import ArrayGlyph, ColorBar, PointOverlay - from cleopatra.geo import Basemap - from cleopatra.styles import ColorScale - - # Deprecate + translate the loose plot kwargs to the typed specs (cbar_* -> - # ColorBar, point_*/pid_* -> PointOverlay, dict basemap -> Basemap) so every - # raster plot/animate call is steered onto the typed API in one place and - # cleopatra only ever sees the typed form. The facet path is skipped: - # cleopatra's ``ArrayGlyph.facet`` does not accept ``colorbar=ColorBar`` / - # ``PointOverlay`` (only the loose ``cbar_*`` kwargs, like the mesh glyph), so - # faceting keeps the loose forms — there is no typed alternative there. - # Once cleopatra#256 lands (``ArrayGlyph.facet`` accepts ``colorbar=ColorBar``), - # drop this facet exception (and the ``cbar_*`` in ``RENDER_ONLY_OVERRIDES`` - # below) so facet folds the loose kwargs like plot/animate. Tracked in #934. - if mode != "facet": - _migrate_deprecated_plot_specs(kwargs, ColorBar, PointOverlay) + from cleopatra.basemap.geo import Basemap + from cleopatra.glyphs.gridded.array_glyph import ( + ArrayGlyph, + PanelLabels, + PointOverlay, + ) + + # The loose styling kwargs are no longer translated here: they moved onto the typed + # render groups (color=ColorScaling / contour=Contour / cells=CellValues / + # data_style=DataStyle / points=PointOverlay / colorbar=ColorBar). Passing a form + # cleopatra removed (color_scale / style / levels / point_* / ...) surfaces cleopatra's + # own "moved onto a grouped parameter object" error; the loose cbar_* / ticks_spacing + # forms — which cleopatra still tolerates — are rejected here so pyramids exposes a + # single typed colour-bar surface (``colorbar=ColorBar``). + _reject_replaced_cbar_kwargs(kwargs) + # Translate the deprecated ``dict`` basemap alias to a ``Basemap``. if isinstance(basemap, dict) and basemap: warnings.warn( "Passing a dict as basemap= is deprecated; pass " @@ -599,25 +514,16 @@ def render_array( # The stretch parameters have been consumed by ``prepare_array`` above; # null them so the constructor call below cannot re-read inert values. rgb = surface_reflectance = cutoff = percentile = None - # Fail fast on an invalid ``color_scale`` with a pyramids-side message - # that lists the valid options, instead of deferring to a less-targeted - # cleopatra error deep in the render call. ``ColorScale`` lookup is - # case-insensitive, so any-case valid values pass through unchanged. - color_scale = kwargs.get("color_scale") - if color_scale is not None: - try: - ColorScale(color_scale) - except ValueError: - valid = [s.value for s in ColorScale] - raise ValueError( - f"Unsupported color_scale {color_scale!r}; valid options: {valid}." - ) from None + # A bare ``(N, 3)`` points array is no longer auto-wrapped by cleopatra (it raises on + # the overlay-drawing kinds, and is silently ignored on contour); wrap it in a + # ``PointOverlay`` so pyramids callers can keep passing a plain array (the ``points`` + # param is typed ``np.ndarray | PointOverlay``). A value already given as a + # ``PointOverlay`` is left as-is. + points = kwargs.get("points") + if points is not None and not isinstance(points, PointOverlay): + kwargs["points"] = PointOverlay(np.asarray(points)) - # Normalise + version-gate the style/hillshade presets (shared with the mesh - # path). Mutates kwargs to drop a no-op hillshade; raises a clear upgrade hint - # on a cleopatra whose ArrayGlyph lacks the presets. (issue #737) option_keys = ArrayGlyph.option_keys() - _guard_style_hillshade(kwargs, option_keys) # Unwrap a wrapping geographic longitude before handing curvilinear coords # to cleopatra, so its pcolormesh doesn't smear a ~178-degree quad across @@ -660,13 +566,10 @@ def render_array( # render method only overwrites ``default_options["title"]`` when its # ``title`` arg is not ``None``, so a constructor-set title survives and # routing it to the constructor (via ``option_keys()``) is correct. - # ``kind`` is force-routed to the render call. The deprecated loose cbar_* - # kwargs are too — but only the ``facet`` path still carries them here (plot / - # animate already folded them into ``colorbar=ColorBar(...)`` above, so this is - # a no-op there). cleopatra's ``facet`` renders them only when they reach the - # facet *call* (its per-panel constructors), so force-routing keeps the loose - # cbar_* rendering on facet instead of being dropped on the parent constructor. - RENDER_ONLY_OVERRIDES = {"kind", *_CBAR_TO_COLORBAR} + # ``kind`` is force-routed to the render call. The loose cbar_* kwargs no longer + # need force-routing: they are rejected up front by ``_reject_replaced_cbar_kwargs`` + # (``colorbar=ColorBar`` is the only colour-bar surface), so they never reach the split. + RENDER_ONLY_OVERRIDES = {"kind"} # Reuse the option set resolved for the style/hillshade guard above — it is # cleopatra's declared constructor options and does not change within a call. ctor_option_keys = option_keys @@ -677,6 +580,9 @@ def render_array( ctor_kwargs[key] = value else: render_kwargs[key] = value + # The cleopatra render groups (``color`` / ``contour`` / ``cells`` / ``data_style``) the + # facades pass are not constructor options, so the split above already routed them to + # ``render_kwargs`` — they ride on the plot / animate / facet call as-is. # The ``"animate"`` path only flows kwargs into ``cleo.animate(...)``, # not the constructor — keys like ``interval`` are valid for animate # but not in cleopatra's ``DEFAULT_OPTIONS`` and would trigger an @@ -717,7 +623,7 @@ def render_array( # with pyramids' pre-existing web-tile ``basemap=``, so dispatch on the type: # - a ``str`` provider name (or ``True``) is a pyramids web-tile basemap # drawn under the raster -- pyramids owns this via ``add_basemap``; - # - a ``cleopatra.geo.Basemap`` is cleopatra's relief/features reference + # - a ``cleopatra.basemap.geo.Basemap`` is cleopatra's relief/features reference # layer, forwarded to the glyph's own ``basemap=`` on the render call. # (A non-empty ``dict`` was already deprecated + translated to a # ``Basemap`` up front, so only an empty ``{}`` reaches here — no basemap.) @@ -797,7 +703,18 @@ def _apply_basemap(target_ax: Any) -> None: "(basemap='') for per-panel tiles, or plot without faceting " "for a relief/features basemap." ) - result = cleo.facet(**facet_kwargs, **render_kwargs) + # cleopatra 0.30 renamed facet's ``figsize`` to ``figure_size`` and moved the + # per-panel coordinate labels onto a ``PanelLabels`` group (``col`` / ``row``). + # Translate the loose facet_kwargs (built by the NetCDF facet path) into the new + # names so the NetCDF facet builder can keep emitting the historical spelling. + facet_call = dict(facet_kwargs) + col_coords = facet_call.pop("col_coords", None) + row_coords = facet_call.pop("row_coords", None) + if col_coords is not None or row_coords is not None: + facet_call["labels"] = PanelLabels(col=col_coords, row=row_coords) + if "figsize" in facet_call: + facet_call["figure_size"] = facet_call.pop("figsize") + result = cleo.facet(**facet_call, **render_kwargs) if tile_basemap: # Every facet panel renders the same spatial domain (cleopatra # reuses the parent extent / curvilinear coords across panels), @@ -824,7 +741,7 @@ def mesh_render( """N-6 — sibling of :func:`render_array` for UGRID mesh data. Routes a pyramids ``Mesh2d`` + a per-element data array through - cleopatra's :class:`~cleopatra.mesh_glyph.MeshGlyph`, returning the + cleopatra's :class:`~cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph`, returning the glyph instance. Mirrors the :func:`render_array` contract — "single backend abstraction, one entry point per cleopatra glyph" — so the raster facade (:meth:`pyramids.dataset.Dataset.plot`, @@ -852,7 +769,7 @@ def mesh_render( ``edgecolor``, ``colorbar``, ``title``. Returns: - cleopatra.mesh_glyph.MeshGlyph: The same instance that + cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph: The same instance that :func:`pyramids.netcdf.ugrid.plot.plot_mesh_data` returns. Raises: @@ -906,19 +823,10 @@ def mesh_render( if basemap and basemap_epsg is None: raise ValueError("Dataset must have a CRS (epsg) to use basemap.") require_cleopatra() - from cleopatra.mesh_glyph import MeshGlyph - + # ``plot_mesh_data`` forwards the typed render groups straight to ``MeshGlyph.plot`` and + # rejects the removed loose colour-bar kwargs, so no pre-processing is needed here. from pyramids.netcdf.ugrid.plot import plot_mesh_data - # Normalise + version-gate the style/hillshade presets against MeshGlyph, the - # same way render_array does against ArrayGlyph, so the mesh path exposes - # cleopatra's data-style presets with a clean upgrade hint on older cleopatra. - # Note: unlike the raster path (where style is an ArrayGlyph *constructor* - # option), plot_mesh_data forwards style/hillshade to MeshGlyph.plot(), not the - # constructor. Feature-detecting against option_keys() therefore assumes cleopatra - # declares them there in lock-step with .plot() accepting them (true on 0.24). - _guard_style_hillshade(kwargs, MeshGlyph.option_keys()) - result = plot_mesh_data(mesh, data, location=location, **kwargs) if basemap: # The guard above already proved basemap_epsg is set when basemap is diff --git a/src/pyramids/dataset/abstract_dataset.py b/src/pyramids/dataset/abstract_dataset.py index 5e3f9acbaf..52fd6a82d9 100644 --- a/src/pyramids/dataset/abstract_dataset.py +++ b/src/pyramids/dataset/abstract_dataset.py @@ -1062,11 +1062,9 @@ def plot( self, band: int | None = None, exclude_value: Any | None = None, - rgb: list[int] | None = None, - surface_reflectance: int | None = None, - cutoff: list | None = None, overview: bool = False, overview_index: int = 0, + rgb_options: dict | None = None, **kwargs, ): """Plot. @@ -1078,19 +1076,14 @@ def plot( The band you want to get its data. Default is 0. exclude_value (Any, optional): Value to exclude from the plot. Default is None. - rgb (List[int], optional): - RGB band indices. Default is [3, 2, 1]. - surface_reflectance (int | None, optional): - Surface reflectance value used to normalise satellite reflectance bands - (typically ``10000`` for Sentinel-2). Default is ``None`` — concrete - subclasses are responsible for picking a meaningful default when relevant. - cutoff (List, optional): - Clip the range of pixel values for each band (take only the pixel values from 0 to the value of - the cutoff and scale them back to between 0 and 1). Default is None. overview (bool, optional): True if you want to plot the overview. Default is False. overview_index (int, optional): Index of the overview. Default is 0. + rgb_options (dict, optional): + Grouped Sentinel-imagery options for a true-colour composite. Accepted + keys: ``"rgb"`` (band indices), ``"surface_reflectance"``, ``"cutoff"``, + ``"percentile"``. Default is ``None``. **kwargs: Additional plotting options. points (array | PointOverlay): Point overlay. A 3 column array with the first column as the value you want to display for @@ -1098,10 +1091,8 @@ def plot( the column index in the array. The second and third columns tell the location of the point in the array. To style the points, pass a ``pyramids.plot.PointOverlay(points, color=..., size=..., label_color=..., - label_size=...)`` instead — pyramids folds the loose ``point_color`` / ``point_size`` / - ``point_label_color`` / ``point_label_size`` / ``pid_color`` / ``pid_size`` kwargs into a - ``PointOverlay`` and emits a ``DeprecationWarning``; set the styling on the - ``PointOverlay`` instead. + label_size=...)`` instead of a bare array; the loose ``point_*`` / ``pid_*`` + styling kwargs were removed — set the styling on the ``PointOverlay``. figsize (tuple, optional): Figure size. The default is (8, 8). title (str, optional): @@ -1110,33 +1101,22 @@ def plot( Title size. The default is 15. colorbar (bool | ColorBar, optional): Colour-bar spec ``pyramids.plot.ColorBar(label=..., length=..., orientation=..., - label_size=..., label_rotation=..., label_location=..., ticks_spacing=...)`` - (cleopatra >= 0.28) — the complete, preferred replacement for the loose ``cbar_*`` / - ``ticks_spacing`` kwargs, which are deprecated (still accepted, but they emit a - ``DeprecationWarning``). ``False`` hides the bar, ``None`` uses the default. - color_scale (str, optional): - Color-scale mode. One of "linear", "power", "sym-lognorm", "boundary-norm", "midpoint" - (case-insensitive), or a ``cleopatra.styles.ColorScale`` member. Integer codes are no - longer accepted. The default is "linear". - gamma (float, optional): - Exponent for the "power" color scale. The default is 1./2. - line_threshold (float, optional): - ``linthresh`` for the "sym-lognorm" color scale. The default is 0.0001. - line_scale (float, optional): - ``linscale`` for the "sym-lognorm" color scale. The default is 0.001. - bounds (List, optional): - A list of numbers used as discrete bounds for the "boundary-norm" color scale. Default is None. - midpoint (float, optional): - Midpoint value for the "midpoint" color scale. The default is 0. + label_size=..., label_rotation=..., label_location=..., ticks_spacing=...)``. + The loose ``cbar_*`` / ``ticks_spacing`` kwargs it replaces were removed — + passing one now raises a ``ValueError``. ``False`` hides the bar, ``None`` + uses the default. cmap (str, optional): Color style. The default is 'coolwarm_r'. - display_cell_value (bool, optional): - True if you want to display the values of the cells as text. - num_size (int, optional): - Size of the numbers plotted on top of each cell. The default is 8. - background_color_threshold (float | int, optional): - Threshold value. If the value of the cell is greater, the plotted numbers will be black; if smaller, - the plotted number will be white. If None, maxvalue/2 will be considered. The default is None. + + Colour-scale, contour, cell-value and data-style options moved onto the typed + render groups (all re-exported from ``pyramids.plot``): pass + ``color=ColorScaling(...)`` (linear / power / sym-log / boundary / midpoint + norm), ``contour=Contour(levels=..., ...)``, ``cells=CellValues(show=..., + size=..., background_threshold=...)`` and ``data_style=DataStyle(style=..., + hillshade=...)``. The loose forms they replace (``color_scale`` / ``gamma`` / + ``line_threshold`` / ``line_scale`` / ``bounds`` / ``midpoint`` / ``levels`` / + ``display_cell_value`` / ``num_size`` / ``background_color_threshold`` / + ``style`` / ``hillshade``) were removed and now raise a ``ValueError``. Returns: Tuple[Axes, Any]: diff --git a/src/pyramids/dataset/collection.py b/src/pyramids/dataset/collection.py index 62bcf6a900..6fd1e02f67 100644 --- a/src/pyramids/dataset/collection.py +++ b/src/pyramids/dataset/collection.py @@ -11,7 +11,7 @@ from collections.abc import Callable, Sequence from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Unpack, cast import numpy as np import pandas as pd @@ -30,7 +30,7 @@ ) from pyramids.base.crs import crs_spec from pyramids.base.remote import cloud_config_from_env -from pyramids.dataset._plot_helpers import render_array +from pyramids.dataset._plot_helpers import nonnull_group_kwargs, render_array from pyramids.dataset._reduce_ops import resolve_dask_op from pyramids.dataset._stac import from_point as _from_point from pyramids.dataset._stac import from_stac as _from_stac @@ -50,8 +50,16 @@ from pyramids.feature import FeatureCollection if TYPE_CHECKING: - from cleopatra.array_glyph import ArrayGlyph, FrameLabel - from cleopatra.geo import Basemap + from cleopatra.basemap.geo import Basemap + from cleopatra.glyphs.gridded.array_glyph import ( + AnimateKwargs, + ArrayGlyph, + FrameLabel, + PointOverlay, + ) + from cleopatra.styling.colorbar import ColorBar + from cleopatra.styling.params import CellValues, Contour, DataStyle + from cleopatra.styling.scaling import ColorScaling from dask.delayed import Delayed @@ -2562,18 +2570,57 @@ def iloc(self, i: int) -> Dataset: """ return self._dataset_at(i) + def _validate_rgb_animation( + self, rgb: list[int], exclude_value: Any | None + ) -> None: + """Validate the ``rgb`` band layout for an RGB time-lapse (issue #538). + + Args: + rgb: The band indices requested for the true-colour composite. + exclude_value: The caller's ``exclude_value`` (ignored for RGB frames; + a non-``None`` value warns because true-colour frames are not masked). + + Raises: + ValueError: If ``rgb`` is not 3 or 4 indices, has a negative index, or asks + for more bands than the collection's datasets carry. + """ + if len(rgb) not in (3, 4): + raise ValueError( + f"rgb must list 3 band indices (RGB) or 4 (RGBA), got " + f"{rgb!r} with {len(rgb)} entries." + ) + if min(rgb) < 0: + raise ValueError(f"rgb band indices must be non-negative, got {rgb!r}.") + if exclude_value is not None: + warnings.warn( + "exclude_value is ignored for RGB animations; true-colour " + "frames are not masked. Drop exclude_value, or render a " + "single band to mask by no-data.", + UserWarning, + stacklevel=3, + ) + needed = max(rgb) + 1 + if self.base.band_count < needed: + raise ValueError( + f"rgb={rgb} needs at least {needed} bands, but the " + f"collection's datasets have {self.base.band_count}." + ) + def plot( self, band: int = 0, exclude_value: Any | None = None, - rgb: list[int] | None = None, - surface_reflectance: int | None = None, - cutoff: list | None = None, - percentile: int | None = None, rgb_options: dict | None = None, basemap: bool | str | dict[str, Any] | Basemap | None = None, frame_label: FrameLabel | None = None, - **kwargs: Any, + colorbar: bool | ColorBar | None = None, + points: np.ndarray | PointOverlay | None = None, + color: ColorScaling | None = None, + contour: Contour | None = None, + cells: CellValues | None = None, + data_style: DataStyle | None = None, + animation_axis_values: Any = None, + **kwargs: Unpack[AnimateKwargs], ) -> ArrayGlyph: r"""Render the collection as an animated stack of band slices. @@ -2608,70 +2655,71 @@ def plot( Ignored when ``rgb`` is set (true-colour frames are not masked); passing it together with ``rgb`` emits a :class:`UserWarning`. - rgb (list[int], optional): - Band indices ``[red, green, blue]`` (or four, with - alpha) to composite into a true-colour time-lapse. When - set, every timestep is rendered as an RGB frame instead - of a single colormapped band, and the result is a - ``(time, rows, cols, 3)`` animation with no colorbar. - Each ``Dataset`` in the collection must carry at least - ``max(rgb) + 1`` bands. Default ``None`` (single-band - colormapped animation). **Deprecated**; pass via - ``rgb_options={"rgb": [...]}`` instead. - surface_reflectance (int, optional): - Surface-reflectance scale for normalising RGB bands - (e.g. ``10000`` for Sentinel-2, ``255`` for 8-bit). - Only used when ``rgb`` is set. Default ``None``. - **Deprecated**; pass via - ``rgb_options={"surface_reflectance": ...}``. - cutoff (list, optional): - Per-band clip values for the RGB stretch. Only used when - ``rgb`` is set. Default ``None``. **Deprecated**; pass - via ``rgb_options={"cutoff": ...}``. - percentile (int, optional): - Percentile stretch for the RGB bands (takes precedence - over ``surface_reflectance``). Only used when ``rgb`` is - set. Default ``None``. **Deprecated**; pass via - ``rgb_options={"percentile": ...}``. rgb_options (dict, optional): - Recommended grouped form of the RGB parameters above. - Accepted keys: ``"rgb"``, ``"surface_reflectance"``, - ``"cutoff"``, ``"percentile"``. Mirrors - :meth:`Dataset.plot`. On collision with a loose kwarg the - ``rgb_options`` value wins. Default ``None``. + Grouped Sentinel-imagery options for a true-colour time-lapse (mirrors + :meth:`Dataset.plot`). Accepted keys: ``"rgb"`` (band indices + ``[red, green, blue(, alpha)]`` — every timestep renders as an RGB frame, + a ``(time, rows, cols, 3)`` animation with no colorbar; each ``Dataset`` + must carry at least ``max(rgb) + 1`` bands), ``"surface_reflectance"`` + (scale for normalising RGB bands, e.g. ``10000`` for Sentinel-2), + ``"cutoff"`` (per-band clip values), ``"percentile"`` (percentile stretch, + takes precedence over ``surface_reflectance``). Default ``None`` + (single-band colormapped animation). basemap (bool, str, or Basemap, optional): Reference layer under the animation, dispatched by type. ``True`` or a tile-provider string (e.g. ``"CartoDB.Positron"``) overlays a pyramids web-tile basemap; a ``pyramids.plot.Basemap(relief=..., - features=...)`` (cleopatra >= 0.28) draws a shaded-relief / + features=...)`` draws a shaded-relief / coastline layer instead. The base raster's CRS is supplied automatically. Default ``None`` (no basemap). Requires the ``[viz]`` extra. frame_label (FrameLabel, optional): Typed per-frame label spec ``pyramids.plot.FrameLabel(...)`` - (cleopatra >= 0.28) that styles the animation's frame caption + that styles the animation's frame caption (colour, size, placement). ``animation_axis_values`` sets the label *text* per frame; ``frame_label`` styles it. Default ``None`` (cleopatra's default frame label). + colorbar (bool or ColorBar, optional): + Colour-bar spec ``pyramids.plot.ColorBar(label=…, length=…, + orientation=…, label_size=…, label_rotation=…, label_location=…, + ticks_spacing=…)``. The loose ``cbar_*`` / ``ticks_spacing`` kwargs it + replaces were removed — passing one now raises a :class:`ValueError`. + ``False`` hides it, ``None`` uses the default. Default ``None``. + points (np.ndarray or PointOverlay, optional): + Point overlay. A 3-column array ``(value, row, col)`` draws unstyled + points; to style them pass a ``pyramids.plot.PointOverlay(points, + color=…, size=…, label_color=…, label_size=…)`` instead (the loose + ``point_*`` / ``pid_*`` styling kwargs were removed). Default ``None``. + color (ColorScaling, optional): + Colour-scale spec ``pyramids.plot.ColorScaling`` (linear / power / sym-log / + boundary / midpoint norm), e.g. ``ColorScaling.power(gamma=0.7)``. Default + ``None``. + contour (Contour, optional): + Contour-line spec ``pyramids.plot.Contour(levels=…, labels=…, label_kw=…)``. + Default ``None``. + cells (CellValues, optional): + Per-cell value annotation ``pyramids.plot.CellValues(show=…, size=…, + background_threshold=…)``. Default ``None``. + data_style (DataStyle, optional): + Data-style / relief spec ``pyramids.plot.DataStyle(style=…, hillshade=…)``. + Default ``None``. + animation_axis_values (sequence, optional): + Per-frame labels for the animation, one per timestep. Defaults to the + collection's ``time`` axis when set (e.g. dates parsed by + ``read_multiple_files``), else ``range(time_length)`` (index labels). + Pass a sequence to override (e.g. ``range(2000, 2024)``); it must carry + exactly one label per timestep or a :class:`ValueError` is raised. + Default ``None``. **kwargs: + Still-loose cleopatra render kwargs (colour-scale, contour, and cell-value + styling moved onto the ``color`` / ``contour`` / ``cells`` params above): + | Parameter | Type | Description | |----------------------------|-----------------------|-------------| - | animation_axis_values | sequence, optional | Per-frame labels for the animation, one per timestep. Defaults to the collection's `time` axis when set (e.g. dates parsed by `read_multiple_files`), else `range(time_length)` (index labels). Pass a sequence here to override (e.g. `range(2000, 2024)`). | - | points | array \| PointOverlay | Point overlay. A 3-column array (value to display, row index, column index) draws unstyled points. To style them, pass a `cleopatra.array_glyph.PointOverlay(points, color=..., size=..., label_color=..., label_size=...)` instead — on cleopatra >= 0.26 the loose `point_color` / `point_size` / `pid_color` / `pid_size` kwargs are deprecated; set the styling on the `PointOverlay` instead. | | figsize | tuple, optional | Figure size. Default is `(8, 8)`. | | title | str, optional | Title of the plot. Default is `'Total Discharge'`. | | title_size | int, optional | Title size. Default is `15`. | - | colorbar | bool \| ColorBar, optional | Colour-bar spec `pyramids.plot.ColorBar(label=…, length=…, orientation=…, label_size=…, label_rotation=…, label_location=…, ticks_spacing=…)` (cleopatra >= 0.28). The loose `cbar_*` / `ticks_spacing` kwargs it replaces are deprecated — still accepted, but they emit a `DeprecationWarning`. `False` hides it, `None` uses the default. | - | color_scale | str, optional | Color-scale mode (default `"linear"`): one of `"linear"`, `"power"`, `"sym-lognorm"`, `"boundary-norm"`, `"midpoint"` (case-insensitive), or a `cleopatra.styles.ColorScale` member. Integer codes are no longer accepted. | - | gamma | float, optional | Exponent for `color_scale="power"`. Default is `1/2`. | - | line_threshold | float, optional | `linthresh` for `color_scale="sym-lognorm"`. Default is `0.0001`. | - | line_scale | float, optional | `linscale` for `color_scale="sym-lognorm"`. Default is `0.001`. | - | bounds | list | Discrete bounds for `color_scale="boundary-norm"`. Default is `None`. | - | midpoint | float, optional | Midpoint value for `color_scale="midpoint"`. Default is `0`. | | cmap | str, optional | Color map style. Default is `'coolwarm_r'`. | - | display_cell_value | bool | Whether to display the values of the cells as text. | - | num_size | int, optional | Size of the numbers plotted on top of each cell. Default is `8`. | - | background_color_threshold | float \| int, optional| Threshold for deciding number color: if value > threshold -> black; else white. If `None`, uses `max_value/2`. Default is `None`. | Returns: @@ -2685,7 +2733,7 @@ def plot( ValueError: When ``rgb`` does not list exactly 3 (RGB) or 4 (RGBA) band indices, when any index is negative, or when the collection's datasets carry fewer than ``max(rgb) + 1`` bands. - Also raised (via ``_merge_rgb_options``) for an unknown key + Also raised (via ``_unpack_rgb_options``) for an unknown key in ``rgb_options``. Warns: @@ -2729,20 +2777,15 @@ def plot( See Also: - :meth:`pyramids.dataset.Dataset.plot`: The single-frame renderer (still or RGB still) for one ``Dataset``; shares the - ``rgb`` / ``rgb_options`` contract via ``_merge_rgb_options``. + ``rgb_options`` contract via ``_unpack_rgb_options``. - :func:`pyramids.dataset._plot_helpers.render_array`: The shared cleopatra dispatch that composites the true-colour frames for the animate path. """ - # Resolve the grouped ``rgb_options`` against the deprecated loose - # kwargs exactly as ``Dataset.plot`` does, so both facades share one - # RGB-parameter contract (and one deprecation message). - rgb, surface_reflectance, cutoff, percentile = Dataset._merge_rgb_options( - rgb_options=rgb_options, - rgb=rgb, - surface_reflectance=surface_reflectance, - cutoff=cutoff, - percentile=percentile, + # Unpack the grouped ``rgb_options`` exactly as ``Dataset.plot`` does, so both + # facades share one RGB-parameter contract. + rgb, surface_reflectance, cutoff, percentile = Dataset._unpack_rgb_options( + rgb_options ) # Frame labels for the animation. Default to the collection's time axis # when it has one (e.g. dates parsed from the file names by @@ -2753,7 +2796,9 @@ def plot( default_labels = ( list(self.time) if self.time is not None else list(range(self.time_length)) ) - axis_values = kwargs.pop("animation_axis_values", default_labels) + axis_values = ( + default_labels if animation_axis_values is None else animation_axis_values + ) if not hasattr(axis_values, "__len__"): axis_values = list(axis_values) # materialise a generator override # An explicit override must carry exactly one label per frame; the @@ -2773,9 +2818,18 @@ def plot( animate_extras: dict[str, Any] = { "basemap": basemap, "basemap_epsg": self.base.epsg, + "colorbar": colorbar, + "points": points, } if frame_label is not None: animate_extras["frame_label"] = frame_label + # Fold the explicitly-set cleopatra render groups in; unset ones are dropped so + # they do not override cleopatra's backend default for that group. + animate_extras.update( + nonnull_group_kwargs( + color=color, contour=contour, cells=cells, data_style=data_style + ) + ) # Materialise the cube on demand for plotting. The render helper # expects a single (time, rows, cols) numpy array; reading each # Dataset's band into one stacked array is fine for a plot call @@ -2784,30 +2838,10 @@ def plot( if rgb is not None: # RGB time-lapse: read the FULL multi-band array per timestep and # stack to (time, bands, rows, cols); render_array composites the - # true-colour frames. Guard the band layout here so a misshapen - # ``rgb`` raises a clear error instead of cleopatra silently + # true-colour frames. The band layout is validated in a helper so a + # misshapen ``rgb`` raises a clear error instead of cleopatra silently # collapsing the time axis into the colour channels (issue #538). - if len(rgb) not in (3, 4): - raise ValueError( - f"rgb must list 3 band indices (RGB) or 4 (RGBA), got " - f"{rgb!r} with {len(rgb)} entries." - ) - if min(rgb) < 0: - raise ValueError(f"rgb band indices must be non-negative, got {rgb!r}.") - if exclude_value is not None: - warnings.warn( - "exclude_value is ignored for RGB animations; true-colour " - "frames are not masked. Drop exclude_value, or render a " - "single band to mask by no-data.", - UserWarning, - stacklevel=2, - ) - needed = max(rgb) + 1 - if self.base.band_count < needed: - raise ValueError( - f"rgb={rgb} needs at least {needed} bands, but the " - f"collection's datasets have {self.base.band_count}." - ) + self._validate_rgb_animation(rgb, exclude_value) data = np.stack([ds.read_array(band=None) for ds in self.datasets], axis=0) return render_array( arr=data, diff --git a/src/pyramids/dataset/dataset.py b/src/pyramids/dataset/dataset.py index df0474e432..66e54a2329 100644 --- a/src/pyramids/dataset/dataset.py +++ b/src/pyramids/dataset/dataset.py @@ -13,7 +13,7 @@ from collections.abc import Sequence from numbers import Number from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Unpack, cast import geopandas as gpd import numpy as np @@ -38,6 +38,7 @@ ) from pyramids.base.remote import cloud_config_from_env, redact_credentials from pyramids.dataset._ogc_coverages import from_ogc_coverages as _from_ogc_coverages +from pyramids.dataset._plot_helpers import nonnull_group_kwargs from pyramids.dataset._wcs import from_wcs as _from_wcs from pyramids.dataset._wms import from_wms as _from_wms from pyramids.dataset._wms import from_wmts as _from_wmts @@ -232,7 +233,11 @@ def _remap_nodata_to(arr: np.ndarray, src_nd: Any, dst_nd: Any) -> np.typing.NDA if TYPE_CHECKING: import dask.array as da - from cleopatra.geo import Basemap + from cleopatra.basemap.geo import Basemap + from cleopatra.glyphs.gridded.array_glyph import PlotKwargs, PointOverlay + from cleopatra.styling.colorbar import ColorBar + from cleopatra.styling.params import CellValues, Contour, DataStyle + from cleopatra.styling.scaling import ColorScaling from geopandas import GeoDataFrame @@ -798,19 +803,23 @@ def _infer_rgb_band_order(self) -> list[int]: resolved_rgb = [int(v) for v in cast("list[int]", candidate)] return resolved_rgb - def plot( + def plot( # type: ignore[override] # narrows **kwargs to cleopatra's typed PlotKwargs self, band: int | None = None, exclude_value: Any | None = None, - rgb: list[int] | None = None, - surface_reflectance: int | None = None, - cutoff: list | None = None, overview: bool | None = False, overview_index: int | None = 0, - percentile: int | None = None, basemap: bool | str | dict[str, Any] | Basemap | None = None, + colorbar: bool | ColorBar | None = None, + points: np.ndarray | PointOverlay | None = None, + kind: str = "auto", + title: str | None = None, + color: ColorScaling | None = None, + contour: Contour | None = None, + cells: CellValues | None = None, + data_style: DataStyle | None = None, rgb_options: dict | None = None, - **kwargs: Any, + **kwargs: Unpack[PlotKwargs], ): """Plot the values/overviews of a band. @@ -826,11 +835,8 @@ def plot( facade defaults to band ``0``. See :meth:`Analysis.plot` for the full kwargs surface. - The four satellite-imagery kwargs ``rgb``, ``surface_reflectance``, ``cutoff``, - and ``percentile`` may be grouped under a single ``rgb_options=`` dict - (recommended) or passed loose at the top level (deprecated; emits - :class:`DeprecationWarning`). When both forms are mixed, the values inside - ``rgb_options`` win. + The four satellite-imagery options (``rgb``, ``surface_reflectance``, ``cutoff``, + ``percentile``) are passed through the single ``rgb_options=`` dict. Args: band (int, optional): @@ -838,41 +844,53 @@ def plot( :meth:`_resolve_plot_band`. exclude_value (Any, optional): Pixel value to mask out before plotting. Default is ``None``. - rgb (list[int], optional): - **Deprecated**; pass via ``rgb_options={"rgb": [...]}`` instead. - Three- or four-element list of band indices ``[r, g, b]`` (optionally - ``[r, g, b, a]``) to render the dataset as a true-colour composite. - Only honoured when the dataset has at least 3 bands and at least one - band reports a colour interpretation. Default is ``None``. - surface_reflectance (int, optional): - **Deprecated**; pass via ``rgb_options={"surface_reflectance": ...}``. - Surface-reflectance scale factor used to normalise satellite reflectance - bands (typically ``10000`` for Sentinel-2). Default is ``None``. - cutoff (list, optional): - **Deprecated**; pass via ``rgb_options={"cutoff": ...}``. - Per-band clip values used when rendering RGB composites. Default is - ``None``. overview (bool, optional): If ``True``, plot the overview pyramid level instead of the full-resolution array. Default is ``False``. overview_index (int, optional): Index of the overview level to plot when ``overview=True``. Default is ``0``. - percentile (int, optional): - **Deprecated**; pass via ``rgb_options={"percentile": ...}``. - Percentile used when computing colour-scale limits. Default is ``None``. basemap (bool, str, or Basemap, optional): Reference layer, dispatched by type. ``True`` or a tile-provider string (e.g. ``"CartoDB.Positron"``) overlays a pyramids web-tile basemap. A - ``pyramids.plot.Basemap(relief=..., features=...)`` (cleopatra >= 0.28) + ``pyramids.plot.Basemap(relief=..., features=...)`` draws a shaded-relief / coastline layer instead. Passing a ``dict`` here is a deprecated alias for ``Basemap`` (emits a ``DeprecationWarning``). Default is ``None``. Requires the ``[viz]`` extra. + colorbar (bool or ColorBar, optional): + Colour-bar spec. A ``pyramids.plot.ColorBar(label=…, length=…, + orientation=…, …)`` draws a configured bar. The loose ``cbar_*`` / + ``ticks_spacing`` kwargs it replaces were removed — passing one now raises a + :class:`ValueError` pointing here. ``False`` hides the bar, ``None`` uses + cleopatra's default. Default is ``None``. + points (np.ndarray or PointOverlay, optional): + Point overlay. A 3-column array ``(value, row, col)`` draws unstyled + points; pass a ``pyramids.plot.PointOverlay(points, color=…, size=…, …)`` + to style them. Default is ``None``. + kind (str, optional): + Renderer to use. ``"auto"`` (default) picks per data; otherwise one of + ``"imshow"`` / ``"pcolormesh"`` / ``"contour"`` / ``"contourf"``. + title (str, optional): + Axes title. Default is ``None`` (cleopatra's default title). + color (ColorScaling, optional): + Colour-scale spec ``pyramids.plot.ColorScaling`` (linear / power / sym-log / + boundary / midpoint norm), e.g. ``ColorScaling.power(gamma=0.7)`` or + ``ColorScaling.boundary(bounds=[0, 0.5, 1])``. Default ``None``. + contour (Contour, optional): + Contour-line spec ``pyramids.plot.Contour(levels=…, labels=…, label_kw=…)``. + Default ``None``. + cells (CellValues, optional): + Per-cell value annotation ``pyramids.plot.CellValues(show=…, size=…, + background_threshold=…)``. Default ``None``. + data_style (DataStyle, optional): + Data-style / relief spec ``pyramids.plot.DataStyle(style=…, hillshade=…)``. + Default ``None``. rgb_options (dict, optional): - Grouped Sentinel-imagery kwargs. Accepted keys: - ``"rgb"``, ``"surface_reflectance"``, ``"cutoff"``, - ``"percentile"``. Recommended over the loose top-level - kwargs (which emit :class:`DeprecationWarning`). Default - is ``None``. + Grouped Sentinel-imagery options for a true-colour composite. Accepted + keys: ``"rgb"`` (3- or 4-element band-index list ``[r, g, b(, a)]``, only + honoured when the dataset has >= 3 bands and a colour interpretation), + ``"surface_reflectance"`` (reflectance scale factor, e.g. ``10000`` for + Sentinel-2), ``"cutoff"`` (per-band clip values), ``"percentile"`` + (percentile stretch). Default is ``None``. **kwargs: Additional keyword arguments forwarded verbatim to :meth:`Analysis.plot`. See that method for the full kwargs surface @@ -938,28 +956,19 @@ def plot( ... rgb_options={"rgb": [0, 1, 2], "surface_reflectance": 255}, ... ) - ``` - - - The deprecated loose-kwarg form still works but emits a - :class:`DeprecationWarning`. New code should prefer the - grouped ``rgb_options=`` form shown above: - - ```python - >>> cleo = rgb_ds.plot( # doctest: +SKIP - ... rgb=[0, 1, 2], surface_reflectance=255, - ... ) - DeprecationWarning: Passing `rgb=`, `surface_reflectance=`... - ``` """ - rgb, surface_reflectance, cutoff, percentile = self._merge_rgb_options( - rgb_options=rgb_options, - rgb=rgb, - surface_reflectance=surface_reflectance, - cutoff=cutoff, - percentile=percentile, + rgb, surface_reflectance, cutoff, percentile = self._unpack_rgb_options( + rgb_options ) resolved_band, resolved_rgb = self._resolve_plot_band(band, rgb) + # Spread the explicitly-set cleopatra render groups as their own ``**`` (not merged + # into the typed ``**kwargs``, whose PlotKwargs TypedDict has no group keys); the + # unset ones are dropped so they do not override cleopatra's backend default for + # that group. + group_kwargs = nonnull_group_kwargs( + color=color, contour=contour, cells=cells, data_style=data_style + ) return self.analysis.plot( band=resolved_band, exclude_value=exclude_value, @@ -970,127 +979,50 @@ def plot( overview_index=overview_index, percentile=percentile, basemap=basemap, + colorbar=colorbar, + points=points, + kind=kind, + title=title, + **group_kwargs, **kwargs, ) @staticmethod - def _merge_rgb_options( - *, + def _unpack_rgb_options( rgb_options: dict | None, - rgb: list[int] | None, - surface_reflectance: int | None, - cutoff: list | None, - percentile: int | None, ) -> tuple[list[int] | None, int | None, list | None, int | None]: - """Merge `rgb_options=` with the deprecated loose top-level kwargs. - - Returns the resolved four-tuple ``(rgb, surface_reflectance, - cutoff, percentile)`` passed forward to the renderer. Values in - ``rgb_options`` win over the loose kwargs on collision; using - any of the four loose kwargs emits a :class:`DeprecationWarning` - recommending the grouped form. + """Unpack the ``rgb_options`` group into the four Sentinel-imagery values. Args: - rgb_options: Recommended grouped form (``{"rgb": ..., ...}``). - Accepted keys: ``"rgb"``, ``"surface_reflectance"``, - ``"cutoff"``, ``"percentile"``. ``None`` means - no grouped options were supplied. - rgb: Deprecated top-level kwarg. - surface_reflectance: Deprecated top-level kwarg. - cutoff: Deprecated top-level kwarg. - percentile: Deprecated top-level kwarg. + rgb_options: Grouped Sentinel-imagery options, or ``None``. Accepted keys: + ``"rgb"``, ``"surface_reflectance"``, ``"cutoff"``, ``"percentile"``. Returns: - tuple: ``(rgb, surface_reflectance, cutoff, percentile)`` - resolved values, with the grouped form taking precedence. + tuple: ``(rgb, surface_reflectance, cutoff, percentile)`` — each the value from + ``rgb_options`` or ``None`` when absent. Raises: - ValueError: If ``rgb_options`` contains a key outside the - accepted set ``{"rgb", "surface_reflectance", "cutoff", - "percentile"}``. + ValueError: If ``rgb_options`` contains a key outside the accepted set. Examples: - - Pass everything through the grouped form (recommended). - No warnings are emitted and the returned four-tuple - mirrors the inputs in order: + - Unpack the grouped form (``None`` yields an all-``None`` tuple): ```python - >>> import warnings >>> from pyramids.dataset import Dataset - >>> with warnings.catch_warnings(record=True) as caught: - ... warnings.simplefilter("always") - ... result = Dataset._merge_rgb_options( - ... rgb_options={ - ... "rgb": [2, 1, 0], - ... "surface_reflectance": 10000, - ... }, - ... rgb=None, - ... surface_reflectance=None, - ... cutoff=None, - ... percentile=None, - ... ) - >>> result + >>> Dataset._unpack_rgb_options( + ... {"rgb": [2, 1, 0], "surface_reflectance": 10000} + ... ) ([2, 1, 0], 10000, None, None) - >>> [w.category.__name__ for w in caught] - [] - - ``` - - - Passing a value via the loose top-level kwarg path emits - a :class:`DeprecationWarning` that names the kwarg(s) - used and points to the grouped replacement: - - ```python - >>> import warnings - >>> from pyramids.dataset import Dataset - >>> with warnings.catch_warnings(record=True) as caught: - ... warnings.simplefilter("always") - ... result = Dataset._merge_rgb_options( - ... rgb_options=None, - ... rgb=[2, 1, 0], - ... surface_reflectance=None, - ... cutoff=None, - ... percentile=None, - ... ) - >>> result - ([2, 1, 0], None, None, None) - >>> caught[0].category.__name__ - 'DeprecationWarning' - - ``` - - - When both forms collide, ``rgb_options`` wins. A - :class:`DeprecationWarning` is still emitted for the loose - kwarg: - - ```python - >>> import warnings - >>> from pyramids.dataset import Dataset - >>> with warnings.catch_warnings(record=True) as caught: - ... warnings.simplefilter("always") - ... result = Dataset._merge_rgb_options( - ... rgb_options={"rgb": [0, 1, 2]}, - ... rgb=[3, 4, 5], - ... surface_reflectance=None, - ... cutoff=None, - ... percentile=None, - ... ) - >>> result - ([0, 1, 2], None, None, None) + >>> Dataset._unpack_rgb_options(None) + (None, None, None, None) ``` - - An unknown key in ``rgb_options`` raises - :class:`ValueError`: + - An unknown key raises :class:`ValueError`: ```python - >>> from pyramids.dataset import Dataset - >>> Dataset._merge_rgb_options( # doctest: +IGNORE_EXCEPTION_DETAIL - ... rgb_options={"unknown": 1}, - ... rgb=None, - ... surface_reflectance=None, - ... cutoff=None, - ... percentile=None, + >>> Dataset._unpack_rgb_options( # doctest: +IGNORE_EXCEPTION_DETAIL + ... {"unknown": 1} ... ) Traceback (most recent call last): ... @@ -1098,50 +1030,20 @@ def _merge_rgb_options( ``` """ - loose_pairs = { - "rgb": rgb, - "surface_reflectance": surface_reflectance, - "cutoff": cutoff, - "percentile": percentile, - } + accepted = ("rgb", "surface_reflectance", "cutoff", "percentile") opts = rgb_options or {} - unknown = set(opts) - set(loose_pairs) + unknown = set(opts) - set(accepted) if unknown: raise ValueError( f"Unknown keys in `rgb_options`: {sorted(unknown)}. " - f"Accepted: {sorted(loose_pairs)}." - ) - used_loose = [name for name, value in loose_pairs.items() if value is not None] - # Split the deprecated loose kwargs into those overridden by a - # matching `rgb_options` key (a collision — the loose value is - # discarded) and those used on their own. They get distinct - # warning text so the message isn't misleading: a user who *did* - # use the grouped form shouldn't be told "group them" again. - overridden = [name for name in used_loose if name in opts] - pure_loose = [name for name in used_loose if name not in opts] - if pure_loose: - warnings.warn( - "Passing " - + ", ".join(f"`{name}=`" for name in pure_loose) - + " as top-level kwargs to `Dataset.plot` is deprecated. " - "Group them under `rgb_options={...}` instead.", - DeprecationWarning, - stacklevel=3, + f"Accepted: {sorted(accepted)}." ) - if overridden: - warnings.warn( - "Both the loose " - + ", ".join(f"`{name}=`" for name in overridden) - + " kwarg(s) and `rgb_options` were given for the same key(s); " - "`rgb_options` wins — drop the loose form.", - DeprecationWarning, - stacklevel=3, - ) - rgb = opts.get("rgb", rgb) - surface_reflectance = opts.get("surface_reflectance", surface_reflectance) - cutoff = opts.get("cutoff", cutoff) - percentile = opts.get("percentile", percentile) - return rgb, surface_reflectance, cutoff, percentile + return ( + opts.get("rgb"), + opts.get("surface_reflectance"), + opts.get("cutoff"), + opts.get("percentile"), + ) def crop(self, *args, **kwargs): """Facade — delegates to :meth:`Spatial.crop `.""" diff --git a/src/pyramids/dataset/engines/analysis.py b/src/pyramids/dataset/engines/analysis.py index cf21a579bd..3cbc506e78 100644 --- a/src/pyramids/dataset/engines/analysis.py +++ b/src/pyramids/dataset/engines/analysis.py @@ -28,8 +28,8 @@ from pyramids.feature import FeatureCollection if TYPE_CHECKING: - from cleopatra.array_glyph import ArrayGlyph - from cleopatra.geo import Basemap + from cleopatra.basemap.geo import Basemap + from cleopatra.glyphs.gridded.array_glyph import ArrayGlyph from pyramids.dataset.dataset import Dataset @@ -1674,7 +1674,7 @@ def plot_histogram( """Plot the value distribution of a band as a histogram. Backed by cleopatra's - :class:`~cleopatra.statistical_glyph.StatisticalGlyph`. The band is + :class:`~cleopatra.glyphs.stats.histogram_glyph.HistogramGlyph`. The band is read into memory, the band's no-data value and ``exclude_value`` (and any ``NaN`` for floating-point bands) are dropped, and only the remaining valid samples reach the glyph. Requires the ``[viz]`` extra. @@ -1690,15 +1690,15 @@ def plot_histogram( ax (matplotlib.axes.Axes, optional): Axes to draw on. A new figure/axes is created when ``None``. **kwargs: - Style options forwarded to the ``StatisticalGlyph`` + Style options forwarded to the ``HistogramGlyph`` constructor, filtered via - :meth:`StatisticalGlyph.filter_kwargs` so only accepted keys + :meth:`HistogramGlyph.filter_kwargs` so only accepted keys are passed. Returns: tuple: ``(fig, ax, hist)`` from - :meth:`StatisticalGlyph.histogram` — the + :meth:`HistogramGlyph.histogram` — the :class:`matplotlib.figure.Figure`, the :class:`matplotlib.axes.Axes`, and the histogram ``dict``. @@ -1729,7 +1729,7 @@ def plot_histogram( ``` """ require_cleopatra() - from cleopatra.statistical_glyph import StatisticalGlyph + from cleopatra.glyphs.stats.histogram_glyph import HistogramGlyph arr = self._ds.read_array(band=band).flatten() no_data_value = self._ds.no_data_value[band] @@ -1748,9 +1748,7 @@ def plot_histogram( f"Band {band} has no valid samples to histogram after masking " "no-data / exclude_value / NaN." ) - glyph = StatisticalGlyph( - values, ax=ax, **StatisticalGlyph.filter_kwargs(kwargs) - ) + glyph = HistogramGlyph(values, ax=ax, **HistogramGlyph.filter_kwargs(kwargs)) result = glyph.histogram(bins=bins) return result @@ -1804,7 +1802,7 @@ def to_image( ``` """ require_cleopatra() - from cleopatra.array_glyph import ArrayGlyph + from cleopatra.glyphs.gridded.array_glyph import ArrayGlyph arr = self._ds.read_array(band=band) no_data_value = self._ds.no_data_value[band] @@ -1842,7 +1840,7 @@ def plot_vector_field( Reads ``u_band`` and ``v_band`` as the vector components over the dataset's cell-centre coordinate grid (built from the geotransform) and renders them via cleopatra's - :class:`~cleopatra.vector_glyph.VectorGlyph` as arrows, wind barbs, + :class:`~cleopatra.glyphs.gridded.vector_glyph.VectorGlyph` as arrows, wind barbs, or streamlines, coloured by vector magnitude. Requires the ``[viz]`` extra. @@ -1905,7 +1903,7 @@ def plot_vector_field( ``` """ require_cleopatra() - from cleopatra.vector_glyph import VectorGlyph + from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph band_count = self._ds.band_count for name, idx in (("u_band", u_band), ("v_band", v_band)): @@ -1959,13 +1957,14 @@ def plot( are dataset-type-specific decisions that belong on the facades. When the resolved band carries a GDAL colour table and the caller passes neither - ``cmap`` nor ``color_scale``, the raster renders through that palette: the colour - table is turned into a colormap and handed to cleopatra with - ``color_scale="boundary-norm"`` so each pixel value shows its own colour (#913). - An explicit ``cmap`` / ``color_scale`` opts out. + ``cmap`` nor ``color``, the raster renders through that palette: the colour table is + turned into a colormap and handed to cleopatra with + ``color=ColorScaling.boundary(bounds=...)`` so each pixel value shows its own colour + (#913). An explicit ``cmap`` / ``color`` opts out. - The plot function uses the `cleopatra` as a backend to plot the raster data, for more information check - [ArrayGlyph](https://serapeum-org.github.io/cleopatra/latest/api/array-glyph-class/#cleopatra.array_glyph.ArrayGlyph.plot). + The plot function uses `cleopatra` as a backend to plot the raster data; for more + information see the + [ArrayGlyph reference](https://serapeum-org.github.io/cleopatra/latest/api/array-glyph-class/). Implementation note: this method is a thin caller around the shared :func:`pyramids.dataset._plot_helpers.render_array` @@ -2005,39 +2004,37 @@ def plot( basemap (bool, str, or Basemap, optional): Reference layer under the plot, dispatched by type. ``True`` or a tile-provider string (e.g. "CartoDB.Positron") draws a pyramids web-tile basemap. A - ``pyramids.plot.Basemap(relief=..., features=...)`` (cleopatra >= 0.28) draws a + ``pyramids.plot.Basemap(relief=..., features=...)`` draws a shaded-relief / coastline reference layer instead. Default is None (no basemap). Requires the [viz] extra (mercantile, xyzservices, Pillow). A ``Basemap`` is not supported on the faceted path. kwargs: - The typed specs below (``ColorBar`` / ``PointOverlay`` / ``Basemap``) are - re-exported from ``pyramids.plot`` — import them from there rather than cleopatra. - Configure the colour bar through ``colorbar=ColorBar(label=..., length=..., - orientation=..., label_size=..., label_rotation=..., label_location=..., - ticks_spacing=...)``. The loose ``cbar_*`` / ``ticks_spacing`` kwargs it replaces - are deprecated: pyramids still accepts them but folds them into a ``ColorBar`` - and emits a ``DeprecationWarning``, so they are no longer listed below. The - same applies to ``point_*`` -> ``PointOverlay`` and a ``dict`` basemap -> - ``Basemap``. Migrate to the typed specs. - | Parameter | Type | Description | - |-----------------------------|---------------------|-------------| - | `points` | array \\| PointOverlay | Point overlay. A 3-column array (value to display, row index, column index) draws unstyled points. To style them, pass a `pyramids.plot.PointOverlay(points, color=..., size=..., label_color=..., label_size=...)` instead — pyramids folds the loose `point_color` / `point_size` / `point_label_color` / `point_label_size` / `pid_color` / `pid_size` kwargs into a `PointOverlay` and emits a `DeprecationWarning`; set the styling on the `PointOverlay` instead. | - | `figsize` | tuple, optional | Figure size. Default is `(8, 8)`. | - | `title` | str, optional | Title of the plot. Default is `'Total Discharge'`. | - | `title_size` | int, optional | Title size. Default is `15`. | - | `color_scale` | str, optional | Color-scale mode. One of `"linear"`, `"power"`, `"sym-lognorm"`, `"boundary-norm"`, `"midpoint"` (case-insensitive), or a `cleopatra.styles.ColorScale` member. Integer codes are no longer accepted. Default is `"linear"`. | - | `gamma` | float, optional | Exponent for the `"power"` color scale. Default is `1/2`. | - | `line_threshold` | float, optional | `linthresh` for the `"sym-lognorm"` color scale. Default is `0.0001`. | - | `line_scale` | float, optional | `linscale` for the `"sym-lognorm"` color scale. Default is `0.001`. | - | `bounds` | list, optional | Discrete bounds for the `"boundary-norm"` color scale. Default is `None`. | - | `midpoint` | float, optional | Midpoint value for the `"midpoint"` color scale. Default is `0`. | - | `cmap` | str, optional | Color map style. Default is `'coolwarm_r'`. | - | `display_cell_value` | bool, optional | Whether to display cell values as text. | - | `num_size` | int, optional | Size of numbers plotted on top of each cell. Default is `8`. | - | `background_color_threshold`| float or int, optional | Threshold for deciding text color over cells: if value > threshold -> black text; else white text. If `None`, max value / 2 is used. Default is `None`. | - | `add_colorbar` | bool, optional | Whether to draw the colour bar. Default is `True`. When `False`, no colorbar is created and the returned glyph's `cbar` is `None`. | - | `colorbar` | bool \\| ColorBar, optional | Colour-bar spec `pyramids.plot.ColorBar(label=…, length=…, orientation=…, label_size=…, label_rotation=…, label_location=…, ticks_spacing=…)` (cleopatra >= 0.28) — the complete, preferred replacement for the loose `cbar_*` / `ticks_spacing` kwargs. `False` hides it, `None` uses the default. | - | `full_bleed` | bool \\| str, optional | cleopatra >= 0.28 chrome-free layout: drop axes/margins so the array fills the figure. Default `False`. | + Colour-scale, contour, cell-value and data-style options moved onto typed + render groups (all re-exported from ``pyramids.plot``): pass + ``color=ColorScaling(...)`` / ``contour=Contour(...)`` / + ``cells=CellValues(...)`` / ``data_style=DataStyle(...)``, the colour bar as + ``colorbar=ColorBar(...)``, and point overlays as ``points=PointOverlay(...)``. + The loose forms they replace — ``color_scale`` / ``gamma`` / ``bounds`` / + ``midpoint`` / ``line_*`` / ``levels`` / ``display_cell_value`` / ``num_size`` / + ``background_color_threshold`` / ``style`` / ``hillshade`` / ``point_*`` / + ``cbar_*`` / ``ticks_spacing`` — are no longer accepted and now raise. The + remaining still-loose kwargs pass through to cleopatra: + + - `points` (array | PointOverlay): Point overlay. A bare 3-column array + `(value, row, col)` draws unstyled points; pass a + `pyramids.plot.PointOverlay(points, color=..., size=..., ...)` to style them. + - `cmap` (str, optional): Color map style. Default is `'coolwarm_r'`. + - `figsize` (tuple, optional): Figure size. Default is `(8, 8)`. + - `title` (str, optional): Title of the plot. Default is `'Total Discharge'`. + - `title_size` (int, optional): Title size. Default is `15`. + - `add_colorbar` (bool, optional): Whether to draw the colour bar. Default is + `True`; when `False` the returned glyph's `cbar` is `None`. + - `colorbar` (bool | ColorBar, optional): Colour-bar spec + `pyramids.plot.ColorBar(label=..., orientation=..., ...)` — replaces the + removed loose `cbar_*` / `ticks_spacing` kwargs. `False` hides it, `None` + uses the default. + - `full_bleed` (bool | str, optional): Chrome-free layout: drop axes/margins + so the array fills the figure. Default `False`. Returns: ArrayGlyph: A cleopatra ``ArrayGlyph`` wrapping the rendered figure. The underlying matplotlib @@ -2068,27 +2065,28 @@ def plot( (
, ) ``` - - plot using power scale. + - plot using a power scale. ```python - >>> dataset.plot(band=0, color_scale="power") # doctest: +SKIP + >>> from pyramids.plot import ColorScaling # doctest: +SKIP + >>> dataset.plot(band=0, color=ColorScaling.power(gamma=0.7)) # doctest: +SKIP (
, ) ``` - - plot using SymLogNorm scale. + - plot using a SymLogNorm scale. ```python - >>> dataset.plot(band=0, color_scale="sym-lognorm") # doctest: +SKIP + >>> dataset.plot(band=0, color=ColorScaling.sym_log()) # doctest: +SKIP (
, ) ``` - - plot using PowerNorm scale. + - plot using a BoundaryNorm scale. ```python - >>> dataset.plot(band=0, color_scale="boundary-norm", bounds=[0, 0.2, 0.4, 0.6, 0.8, 1]) # doctest: +SKIP + >>> dataset.plot(band=0, color=ColorScaling.boundary(bounds=[0, 0.2, 0.4, 0.6, 0.8, 1])) # doctest: +SKIP (
, ) ``` - - plot using BoundaryNorm scale. + - plot using a midpoint scale. ```python - >>> dataset.plot(band=0, color_scale="midpoint") # doctest: +SKIP + >>> dataset.plot(band=0, color=ColorScaling.midpoint(at=0)) # doctest: +SKIP (
, ) ``` @@ -2128,26 +2126,27 @@ def plot( effective_extent = ( injected_extent if injected_extent is not None else self._ds.bbox ) - # Render a paletted band through its GDAL colour table (#913): build a - # discrete colormap from the palette and hand it to cleopatra as an - # explicit ``cmap`` + ``color_scale="boundary-norm"``. Only the single-band - # static path (no ``rgb``, no facet) carries a palette, and an explicit - # ``cmap`` / ``color_scale`` / ``bounds`` from the caller wins over it. + # Render a paletted band through its GDAL colour table (#913): build a discrete + # colormap from the palette and hand it to cleopatra as an explicit ``cmap`` plus a + # boundary-norm ``color=ColorScaling.boundary(bounds=...)`` (cleopatra 0.30 moved the + # colour scale onto the typed group). Only the single-band static path (no ``rgb``, + # no facet) carries a palette, and an explicit ``cmap`` / ``color`` from the caller + # wins over it. if ( mode == "plot" and rgb is None and kwargs.get("cmap") is None - and kwargs.get("color_scale") is None - and kwargs.get("bounds") is None + and kwargs.get("color") is None ): # Read only the resolved band's colour table, not every band's — the # full-dataset ``color_table`` rebuilds a row per entry for all bands. band_color_table = self._ds.bands._get_color_table(band=band) if not band_color_table.empty: + from cleopatra.styling.scaling import ColorScaling + cmap, bounds = self._palette_colormap(band_color_table) kwargs["cmap"] = cmap - kwargs["color_scale"] = "boundary-norm" - kwargs["bounds"] = bounds + kwargs["color"] = ColorScaling.boundary(bounds=bounds) return render_array( arr=arr, extent=effective_extent, @@ -2224,7 +2223,7 @@ def _read_plot_lazy_array(self, band: int, chunks: Any) -> np.ndarray: @staticmethod def _process_color_table(color_table: DataFrame) -> DataFrame: require_cleopatra() - from cleopatra.colors import Colors + from cleopatra.styling.colors import Colors # if the color_table does not contain the red, green, and blue columns, assume it has one column with # the color as hex and then, convert the color to rgb. @@ -2255,8 +2254,8 @@ def _palette_colormap(color_table: DataFrame) -> tuple[Any, list[float]]: :meth:`_process_color_table`) into a matplotlib ``ListedColormap`` carrying the palette's exact colours, and derives the ``BoundaryNorm`` bin edges from cleopatra's ``category_boundaries``. The colormap is handed to cleopatra as an - explicit ``cmap`` with ``color_scale="boundary-norm"`` so a paletted raster - renders through its own colours — pyramids only builds the mapping; cleopatra + explicit ``cmap`` with ``color=ColorScaling.boundary(bounds=...)`` so a paletted + raster renders through its own colours — pyramids only builds the mapping; cleopatra draws it. cleopatra renders with ``BoundaryNorm(bounds, ncolors=256)``, so the palette is @@ -2284,7 +2283,7 @@ def _palette_colormap(color_table: DataFrame) -> tuple[Any, list[float]]: boundary edges, sorted by colour-table value. """ require_cleopatra() - from cleopatra.colors import category_boundaries + from cleopatra.styling.colors import category_boundaries from matplotlib.colors import BoundaryNorm, ListedColormap processed = Analysis._process_color_table(color_table).sort_values("values") diff --git a/src/pyramids/dataset/engines/bands.py b/src/pyramids/dataset/engines/bands.py index ee36872aa3..52f6448151 100644 --- a/src/pyramids/dataset/engines/bands.py +++ b/src/pyramids/dataset/engines/bands.py @@ -923,11 +923,17 @@ def set_color_ramp( ) require_cleopatra() - from cleopatra.colors import Colors + from cleopatra.styling.colors import Colors if colormap: ramp = self._ramp_from_colormap(colormap, start_value, end_value) else: + # ``_validate_color_ramp_args`` guarantees the colour pair is present when + # ``colormap`` is unset (exactly one of the pair / colormap is given), but mypy + # can't follow that cross-method narrowing — now that the mypy override reads + # cleopatra's inline types, ``Colors`` needs concrete ``str`` colours. + assert start_color is not None + assert end_color is not None start_rgb, end_rgb = Colors([start_color, end_color]).to_rgb( normalized=False ) @@ -1080,7 +1086,7 @@ def _set_color_table(self, color_df: DataFrame, overwrite: bool = False) -> None True to overwrite the existing color table. Default is False. """ require_cleopatra() - from cleopatra.colors import Colors + from cleopatra.styling.colors import Colors color = Colors(color_df["color"].tolist()) color_rgb = color.to_rgb(normalized=False) diff --git a/src/pyramids/dataset/engines/vectorize.py b/src/pyramids/dataset/engines/vectorize.py index ed70d00ab4..09bcbd15bb 100644 --- a/src/pyramids/dataset/engines/vectorize.py +++ b/src/pyramids/dataset/engines/vectorize.py @@ -884,10 +884,11 @@ def cluster( >>> dataset = Dataset.create_from_array( ... arr, top_left_corner=top_left_corner, cell_size=cell_size, epsg=4326 ... ) - >>> dataset.plot( - ... color_scale="boundary-norm", bounds=[1, 1.9, 4.1, 5], display_cell_value=True, num_size=12, - ... background_color_threshold=5 - ... ) # doctest: +SKIP + >>> from pyramids.plot import ColorScaling, CellValues # doctest: +SKIP + >>> dataset.plot( # doctest: +SKIP + ... color=ColorScaling.boundary(bounds=[1, 1.9, 4.1, 5]), + ... cells=CellValues(show=True, size=12, background_threshold=5), + ... ) ``` ![cluster](./../../_images/dataset/cluster.png) diff --git a/src/pyramids/feature/_plot.py b/src/pyramids/feature/_plot.py index f434c55649..2460187e0f 100644 --- a/src/pyramids/feature/_plot.py +++ b/src/pyramids/feature/_plot.py @@ -26,18 +26,47 @@ def plot( column: str | None = None, basemap: Any = None, engine: str = "geopandas", + colorbar: Any = None, + title: str | None = None, + color: Any = None, + contour: Any = None, + classify: Any = None, **kwargs: Any, ) -> Any: - """Render `fc` via geopandas or cleopatra, optionally over a web-tile basemap.""" + """Render `fc` via geopandas or cleopatra, optionally over a web-tile basemap. + + ``colorbar`` / ``title`` are the raster-family plot params that map onto both vector + back-ends: on geopandas ``colorbar`` toggles the ``legend`` and ``title`` is set on the + returned Axes; on cleopatra both forward to the glyph's ``plot`` call. The typed render + groups ``color`` / ``contour`` / ``classify`` are cleopatra-glyph concepts with no + geopandas equivalent, so they forward to the glyph on ``engine="cleopatra"`` and are + ignored on ``engine="geopandas"``. Each is only applied when set. + """ if engine == "geopandas": # geopandas' `.plot` is a CachedAccessor: `GeoDataFrame.plot` is the accessor # class, so construct it bound to `fc` and then call it. This reaches geopandas' # implementation while bypassing FeatureCollection's `plot` facade override — # equivalent to the original in-class `super().plot(...)`. - result = gpd.GeoDataFrame.plot(fc)(column=column, **kwargs) + gp_kwargs = dict(kwargs) + if colorbar is not None: + # geopandas has no ColorBar object; its `legend` draws the bar, so a + # ColorBar spec (or True) shows it and False hides it. + gp_kwargs["legend"] = bool(colorbar) + result = gpd.GeoDataFrame.plot(fc)(column=column, **gp_kwargs) ax = result + if title is not None: + ax.set_title(title) elif engine == "cleopatra": - result, ax = plot_cleopatra(fc, column=column, **kwargs) + result, ax = plot_cleopatra( + fc, + column=column, + colorbar=colorbar, + title=title, + color=color, + contour=contour, + classify=classify, + **kwargs, + ) else: raise ValueError( f"Unsupported engine {engine!r}; choose 'geopandas' or 'cleopatra'." @@ -50,8 +79,23 @@ def plot( return result -def plot_cleopatra(fc: Any, column: str | None = None, **kwargs: Any) -> Any: - """Build and draw the cleopatra glyph for `fc`, returning ``(glyph, ax)``.""" +def plot_cleopatra( + fc: Any, + column: str | None = None, + colorbar: Any = None, + title: str | None = None, + color: Any = None, + contour: Any = None, + classify: Any = None, + **kwargs: Any, +) -> Any: + """Build and draw the cleopatra glyph for `fc`, returning ``(glyph, ax)``. + + ``colorbar`` / ``title`` and the typed render groups ``color`` / ``contour`` / + ``classify`` are forwarded to the glyph's ``plot`` call (both ``ScatterGlyph.plot`` and + ``PolygonGlyph.plot`` accept them), each only when set so the glyph default is + preserved otherwise. + """ require_cleopatra() if column is not None and column not in fc.columns: raise ValueError( @@ -74,14 +118,25 @@ def plot_cleopatra(fc: Any, column: str | None = None, **kwargs: Any) -> Any: "engine='cleopatra' supports single Point or Polygon/MultiPolygon geometries; got " f"{sorted(geom_types, key=str)} (MultiPoint is not supported)." ) - _fig, ax, _coll = glyph.plot() + plot_call: dict[str, Any] = {} + if colorbar is not None: + plot_call["colorbar"] = colorbar + if title is not None: + plot_call["title"] = title + if color is not None: + plot_call["color"] = color + if contour is not None: + plot_call["contour"] = contour + if classify is not None: + plot_call["classify"] = classify + _fig, ax, _coll = glyph.plot(**plot_call) return glyph, ax def scatter_glyph(fc: Any, values: Any, **kwargs: Any) -> Any: """Build a cleopatra ScatterGlyph from `fc`'s point coordinates.""" require_cleopatra() - from cleopatra.scatter_glyph import ScatterGlyph + from cleopatra.glyphs.primitives.scatter_glyph import ScatterGlyph return ScatterGlyph( fc.geometry.x.to_numpy(), @@ -94,7 +149,7 @@ def scatter_glyph(fc: Any, values: Any, **kwargs: Any) -> Any: def polygon_glyph(fc: Any, values: Any, **kwargs: Any) -> Any: """Build a cleopatra PolygonGlyph from `fc`'s polygon exterior rings.""" require_cleopatra() - from cleopatra.polygon_glyph import PolygonGlyph + from cleopatra.glyphs.primitives.polygon_glyph import PolygonGlyph polygons: list = [] poly_values: list | None = [] if values is not None else None diff --git a/src/pyramids/feature/collection.py b/src/pyramids/feature/collection.py index be019788d7..aac86f3e5c 100644 --- a/src/pyramids/feature/collection.py +++ b/src/pyramids/feature/collection.py @@ -30,6 +30,11 @@ from typing import TYPE_CHECKING, Any, Literal, cast if TYPE_CHECKING: + from cleopatra.glyphs.gridded.array_glyph import PointOverlay + from cleopatra.styling.colorbar import ColorBar + from cleopatra.styling.params import Classify, Contour + from cleopatra.styling.scaling import ColorScaling + from pyramids.dataset import Dataset from pyramids.feature._lazy_collection import LazyFeatureCollection @@ -2190,6 +2195,13 @@ def plot( column: str | None = None, basemap: bool | str | None = None, engine: str = "geopandas", + colorbar: bool | ColorBar | None = None, + points: np.ndarray | PointOverlay | None = None, + kind: str = "auto", + title: str | None = None, + color: ColorScaling | None = None, + contour: Contour | None = None, + classify: Classify | None = None, **kwargs: Any, ) -> Any: """Plot features, optionally on a web-tile basemap. @@ -2200,8 +2212,8 @@ def plot( :meth:`geopandas.GeoDataFrame.plot` and return the matplotlib ``Axes``. This is the long-standing behaviour and is unchanged. - ``"cleopatra"``: render polygons through - :class:`~cleopatra.polygon_glyph.PolygonGlyph` or points through - :class:`~cleopatra.scatter_glyph.ScatterGlyph` — sharing the + :class:`~cleopatra.glyphs.primitives.polygon_glyph.PolygonGlyph` or points through + :class:`~cleopatra.glyphs.primitives.scatter_glyph.ScatterGlyph` — sharing the colour/colorbar styling of the raster glyph path — and return the cleopatra glyph. Requires the ``[viz]`` extra. @@ -2213,6 +2225,33 @@ def plot( renders a single flat colour. basemap: ``True`` for OpenStreetMap, or a provider name string. engine: ``"geopandas"`` (default) or ``"cleopatra"``. + colorbar (bool or ColorBar, optional): Colour-bar spec, part of the + shared plot signature. On ``engine="geopandas"`` it toggles the + geopandas ``legend`` (a ``ColorBar`` object counts as "show"); on + ``engine="cleopatra"`` a ``pyramids.plot.ColorBar(label=…, …)`` styles + the glyph's bar. ``None`` (default) leaves each back-end's default. + points (np.ndarray or PointOverlay, optional): Accepted for signature + symmetry with the raster plot family, but a **no-op here** — a vector + layer has no separate point overlay (the geometry is the data). + Ignored. + kind (str, optional): Accepted for signature symmetry with the raster + plot family, but a **no-op here** — ``kind`` selects a raster renderer + (``imshow``/``pcolormesh``), which has no vector equivalent. Ignored. + title (str, optional): Axes/glyph title. Set on the returned Axes + (``engine="geopandas"``) or forwarded to the glyph + (``engine="cleopatra"``). Default ``None``. + color (ColorScaling, optional): Typed colour-scale spec + ``pyramids.plot.ColorScaling``, forwarded to the glyph on + ``engine="cleopatra"``. **Ignored on ``engine="geopandas"``** (no + geopandas equivalent). Default ``None``. + contour (Contour, optional): Typed contour-line spec + ``pyramids.plot.Contour``, forwarded to the glyph on + ``engine="cleopatra"``. Ignored on ``engine="geopandas"``. Default + ``None``. + classify (Classify, optional): Typed value-classification spec + ``pyramids.plot.Classify`` (``scheme`` / ``k``), forwarded to the glyph on + ``engine="cleopatra"``. Ignored on ``engine="geopandas"``. Default + ``None``. **kwargs: Forwarded to the chosen back-end. For ``"cleopatra"`` they are filtered to the glyph's accepted options via ``filter_kwargs``. @@ -2254,7 +2293,23 @@ def plot( ``` """ - return _plot.plot(self, column=column, basemap=basemap, engine=engine, **kwargs) + # ``points`` / ``kind`` are part of the shared raster-family plot signature + # but have no meaning for vector geometry, so they are accepted and ignored. + # ``colorbar`` / ``title`` map onto both back-ends; ``color`` / ``contour`` / + # ``classify`` are cleopatra-glyph groups forwarded on that engine (and ignored on + # geopandas). All are forwarded and applied only when set. + return _plot.plot( + self, + column=column, + basemap=basemap, + engine=engine, + colorbar=colorbar, + title=title, + color=color, + contour=contour, + classify=classify, + **kwargs, + ) def _plot_cleopatra(self, column: str | None = None, **kwargs: Any): """Render via cleopatra ``PolygonGlyph``/``ScatterGlyph``. @@ -2293,7 +2348,7 @@ def _cleopatra_scatter_glyph(self, values: Any, **kwargs: Any) -> Any: **kwargs: Style options, filtered to the glyph's accepted keys. Returns: - cleopatra.scatter_glyph.ScatterGlyph: The point glyph. + cleopatra.glyphs.primitives.scatter_glyph.ScatterGlyph: The point glyph. """ return _plot.scatter_glyph(self, values, **kwargs) @@ -2311,7 +2366,7 @@ def _cleopatra_polygon_glyph(self, values: Any, **kwargs: Any) -> Any: **kwargs: Style options, filtered to the glyph's accepted keys. Returns: - cleopatra.polygon_glyph.PolygonGlyph: The polygon glyph. + cleopatra.glyphs.primitives.polygon_glyph.PolygonGlyph: The polygon glyph. """ return _plot.polygon_glyph(self, values, **kwargs) diff --git a/src/pyramids/netcdf/_plot.py b/src/pyramids/netcdf/_plot.py index 5b4a072b41..da469cdef7 100644 --- a/src/pyramids/netcdf/_plot.py +++ b/src/pyramids/netcdf/_plot.py @@ -20,7 +20,7 @@ from pyramids.netcdf.plot_options import ColorOpts, FacetSpec, Selectors if TYPE_CHECKING: - from cleopatra.geo import Basemap + from cleopatra.basemap.geo import Basemap from pyramids.netcdf.netcdf import NetCDF @@ -106,7 +106,6 @@ def _reject_forbidden_kwargs(kwargs: dict[str, Any]) -> None: "extend", "cbar_kwargs", "aspect", - "levels", "center", "norm", "robust", @@ -501,13 +500,10 @@ def _build_render_kwargs( ("cmap", colour.cmap), ("vmin", colour.vmin), ("vmax", colour.vmax), - ("levels", colour.levels), ("norm", colour.norm), ("center", colour.center), ("extend", colour.extend), ("cbar_kwargs", colour.cbar_kwargs), - ("style", colour.style), - ("hillshade", colour.hillshade), ("ax", ax), ("figsize", figsize), ("title", title), @@ -516,6 +512,26 @@ def _build_render_kwargs( out[key] = value if colour.robust: out["robust"] = True + # cleopatra 0.30 moved ``levels`` / ``style`` / ``hillshade`` off the loose kwargs + # onto the typed render groups, so build them from the ColorOpts bag here (a no-op + # ``style=None`` / falsy ``hillshade`` is dropped, matching the render-group rules). + # The cleopatra import is deferred to the branch that actually builds a group, so a + # plain NetCDF plot (no levels/style/hillshade) never imports it. An explicit hoisted + # ``contour=`` / ``data_style=`` (already in ``out`` via ``base_kwargs``) wins — the + # ColorOpts-derived group only fills the slot when the caller left it unset. + if colour.levels is not None and "contour" not in out: + from cleopatra.styling.params import Contour + + out["contour"] = Contour(levels=colour.levels) + data_style_fields: dict[str, Any] = {} + if colour.style is not None: + data_style_fields["style"] = colour.style + if colour.hillshade is not None and colour.hillshade is not False: + data_style_fields["hillshade"] = colour.hillshade + if data_style_fields and "data_style" not in out: + from cleopatra.styling.params import DataStyle + + out["data_style"] = DataStyle(**data_style_fields) # Curvilinear coord resolution. Priority (highest first): # 1. Explicit user `coords=`. @@ -544,8 +560,8 @@ def _remove_colorbar(result: Any) -> None: Honours the ``add_colorbar=False`` switch on :meth:`NetCDF.plot`. Cleopatra always attaches a colorbar to its - :class:`~cleopatra.array_glyph.ArrayGlyph` / - :class:`~cleopatra.array_glyph.FacetGrid` results, so pyramids + :class:`~cleopatra.glyphs.gridded.array_glyph.ArrayGlyph` / + :class:`~cleopatra.glyphs.gridded.array_glyph.FacetGrid` results, so pyramids applies the removal here after the render returns. The helper is defensive — it leaves ``result`` untouched when no ``.cbar`` attribute exists, when the attribute is already @@ -706,7 +722,7 @@ def _build_facet_stack( Returns: tuple: ``(stack, facet_kwargs)`` — the materialised array and the kwargs dict to forward to - :meth:`cleopatra.array_glyph.ArrayGlyph.facet`. + :meth:`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph.facet`. Examples: - Build a 3-D stack from a 3-D variable's single time dim @@ -987,7 +1003,7 @@ def _render_animate( call. Returns: - cleopatra.array_glyph.ArrayGlyph: The cleopatra glyph + cleopatra.glyphs.gridded.array_glyph.ArrayGlyph: The cleopatra glyph wrapping the streamed ``FuncAnimation``. The matplotlib animation object is reachable via the glyph's matplotlib figure. diff --git a/src/pyramids/netcdf/netcdf.py b/src/pyramids/netcdf/netcdf.py index 2e0a092475..b805846216 100644 --- a/src/pyramids/netcdf/netcdf.py +++ b/src/pyramids/netcdf/netcdf.py @@ -34,6 +34,7 @@ from pyramids.base.protocols import ArrayLike from pyramids.base.remote import is_remote from pyramids.dataset import Dataset +from pyramids.dataset._plot_helpers import nonnull_group_kwargs from pyramids.dataset.dataset import _COLLABORATOR_ATTRS from pyramids.feature import FeatureCollection from pyramids.netcdf._axis import detect_axis_indices @@ -71,7 +72,11 @@ from pyramids.netcdf.utils import _read_attributes, create_time_conversion_func if TYPE_CHECKING: - from cleopatra.geo import Basemap + from cleopatra.basemap.geo import Basemap + from cleopatra.glyphs.gridded.array_glyph import PointOverlay + from cleopatra.styling.colorbar import ColorBar + from cleopatra.styling.params import CellValues, Contour, DataStyle + from cleopatra.styling.scaling import ColorScaling # Guards the per-container `_lazy_managers` WeakSet against a concurrent lazy `read_array` (which adds) # and `close()` (which snapshots) on the same container from different threads. @@ -1650,6 +1655,12 @@ def plot( # type: ignore[override] title: str | None = None, ax: Any | None = None, figsize: tuple[float, float] | None = None, + colorbar: bool | ColorBar | None = None, + points: np.ndarray | PointOverlay | None = None, + color: ColorScaling | None = None, + contour: Contour | None = None, + cells: CellValues | None = None, + data_style: DataStyle | None = None, **kwargs: Any, ): """Plot a 2-D slice of a NetCDF variable using the plot vocabulary. @@ -1750,7 +1761,7 @@ def plot( # type: ignore[override] basemap (bool, str, or Basemap, optional): Reference layer, dispatched by type. ``True`` or a named basemap tile provider overlays a pyramids web-tile basemap; a - ``pyramids.plot.Basemap`` (cleopatra >= 0.28) draws a relief/features + ``pyramids.plot.Basemap`` draws a relief/features layer instead (not supported on the faceted path). Defaults to None. exclude_value (Any, optional): Pixel value to mask out before plotting. Defaults to None. @@ -1760,6 +1771,30 @@ def plot( # type: ignore[override] Existing matplotlib Axes to draw into. Defaults to None. figsize (tuple, optional): Figure size in inches. Defaults to None. + colorbar (bool or ColorBar, optional): + Colour-bar spec. A ``pyramids.plot.ColorBar(label=…, …)`` + draws a configured bar; ``False`` hides it and + ``None`` uses cleopatra's default. Only forwarded when set. Default + is ``None``. + points (np.ndarray or PointOverlay, optional): + Point overlay for the static / animate paths. A 3-column array + ``(value, row, col)`` draws unstyled points; pass a + ``pyramids.plot.PointOverlay(...)`` to style them. Only forwarded + when set — the multi-panel ``facet`` path has no ``points`` + parameter and raises if one is supplied. Default is ``None``. + color (ColorScaling, optional): + Colour-scale spec ``pyramids.plot.ColorScaling`` (linear / power / sym-log / + boundary / midpoint norm), e.g. ``ColorScaling.power(gamma=0.7)``. Default + ``None``. + contour (Contour, optional): + Contour-line spec ``pyramids.plot.Contour(levels=…, labels=…, label_kw=…)``. + Default ``None``. + cells (CellValues, optional): + Per-cell value annotation ``pyramids.plot.CellValues(show=…, size=…, + background_threshold=…)``. Default ``None``. + data_style (DataStyle, optional): + Data-style / relief spec ``pyramids.plot.DataStyle(style=…, hillshade=…)``. + Default ``None``. **kwargs: Additional keyword arguments forwarded to :meth:`Analysis.plot `. @@ -1774,14 +1809,14 @@ def plot( # type: ignore[override] passing the CRS the data was plotted in (its own CRS — no reprojection needed) so the layer lines up:: - from cleopatra.reference import add_features + from cleopatra.basemap.reference import add_features var = nc.get_variable("t2m") glyph = var.plot() add_features(glyph.ax, "coastline", crs=var.epsg, zorder=5) ``add_features`` fetches Natural Earth data (cached under ``~/.cleopatra``), so it needs the ``[viz]`` extra and network access on first use. A relief backdrop is - available the same way via :func:`cleopatra.reference.add_relief`. + available the same way via :func:`cleopatra.basemap.reference.add_relief`. Raises: TypeError: If any of the Sentinel-only kwargs (``rgb``, @@ -2009,7 +2044,7 @@ def plot( # type: ignore[override] - Facet over the time dim. :class:`FacetSpec` lists the column dim (and optionally a row dim and a wrap value). The return type becomes - :class:`cleopatra.array_glyph.FacetGrid`: + :class:`cleopatra.glyphs.gridded.array_glyph.FacetGrid`: ```python >>> from pyramids.netcdf import FacetSpec @@ -2116,6 +2151,22 @@ def plot( # type: ignore[override] ``` """ + # ``colorbar``/``points`` and the typed render groups (``color`` / ``contour`` / + # ``cells`` / ``data_style``) are hoisted to explicit params for parity with + # ``Dataset.plot`` / ``DatasetCollection.plot``; forward them through the same + # ``**kwargs`` channel the multi-mode render path already consumes. Only inject + # when set — the facet path rejects ``points`` outright, so a default ``None`` + # must not reach it, and an unset group must not override cleopatra's backend + # default for that group. + if colorbar is not None: + kwargs["colorbar"] = colorbar + if points is not None: + kwargs["points"] = points + kwargs.update( + nonnull_group_kwargs( + color=color, contour=contour, cells=cells, data_style=data_style + ) + ) return NetCDFPlot(self).run( variable, selectors=selectors, diff --git a/src/pyramids/netcdf/plot_options.py b/src/pyramids/netcdf/plot_options.py index 361c3f9168..a41743c42c 100644 --- a/src/pyramids/netcdf/plot_options.py +++ b/src/pyramids/netcdf/plot_options.py @@ -7,10 +7,10 @@ variable down to a single 2-D slice (or to the residual stack the facet / animate paths walk). - :class:`ColorOpts` — colour controls forwarded - verbatim to cleopatra's :class:`~cleopatra.array_glyph.ArrayGlyph` + verbatim to cleopatra's :class:`~cleopatra.glyphs.gridded.array_glyph.ArrayGlyph` constructor. - :class:`FacetSpec` — multi-panel facet layout description forwarded - to :meth:`cleopatra.array_glyph.ArrayGlyph.facet`. + to :meth:`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph.facet`. Each dataclass is :func:`~dataclasses.dataclass` with ``frozen=True`` so callers cannot mutate the option bag once it has been handed to @@ -121,7 +121,7 @@ class ColorOpts: Mirrors common plotting colour kwargs. All fields are optional. Non-``None`` values are forwarded verbatim to - cleopatra's :class:`~cleopatra.array_glyph.ArrayGlyph`; the + cleopatra's :class:`~cleopatra.glyphs.gridded.array_glyph.ArrayGlyph`; the ``add_colorbar`` switch is applied post-render on the pyramids side because cleopatra does not accept the kwarg today. @@ -144,9 +144,9 @@ class ColorOpts: cbar_kwargs: Extra dict forwarded to :meth:`Figure.colorbar`. Defaults to None. style: Name of a cleopatra data-style preset (a key of - ``cleopatra.array_glyph.DATA_STYLES`` — e.g. ``"flow_accumulation"``, + ``cleopatra.glyphs.gridded.array_glyph.DATA_STYLES`` — e.g. ``"flow_accumulation"``, ``"topography"``) to colour the variable by. Forwarded to - :class:`~cleopatra.array_glyph.ArrayGlyph`; requires + :class:`~cleopatra.glyphs.gridded.array_glyph.ArrayGlyph`; requires cleopatra >= 0.24. Defaults to None (no preset). The rendered glyph that ``plot`` returns exposes ``glyph.apply_style(style)`` (cleopatra >= 0.25) to re-apply a preset by name in place without re-plotting. @@ -282,7 +282,7 @@ class FacetSpec: When set, ``NetCDF.plot`` builds a stack of slices along the named dims and hands them to - :meth:`cleopatra.array_glyph.ArrayGlyph.facet`. At least one of + :meth:`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph.facet`. At least one of ``col`` or ``row`` must be set; ``row`` alone (without ``col``) is invalid and rejected by the validator. diff --git a/src/pyramids/netcdf/ugrid/dataset.py b/src/pyramids/netcdf/ugrid/dataset.py index 6cd7ae40a5..9b5f3a0c03 100644 --- a/src/pyramids/netcdf/ugrid/dataset.py +++ b/src/pyramids/netcdf/ugrid/dataset.py @@ -9,11 +9,17 @@ from dataclasses import replace from pathlib import Path -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import geopandas as gpd import numpy as np import shapely + +if TYPE_CHECKING: + from cleopatra.glyphs.gridded.array_glyph import PointOverlay + from cleopatra.styling.colorbar import ColorBar + from cleopatra.styling.params import Contour, DataStyle + from cleopatra.styling.scaling import ColorScaling from osgeo import gdal from pyproj import CRS, Transformer from shapely.geometry import LineString, box @@ -21,6 +27,7 @@ from pyramids.base.crs import sr_from_epsg from pyramids.dataset import Dataset from pyramids.dataset._plot_helpers import mesh_render as _mesh_render +from pyramids.dataset._plot_helpers import nonnull_group_kwargs as _nonnull_group_kwargs from pyramids.feature import FeatureCollection from pyramids.netcdf._mdim import open_mdarray from pyramids.netcdf.cf import write_global_attributes @@ -819,6 +826,12 @@ def plot( cmap: str = "viridis", title: str | None = None, basemap: bool | str | None = None, + colorbar: bool | ColorBar | None = None, + points: np.ndarray | PointOverlay | None = None, + kind: str = "auto", + color: ColorScaling | None = None, + contour: Contour | None = None, + data_style: DataStyle | None = None, **kwargs: Any, ) -> Any: """Plot a mesh data variable. @@ -838,6 +851,25 @@ def plot( basemap: If True, add an OpenStreetMap basemap. If a string, use it as the tile provider name (e.g. "CartoDB.Positron"). Default is None (no basemap). Requires the [viz] extra. + colorbar (bool or ColorBar, optional): Colour-bar spec, part of the + shared plot signature. A ``pyramids.plot.ColorBar(label=…, …)`` + configures the bar; ``False`` hides it and ``None`` (default) uses + cleopatra's default (a bar is drawn). Only forwarded when set. + points (np.ndarray or PointOverlay, optional): Accepted for signature + symmetry with the raster plot family, but a **no-op here** — a mesh + has no point-overlay layer (the mesh geometry is the data). Ignored. + kind (str, optional): Accepted for signature symmetry with the raster + plot family, but a **no-op here** — ``kind`` selects a raster + renderer (``imshow``/``pcolormesh``); the mesh always renders via + ``tripcolor``/``tricontour``. Ignored. + color (ColorScaling, optional): Colour-scale spec + ``pyramids.plot.ColorScaling`` (linear / power / sym-log / boundary / + midpoint norm), e.g. ``ColorScaling.power(gamma=0.7)``. Default ``None``. + contour (Contour, optional): Contour-line spec + ``pyramids.plot.Contour(levels=…, label_kw=…)``. Default ``None``. + data_style (DataStyle, optional): Data-style / relief spec + ``pyramids.plot.DataStyle(style=…, hillshade=…)``. (A mesh has no + cell-value overlay, so there is no ``cells`` param here.) Default ``None``. **kwargs: Additional arguments passed to mesh_render (forwarded to plot_mesh_data). Notably ``colorbar`` (``bool``, default ``True``): pass ``colorbar=False`` to @@ -851,7 +883,7 @@ def plot( shaded-relief array. Returns: - cleopatra.mesh_glyph.MeshGlyph instance with the plot + cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph instance with the plot rendered. Use the returned object to access the matplotlib handles and the mappable: @@ -878,6 +910,17 @@ def plot( title = variable_name if basemap and self.epsg is None: raise ValueError("UgridDataset must have a CRS (epsg) to use basemap.") + # ``points`` / ``kind`` are part of the shared raster-family plot signature + # but have no meaning for a mesh (no point overlay; the renderer is fixed to + # tripcolor/tricontour), so they are accepted and ignored. ``colorbar`` and the + # typed render groups (``color`` / ``contour`` / ``data_style``) map onto the mesh + # backend and are forwarded only when set (so cleopatra's backend defaults are + # preserved otherwise). + if colorbar is not None: + kwargs["colorbar"] = colorbar + kwargs.update( + _nonnull_group_kwargs(color=color, contour=contour, data_style=data_style) + ) result = _mesh_render( mesh=self._mesh, data=data, @@ -899,7 +942,7 @@ def plot_outline(self, ax: Any = None, **kwargs: Any) -> Any: **kwargs: Additional arguments passed to plot_mesh_outline. Returns: - cleopatra.mesh_glyph.MeshGlyph instance with the wireframe + cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph instance with the wireframe rendered. ``glyph.fig`` / ``glyph.ax`` are the matplotlib handles; ``glyph.im`` is ``None`` (an outline carries no scalar mapping, so no mappable is produced). diff --git a/src/pyramids/netcdf/ugrid/plot.py b/src/pyramids/netcdf/ugrid/plot.py index 53f5cb16cd..050a75652f 100644 --- a/src/pyramids/netcdf/ugrid/plot.py +++ b/src/pyramids/netcdf/ugrid/plot.py @@ -1,6 +1,6 @@ -"""UGRID mesh visualization (delegates to cleopatra.mesh_glyph). +"""UGRID mesh visualization (delegates to cleopatra.glyphs.gridded.mesh_glyph). -Thin wrapper around `cleopatra.mesh_glyph.MeshGlyph` that accepts +Thin wrapper around `cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph` that accepts pyramids `Mesh2d` objects and delegates all rendering to cleopatra. Requires the `viz` optional extra. Install with one of: @@ -10,11 +10,15 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from pyramids.base._utils import require_cleopatra +from pyramids.dataset._plot_helpers import _reject_replaced_cbar_kwargs from pyramids.netcdf.ugrid.mesh import Mesh2d +if TYPE_CHECKING: + from cleopatra.styling.colorbar import ColorBar + _CLEOPATRA_MSG = ( "Mesh plotting requires the cleopatra package. Install with one of:\n" " - PyPI: pip install 'pyramids-gis[viz]'\n" @@ -40,7 +44,7 @@ def _mesh_to_glyph(mesh: Mesh2d, **kwargs: Any) -> Any: and any key in `MeshGlyph.default_options`. Returns: - cleopatra.mesh_glyph.MeshGlyph: A MeshGlyph instance ready + cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph: A MeshGlyph instance ready for plotting. Call `.plot()` or `.plot_outline()` on the returned object. @@ -49,7 +53,7 @@ def _mesh_to_glyph(mesh: Mesh2d, **kwargs: Any) -> Any: The error message includes install instructions. """ require_cleopatra(_CLEOPATRA_MSG) - from cleopatra.mesh_glyph import MeshGlyph + from cleopatra.glyphs.gridded.mesh_glyph import MeshGlyph edge_nodes = None if mesh.edge_node_connectivity is not None: @@ -75,7 +79,7 @@ def plot_mesh_data( vmin: float | None = None, vmax: float | None = None, edgecolor: str = "none", - colorbar: bool = True, + colorbar: bool | ColorBar | None = True, title: str | None = None, **kwargs: Any, ) -> Any: @@ -85,7 +89,7 @@ def plot_mesh_data( face-centered data, uses `tripcolor` (each triangle colored by the value of its parent face). For node-centered data, uses `tricontourf` (smooth interpolated contours). All rendering - is delegated to `cleopatra.mesh_glyph.MeshGlyph.plot`. + is delegated to `cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph.plot`. The returned `MeshGlyph` instance gives access to the underlying `Figure`/`Axes` and all cleopatra capabilities @@ -111,8 +115,10 @@ def plot_mesh_data( edgecolor: Edge color for face rendering. Use `"none"` for no edges or `"gray"` for visible mesh lines. Defaults to `"none"`. - colorbar: Whether to add a colorbar to the plot. Defaults - to True. + colorbar: Colour-bar control. `True` (default) draws the default bar, + `False`/`None` hides it, or pass a `pyramids.plot.ColorBar(...)` to + style it (caption / placement / sizing) — cleopatra renders the typed + spec on `MeshGlyph` just like `ArrayGlyph`. title: Plot title string. Defaults to None (no title). **kwargs: Additional keyword arguments forwarded to `MeshGlyph.plot`. Common options include @@ -126,7 +132,7 @@ def plot_mesh_data( `location="node"`). Returns: - cleopatra.mesh_glyph.MeshGlyph: The MeshGlyph instance with + cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph: The MeshGlyph instance with the plot rendered. Access `glyph.fig` and `glyph.ax` for the matplotlib Figure and Axes. @@ -150,6 +156,13 @@ def plot_mesh_data( plot_kwargs["vmax"] = vmax plot_kwargs.update(kwargs) + # cleopatra 0.30's MeshGlyph.plot takes the typed render groups (color / contour / + # data_style) directly; the loose styling kwargs are no longer translated. A loose form + # cleopatra removed (color_scale / style / levels / ...) surfaces cleopatra's own + # "moved onto a grouped parameter object" error, and the loose cbar_* forms are rejected + # here so the mesh path shares pyramids' single typed colour-bar surface. + _reject_replaced_cbar_kwargs(plot_kwargs) + glyph.plot( data, location=location, @@ -188,7 +201,7 @@ def plot_mesh_outline( `matplotlib.collections.LineCollection`. Returns: - cleopatra.mesh_glyph.MeshGlyph: The MeshGlyph instance with + cleopatra.glyphs.gridded.mesh_glyph.MeshGlyph: The MeshGlyph instance with the wireframe rendered. Access `glyph.fig` and `glyph.ax` for the matplotlib Figure and Axes. diff --git a/src/pyramids/plot.py b/src/pyramids/plot.py index a6d9b841b5..a7a3765a09 100644 --- a/src/pyramids/plot.py +++ b/src/pyramids/plot.py @@ -1,7 +1,7 @@ """Typed cleopatra plot specs, re-exported for pyramids. pyramids' ``plot`` and ``animate`` methods accept and forward the cleopatra -plotting spec objects (the ``[viz]`` extra requires cleopatra >= 0.28, which +plotting spec objects (the ``[viz]`` extra requires cleopatra >= 0.30, which carries the complete spec API). Import them from here so a call site never has to reach into ``cleopatra`` directly: @@ -27,6 +27,19 @@ - ``PointOverlay`` — styled point overlay, passed as ``points=``. - ``Basemap`` / ``Feature`` — shaded-relief / coastline reference layers, passed as ``basemap=`` (distinct from a web-tile ``basemap=""``). +- ``ColorScaling`` — the colour-scale spec (linear / power / sym-log / boundary / + midpoint norm + colour-bar construction), passed as ``color=``. Its variant + constructors are ``ColorScaling.power(gamma=...)`` etc. +- ``Contour`` — contour-line control (``levels`` / ``labels`` / ``label_kw``), + passed as ``contour=``. +- ``CellValues`` — per-cell value annotation (``show`` / ``size`` / + ``background_threshold``), passed as ``cells=``. +- ``DataStyle`` — data-style preset + relief (``style`` / ``hillshade`` / ``bands`` / + ``alpha``), passed as ``data_style=``. +- ``Classify`` — value classification for vector glyphs (``scheme`` / ``k``), passed + as ``classify=``. +- ``PanelLabels`` — per-panel coordinate labels for faceting (``col`` / ``row``), + passed as ``labels=`` on the facet path. """ from typing import TYPE_CHECKING, Any @@ -37,22 +50,39 @@ if TYPE_CHECKING: # names for static type checkers / IDEs; resolved lazily at runtime # Re-exported for typing only (runtime resolution is via ``__getattr__``), so # ruff's unused-import rule is silenced rather than deleting the typing intent. - from cleopatra.array_glyph import ( # noqa: F401 - ColorBar, + from cleopatra.basemap.geo import Basemap, Feature # noqa: F401 + from cleopatra.glyphs.gridded.array_glyph import ( # noqa: F401 FrameLabel, + PanelLabels, PointOverlay, ) - from cleopatra.geo import Basemap, Feature # noqa: F401 + from cleopatra.styling.colorbar import ColorBar # noqa: F401 + from cleopatra.styling.params import ( # noqa: F401 + CellValues, + Classify, + Contour, + DataStyle, + ) + from cleopatra.styling.scaling import ColorScaling # noqa: F401 -_ARRAY_GLYPH = "cleopatra.array_glyph" -_GEO = "cleopatra.geo" +_ARRAY_GLYPH = "cleopatra.glyphs.gridded.array_glyph" +_COLORBAR = "cleopatra.styling.colorbar" +_GEO = "cleopatra.basemap.geo" +_PARAMS = "cleopatra.styling.params" +_SCALING = "cleopatra.styling.scaling" _CLEO_EXPORTS: dict[str, tuple[str, str]] = { - "ColorBar": (_ARRAY_GLYPH, "ColorBar"), + "ColorBar": (_COLORBAR, "ColorBar"), "FrameLabel": (_ARRAY_GLYPH, "FrameLabel"), "PointOverlay": (_ARRAY_GLYPH, "PointOverlay"), + "PanelLabels": (_ARRAY_GLYPH, "PanelLabels"), "Basemap": (_GEO, "Basemap"), "Feature": (_GEO, "Feature"), + "ColorScaling": (_SCALING, "ColorScaling"), + "Contour": (_PARAMS, "Contour"), + "CellValues": (_PARAMS, "CellValues"), + "DataStyle": (_PARAMS, "DataStyle"), + "Classify": (_PARAMS, "Classify"), } __all__ = sorted(_CLEO_EXPORTS) @@ -77,7 +107,7 @@ def __getattr__(name: str) -> Any: # branded upgrade hint instead of a bare AttributeError. raise OptionalPackageDoesNotExist( f"cleopatra is installed but too old to provide `{attribute}`. The " - "pyramids plotting specs require cleopatra >= 0.28; upgrade with: " + "pyramids plotting specs require cleopatra >= 0.30; upgrade with: " "pip install -U 'pyramids-gis[viz]'." ) from exc return spec diff --git a/tests/base/test_dtype_conversions.py b/tests/base/test_dtype_conversions.py index 40b98fccf8..e987a4487a 100644 --- a/tests/base/test_dtype_conversions.py +++ b/tests/base/test_dtype_conversions.py @@ -447,7 +447,7 @@ def test_returns_the_submodule_for_a_dotted_name(self): Test scenario: `__import__("a.b")` returns `a`, so the helper reads `sys.modules[name]` back instead. `import_basemap` passes - `cleopatra.tiles`, and only this path distinguishes the two. + `cleopatra.basemap.tiles`, and only this path distinguishes the two. """ module = require_optional("numpy.linalg", "unused", return_module=True) assert module.__name__ == "numpy.linalg", ( diff --git a/tests/basemap/test_basemap.py b/tests/basemap/test_basemap.py index acc288cbb4..9300f825cc 100644 --- a/tests/basemap/test_basemap.py +++ b/tests/basemap/test_basemap.py @@ -1,7 +1,7 @@ """Tests for pyramids.basemap.basemap. ``add_basemap`` and ``get_provider`` are thin wrappers over -``cleopatra.tiles.add_tiles`` / ``cleopatra.tiles.get_provider`` (the +``cleopatra.basemap.tiles.add_tiles`` / ``cleopatra.basemap.tiles.get_provider`` (the cleopatra C-6 helpers). These tests cover the delegation contract: the right cleopatra function is called with the translated kwargs, its return value is propagated, the missing-extra error path is wired up, and the @@ -17,29 +17,31 @@ pytestmark = pytest.mark.plot -pytest.importorskip("cleopatra.tiles", reason="cleopatra[tiles] extra not installed") +pytest.importorskip( + "cleopatra.basemap.tiles", reason="cleopatra[tiles] extra not installed" +) from pyramids.base._errors import OptionalPackageDoesNotExist from pyramids.basemap.basemap import add_basemap, get_provider class TestGetProvider: - """``get_provider`` delegates to ``cleopatra.tiles.get_provider``.""" + """``get_provider`` delegates to ``cleopatra.basemap.tiles.get_provider``.""" def test_delegates_to_cleopatra_get_provider(self): """The ``name`` argument is forwarded and the result returned verbatim. Test scenario: - Patch ``cleopatra.tiles.get_provider`` with a sentinel and + Patch ``cleopatra.basemap.tiles.get_provider`` with a sentinel and confirm ``get_provider("CartoDB.Positron")`` calls it once with that name and hands back its return value. """ - with patch("cleopatra.tiles.get_provider") as mock_get: + with patch("cleopatra.basemap.tiles.get_provider") as mock_get: result = get_provider("CartoDB.Positron") mock_get.assert_called_once_with("CartoDB.Positron") assert result is mock_get.return_value, ( - "get_provider must return cleopatra.tiles.get_provider's result" + "get_provider must return cleopatra.basemap.tiles.get_provider's result" ) def test_default_provider_round_trip(self): @@ -73,19 +75,19 @@ def test_missing_extra_raises(self): class TestAddBasemap: - """``add_basemap`` delegates to ``cleopatra.tiles.add_tiles``.""" + """``add_basemap`` delegates to ``cleopatra.basemap.tiles.add_tiles``.""" def test_delegates_with_default_kwargs(self): """A bare ``add_basemap(ax)`` forwards the documented defaults. Test scenario: - Patch ``cleopatra.tiles.add_tiles`` and call ``add_basemap`` + Patch ``cleopatra.basemap.tiles.add_tiles`` and call ``add_basemap`` with only ``ax``. Every wrapper-level default must reach cleopatra as a keyword (``crs=3857``, ``zoom="auto"``, …) and the axes must be passed positionally. """ sentinel_ax = object() - with patch("cleopatra.tiles.add_tiles") as mock_add: + with patch("cleopatra.basemap.tiles.add_tiles") as mock_add: result = add_basemap(sentinel_ax) mock_add.assert_called_once_with( @@ -101,7 +103,7 @@ def test_delegates_with_default_kwargs(self): retries=2, ) assert result is mock_add.return_value, ( - "add_basemap must return cleopatra.tiles.add_tiles's result" + "add_basemap must return cleopatra.basemap.tiles.add_tiles's result" ) def test_delegates_with_custom_kwargs(self): @@ -113,7 +115,7 @@ def test_delegates_with_custom_kwargs(self): those values (and the untouched defaults for the rest). """ sentinel_ax = object() - with patch("cleopatra.tiles.add_tiles") as mock_add: + with patch("cleopatra.basemap.tiles.add_tiles") as mock_add: add_basemap( sentinel_ax, crs=4326, @@ -141,22 +143,22 @@ def test_missing_extra_raises(self): Test scenario: Patch the ``import_basemap`` guard to raise; ``add_basemap`` must propagate ``OptionalPackageDoesNotExist`` and never reach - ``cleopatra.tiles.add_tiles``. + ``cleopatra.basemap.tiles.add_tiles``. """ with patch( "pyramids.basemap.basemap.import_basemap", side_effect=OptionalPackageDoesNotExist("no cleopatra[tiles]"), ): - with patch("cleopatra.tiles.add_tiles") as mock_add: + with patch("cleopatra.basemap.tiles.add_tiles") as mock_add: with pytest.raises(OptionalPackageDoesNotExist): add_basemap(object()) mock_add.assert_not_called() class TestCleopatraDelegation: - """The ``add_basemap`` -> ``cleopatra.tiles.add_tiles`` contract. + """The ``add_basemap`` -> ``cleopatra.basemap.tiles.add_tiles`` contract. - ``add_basemap`` is a thin wrapper over ``cleopatra.tiles.add_tiles`` + ``add_basemap`` is a thin wrapper over ``cleopatra.basemap.tiles.add_tiles`` (shipped in ``cleopatra >= 0.8.0``, pinned via the ``[viz]`` extra as ``cleopatra[tiles]``). These tests pin the contract that wrapper relies on: the helper is importable, and the two signatures stay compatible — @@ -166,18 +168,18 @@ class TestCleopatraDelegation: """ def test_cleopatra_add_tiles_is_importable(self): - """``cleopatra.tiles.add_tiles`` imports (C-6 contract).""" + """``cleopatra.basemap.tiles.add_tiles`` imports (C-6 contract).""" cleopatra_tiles = pytest.importorskip( - "cleopatra.tiles", + "cleopatra.basemap.tiles", reason="cleopatra[tiles] extra not installed", ) assert hasattr(cleopatra_tiles, "add_tiles"), ( - "cleopatra.tiles.add_tiles must exist so " + "cleopatra.basemap.tiles.add_tiles must exist so " "pyramids.basemap.add_basemap can delegate to it." ) def test_pyramids_and_cleopatra_share_addmap_signature(self): - """``add_basemap`` and ``cleopatra.tiles.add_tiles`` line up. + """``add_basemap`` and ``cleopatra.basemap.tiles.add_tiles`` line up. Test scenario: Compare the parameter names directly so a future cleopatra @@ -185,7 +187,7 @@ def test_pyramids_and_cleopatra_share_addmap_signature(self): at a downstream call site. """ cleopatra_tiles = pytest.importorskip( - "cleopatra.tiles", + "cleopatra.basemap.tiles", reason="cleopatra[tiles] extra not installed", ) cleo_sig = inspect.signature(cleopatra_tiles.add_tiles) @@ -196,18 +198,18 @@ def test_pyramids_and_cleopatra_share_addmap_signature(self): extra = pyr_params - cleo_params # Cleopatra is allowed to grow its surface: new *optional* params # (e.g. `user_agent=` / `max_tiles=` in 0.8.0) don't break the - # `add_basemap(ax, ...) -> cleopatra.tiles.add_tiles(ax, ...)` + # `add_basemap(ax, ...) -> cleopatra.basemap.tiles.add_tiles(ax, ...)` # delegation — pyramids just won't expose them. What *would* break # it is cleopatra making a previously-shared (or any) param # *required* while pyramids lacks it. for name in missing: param = cleo_sig.parameters[name] assert param.default is not inspect.Parameter.empty, ( - f"cleopatra.tiles.add_tiles added a *required* param {name!r} " + f"cleopatra.basemap.tiles.add_tiles added a *required* param {name!r} " "not in pyramids.add_basemap — the delegation contract is broken." ) assert not extra, ( - f"pyramids.add_basemap has params not in cleopatra.tiles.add_tiles: " + f"pyramids.add_basemap has params not in cleopatra.basemap.tiles.add_tiles: " f"{extra}. The thin-wrapper delegation will not work until " "these are pushed upstream." ) diff --git a/tests/dataset/collection/test_plot_labels.py b/tests/dataset/collection/test_plot_labels.py index b09cb04ef6..df211c7881 100644 --- a/tests/dataset/collection/test_plot_labels.py +++ b/tests/dataset/collection/test_plot_labels.py @@ -259,3 +259,30 @@ def test_setter_length_validation_and_clear(self): cube.time = [2000, 2001] cube.time = None assert cube.time is None + + +class TestPlotRenderGroups: + """The typed render groups are forwarded from ``DatasetCollection.plot``. + + ``render_array`` is mocked, so plain sentinels stand in for the cleopatra group + objects — the test only pins that a set group is forwarded and an unset one is not. + """ + + def test_set_groups_are_forwarded(self): + """Explicit ``color`` / ``data_style`` reach ``render_array``.""" + cube = _collection(3) + color, data_style = object(), object() + with patch("pyramids.dataset.collection.render_array") as render: + cube.plot(band=0, color=color, data_style=data_style) + kw = render.call_args.kwargs + assert kw["color"] is color, "explicit color must be forwarded" + assert kw["data_style"] is data_style, "explicit data_style must be forwarded" + + def test_unset_groups_are_not_forwarded(self): + """Unset groups are dropped so they never override the render backend default.""" + cube = _collection(3) + with patch("pyramids.dataset.collection.render_array") as render: + cube.plot(band=0) + kw = render.call_args.kwargs + for name in ("color", "contour", "cells", "data_style"): + assert name not in kw, f"unset {name} must not be forwarded" diff --git a/tests/dataset/plot/test_basemap_dispatch.py b/tests/dataset/plot/test_basemap_dispatch.py index dcf4e42f77..52c47a7618 100644 --- a/tests/dataset/plot/test_basemap_dispatch.py +++ b/tests/dataset/plot/test_basemap_dispatch.py @@ -1,7 +1,7 @@ """Tests for the `basemap=` type-dispatch in `render_array` (cleopatra 0.27). A string provider name (or `True`) draws pyramids' own web-tile basemap under -the raster; a `cleopatra.geo.Basemap` forwards to the glyph's relief/features +the raster; a `cleopatra.basemap.geo.Basemap` forwards to the glyph's relief/features reference layer instead. """ @@ -17,13 +17,15 @@ # Version-gate first: the module binds 0.28-only specs (Basemap) at module scope, # so an installed-but-older cleopatra must skip cleanly, not error at collection. -pytest.importorskip("cleopatra", minversion="0.28", reason="needs cleopatra >= 0.28") +pytest.importorskip("cleopatra", minversion="0.29", reason="needs cleopatra >= 0.29") _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") _cleo_config.Config.set_matplotlib_backend("agg") _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" +) +_cleo_geo = pytest.importorskip( + "cleopatra.basemap.geo", reason="cleopatra not installed" ) -_cleo_geo = pytest.importorskip("cleopatra.geo", reason="cleopatra not installed") ArrayGlyph = _cleo_array.ArrayGlyph Basemap = _cleo_geo.Basemap plt = pytest.importorskip("matplotlib.pyplot", reason="cleopatra not installed") diff --git a/tests/dataset/plot/test_new_render_params.py b/tests/dataset/plot/test_new_render_params.py index e5fda60cec..4f5f9b0533 100644 --- a/tests/dataset/plot/test_new_render_params.py +++ b/tests/dataset/plot/test_new_render_params.py @@ -5,8 +5,6 @@ proves the param reached the glyph rather than being dropped. """ -from unittest.mock import patch - import numpy as np import pytest @@ -17,11 +15,11 @@ # Version-gate first: the module binds a 0.28-only spec (ColorBar) at module scope, # so an installed-but-older cleopatra must skip cleanly, not error at collection. -pytest.importorskip("cleopatra", minversion="0.28", reason="needs cleopatra >= 0.28") +pytest.importorskip("cleopatra", minversion="0.29", reason="needs cleopatra >= 0.29") _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") _cleo_config.Config.set_matplotlib_backend("agg") _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph ColorBar = _cleo_array.ColorBar @@ -87,82 +85,14 @@ def test_full_bleed_reaches_animate(self): ) assert isinstance(result, ArrayGlyph) - def test_deprecated_cbar_kwarg_folds_into_colorbar_and_renders(self): - """A loose `cbar_*` kwarg is folded into `colorbar=ColorBar(...)` and still renders. - - `render_array` translates the deprecated loose colour-bar kwargs into a typed - ColorBar before the cleopatra call (so cleopatra never sees the loose form); the - folded spec still renders the label. - """ - captured: dict = {} - original = ArrayGlyph.plot - - def spy(self, *args, **kwargs): - captured.update(kwargs) - return original(self, *args, **kwargs) - - with patch.object(ArrayGlyph, "plot", spy): - with pytest.warns(DeprecationWarning, match="cbar_"): - glyph = self._dataset().plot(band=0, cbar_label="mm", cbar_length=0.85) - assert "cbar_label" not in captured, ( - "loose cbar_label must be folded away, not passed loose" - ) - assert isinstance(captured.get("colorbar"), ColorBar), ( - "cbar_* must fold into a ColorBar" - ) - label = glyph.cbar.ax.get_ylabel() or glyph.cbar.ax.get_xlabel() - assert label == "mm", f"folded ColorBar label should render, got {label!r}" - - def test_explicit_colorbar_wins_over_loose_cbar(self): - """An explicit `colorbar=ColorBar` wins; the loose `cbar_*` are dropped (deprecated).""" - with pytest.warns( - DeprecationWarning, match="ignored because an explicit colorbar" - ): - glyph = self._dataset().plot( - band=0, colorbar=ColorBar(label="TYPED"), cbar_label="LOOSE" - ) - label = glyph.cbar.ax.get_ylabel() or glyph.cbar.ax.get_xlabel() - assert label == "TYPED", f"explicit ColorBar should win, got {label!r}" - - def test_colorbar_false_drops_loose_cbar_and_hides_bar(self): - """`colorbar=False` hides the bar and drops the loose `cbar_*` (not folded). - - The suppress half of the conflict contract: an explicit `colorbar=False` wins, - so the loose kwarg is dropped with a warning naming the `False` case and no - colour bar is drawn. - """ - with pytest.warns(DeprecationWarning, match="ignored because colorbar=False"): - glyph = self._dataset().plot(band=0, colorbar=False, cbar_label="x") - assert isinstance(glyph, ArrayGlyph) - assert getattr(glyph, "cbar", None) is None, "colorbar=False must hide the bar" - - def test_colorbar_true_still_folds_loose_cbar(self): - """`colorbar=True` + a loose `cbar_*` folds the styling in (not dropped). + def test_loose_cbar_kwarg_rejected(self): + """A loose `cbar_*` kwarg is rejected in favour of the typed `ColorBar` spec. - `True` / `None` carry no caption of their own, so the loose kwargs must still - render — only a typed `ColorBar` (or `colorbar=False`) suppresses them. + cleopatra 0.30 removed the loose colour-bar styling keywords; pyramids exposes a + single typed colour-bar surface, so passing a loose `cbar_*` through the + `Dataset.plot` facade raises a `ValueError` naming `pyramids.plot.ColorBar` + instead of being folded or forwarded. """ - with pytest.warns(DeprecationWarning, match="cbar_"): - glyph = self._dataset().plot(band=0, colorbar=True, cbar_label="DEPTH") - label = glyph.cbar.ax.get_ylabel() or glyph.cbar.ax.get_xlabel() - assert label == "DEPTH", ( - f"colorbar=True must keep the loose label, got {label!r}" - ) - - def test_deprecated_cbar_kwarg_renders_on_the_facet_path(self): - """A loose `cbar_*` kwarg still renders the shared colour-bar label on the facet path. - - cleopatra's `facet` does not accept `colorbar=ColorBar` (only the loose `cbar_*` - kwargs), so the translation is skipped on the facet path — the loose form is kept, - and the returned grid's shared colour bar still carries the label. - """ - stack = np.random.default_rng(3).random((3, 6, 6)).astype("float32") - result = render_array( - arr=stack, - mode="facet", - facet_kwargs={"col": "time", "col_coords": [0, 1, 2]}, - cbar_label="mm", - extent=[0.0, 0.0, 1.0, 1.0], - ) - label = result.cbar.ax.get_ylabel() or result.cbar.ax.get_xlabel() - assert label == "mm", f"facet colour-bar label should render, got {label!r}" + dataset = self._dataset() + with pytest.raises(ValueError, match="ColorBar"): + dataset.plot(band=0, cbar_label="mm", cbar_length=0.85) diff --git a/tests/dataset/plot/test_plot_cleanups.py b/tests/dataset/plot/test_plot_cleanups.py index cd4c3de543..dbc5c96fb4 100644 --- a/tests/dataset/plot/test_plot_cleanups.py +++ b/tests/dataset/plot/test_plot_cleanups.py @@ -13,7 +13,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") @@ -332,7 +332,7 @@ def plot(self, **kwargs): return (None, self.ax) with _patch( - "cleopatra.array_glyph.ArrayGlyph", + "cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=_FakeGlyph, ): _rarr( diff --git a/tests/dataset/plot/test_plot_color.py b/tests/dataset/plot/test_plot_color.py index 742a8bbc99..d5846ce34d 100644 --- a/tests/dataset/plot/test_plot_color.py +++ b/tests/dataset/plot/test_plot_color.py @@ -278,14 +278,20 @@ def test_explicit_cmap_overrides_palette(self): @pytest.mark.plot def test_explicit_color_scale_overrides_palette(self): - """An explicit ``color_scale=`` opts out of the palette's boundary norm.""" - glyph = self._paletted_dataset().plot(band=0, color_scale="linear") + """An explicit ``color=ColorScaling`` opts out of the palette's boundary norm.""" + from pyramids.plot import ColorScaling + + glyph = self._paletted_dataset().plot(band=0, color=ColorScaling.linear()) assert type(glyph.im.norm).__name__ != "BoundaryNorm" @pytest.mark.plot def test_explicit_bounds_overrides_palette(self): - """A lone ``bounds=`` opts out of the palette rather than being overwritten.""" - glyph = self._paletted_dataset().plot(band=0, bounds=[0, 1, 2, 3]) + """An explicit ``color=ColorScaling.boundary`` opts out of the palette wiring.""" + from pyramids.plot import ColorScaling + + glyph = self._paletted_dataset().plot( + band=0, color=ColorScaling.boundary(bounds=[0, 1, 2, 3]) + ) assert glyph.im.cmap.name == "coolwarm_r" @pytest.mark.plot diff --git a/tests/dataset/plot/test_plot_dataset.py b/tests/dataset/plot/test_plot_dataset.py index e3a37e02b5..485838d740 100644 --- a/tests/dataset/plot/test_plot_dataset.py +++ b/tests/dataset/plot/test_plot_dataset.py @@ -13,7 +13,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") @@ -103,7 +103,7 @@ def test_plot_histogram_excludes_invalid_samples(self): Test scenario: A band carrying a no-data pixel (``-9999``) and a repeated ``7.0`` is histogrammed with ``exclude_value=7.0``. Capturing - the values handed to ``StatisticalGlyph`` proves both the + the values handed to ``HistogramGlyph`` proves both the no-data value and the explicit ``exclude_value`` are dropped, leaving only the genuine samples. """ @@ -128,7 +128,9 @@ def __init__(self, values, ax=None, **kwargs): def histogram(self, bins=15): return ("fig", "ax", {}) - with patch("cleopatra.statistical_glyph.StatisticalGlyph", new=_FakeSG): + with patch( + "cleopatra.glyphs.stats.histogram_glyph.HistogramGlyph", new=_FakeSG + ): dataset.plot_histogram(band=0, bins=5, exclude_value=7.0) vals = sorted(captured["values"].tolist()) assert vals == [ @@ -139,27 +141,31 @@ def histogram(self, bins=15): @pytest.mark.plot def test_invalid_color_scale_raises(self, src: Dataset): - """An unsupported ``color_scale`` fails fast with a clear message. + """A loose ``color_scale`` kwarg is rejected with a clear ``ValueError``. Test scenario: - ``color_scale="bogus"`` is rejected before any rendering work, - with a pyramids-side ``ValueError`` that names the offending - value (and lists the valid options). + cleopatra 0.30 moved the colour scale onto the ``color=ColorScaling`` group + and rejects the ``color_scale`` *key* itself (regardless of value), so + ``color_scale="bogus"`` raises a ``ValueError`` naming ``color_scale``, which + pyramids surfaces unchanged. """ dataset = Dataset(src) with pytest.raises(ValueError, match=r"color_scale"): dataset.plot(band=0, color_scale="bogus") @pytest.mark.plot - def test_valid_color_scale_any_case_passes(self, src: Dataset): - """A valid ``color_scale`` passes regardless of case. + def test_color_scaling_group_renders(self, src: Dataset): + """A ``color=ColorScaling`` spec validates and renders without raising. Test scenario: - ``ColorScale`` lookup is case-insensitive, so a mixed-case - ``"Power"`` must validate and render without raising. + cleopatra 0.30 moved the colour scale onto the typed ``ColorScaling`` + group, passed as ``color=``; a ``ColorScaling.power()`` spec must render + through ``Dataset.plot`` and return an ``ArrayGlyph``. """ + from pyramids.plot import ColorScaling + dataset = Dataset(src) - glyph = dataset.plot(band=0, color_scale="Power") + glyph = dataset.plot(band=0, color=ColorScaling.power()) assert isinstance(glyph, ArrayGlyph) @pytest.mark.plot @@ -231,7 +237,7 @@ def test_plot_histogram_all_nodata_raises(self): Test scenario: Every pixel equals the no-data value, so after masking there are no samples; ``plot_histogram`` must raise a targeted ``ValueError`` - rather than passing an empty array to ``StatisticalGlyph``. + rather than passing an empty array to ``HistogramGlyph``. """ arr = np.full((4, 4), -9999.0, dtype="float32") dataset = Dataset.create_from_array( @@ -428,7 +434,7 @@ def test_multi_band( src_arr: np.ndarray, ): dataset = Dataset(sentinel_raster) - array_glyph = dataset.plot(rgb=[3, 2, 1]) + array_glyph = dataset.plot(rgb_options={"rgb": [3, 2, 1]}) assert isinstance(array_glyph, ArrayGlyph) @pytest.mark.plot @@ -479,7 +485,7 @@ def test_geotiff( rasters_folder_rasters_number: int, rasters_folder_dim: tuple, ): - from cleopatra.array_glyph import ArrayGlyph + from cleopatra.glyphs.gridded.array_glyph import ArrayGlyph cube = DatasetCollection.read_multiple_files( rasters_folder_path, with_order=False @@ -555,14 +561,6 @@ def test_rgb_insufficient_bands_raises(self, tmp_path): with pytest.raises(ValueError, match="needs at least 3 bands"): cube.plot(rgb_options={"rgb": [0, 1, 2]}) - @pytest.mark.plot - def test_rgb_loose_kwarg_is_deprecated(self, tmp_path): - """The loose top-level `rgb=` still works but warns, mirroring Dataset.plot.""" - cube = self._rgb_cube(tmp_path, n_times=3) - with pytest.warns(DeprecationWarning, match="rgb_options"): - glyph = cube.plot(rgb=[0, 1, 2], percentile=2) - assert glyph.arr.shape == (3, 8, 8, 3) - @pytest.mark.plot def test_single_band_path_unchanged(self, tmp_path): """Without `rgb`, plot() still yields a colormapped single-band animation.""" @@ -588,7 +586,7 @@ def test_rgb_surface_reflectance_normalisation(self, tmp_path): @pytest.mark.plot def test_rgb_options_unknown_key_raises(self, tmp_path): - """An unknown `rgb_options` key raises (delegated to `_merge_rgb_options`).""" + """An unknown `rgb_options` key raises (delegated to `_unpack_rgb_options`).""" cube = self._rgb_cube(tmp_path, n_times=2) with pytest.raises(ValueError, match=r"Unknown keys in `rgb_options`"): cube.plot(rgb_options={"bogus": 1}) @@ -638,14 +636,6 @@ def test_rgb_with_exclude_value_warns(self, tmp_path): glyph = cube.plot(exclude_value=0, rgb_options={"rgb": [0, 1, 2]}) assert glyph.arr.shape == (2, 8, 8, 3), "RGB stack still rendered" - @pytest.mark.plot - def test_rgb_options_wins_over_loose_kwarg(self, tmp_path): - """On collision, `rgb_options` wins over the loose kwarg and still warns.""" - cube = self._rgb_cube(tmp_path, n_times=2, n_bands=4) - with pytest.warns(DeprecationWarning, match="rgb_options` wins"): - glyph = cube.plot(rgb=[1, 2, 3], rgb_options={"rgb": [0, 1, 2]}) - assert glyph.arr.shape == (2, 8, 8, 3), "grouped rgb composited every frame" - def _make_nc_subset_with_band_count(tmp_path, n_bands: int): """Build a NetCDF variable subset whose ``band_count`` is ``n_bands``. diff --git a/tests/dataset/plot/test_plot_engine.py b/tests/dataset/plot/test_plot_engine.py index bf069699f3..3809c3cb56 100644 --- a/tests/dataset/plot/test_plot_engine.py +++ b/tests/dataset/plot/test_plot_engine.py @@ -5,7 +5,6 @@ import numpy as np import pytest -from pyramids.base._errors import OptionalPackageDoesNotExist from pyramids.dataset import Dataset from pyramids.dataset._plot_helpers import render_array from pyramids.dataset.engines import Analysis @@ -13,7 +12,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph # cleopatra >= 0.26 bundles the point-overlay styling kwargs into this class, @@ -145,20 +144,21 @@ def facet(self, **kwargs): return _FakeGlyph, ctor_seen, plot_seen, animate_seen, facet_seen, animate_args - def test_constructor_owns_cmap_vmin_vmax_levels_cbar(self): - """Style/scale kwargs all land on the constructor for plot mode. + def test_constructor_owns_cmap_vmin_vmax_cbar(self): + """Plain style/scale kwargs land on the constructor for plot mode. Test scenario: - ``cmap``, ``vmin``, ``vmax``, ``levels``, ``cbar_kwargs`` - must reach ``ArrayGlyph.__init__`` so cleopatra's - ``default_options`` is set in one place. None of them may - also reach ``ArrayGlyph.plot``; otherwise the value would be - overwritten twice (the PR-6 D-4 fix). + ``cmap``, ``vmin``, ``vmax``, ``cbar_kwargs`` must reach + ``ArrayGlyph.__init__`` so cleopatra's ``default_options`` is set in one + place. None of them may also reach ``ArrayGlyph.plot``; otherwise the + value would be overwritten twice (the PR-6 D-4 fix). ``cbar_kwargs`` (the + raw matplotlib passthrough) is not one of the loose ``cbar_*`` styling + keywords the ``ColorBar`` spec replaced, so it stays valid. """ fake_cls, ctor, plot, _, _, _ = self._capture_calls() rng = np.random.default_rng(101) arr = rng.random((4, 4)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], @@ -166,10 +166,9 @@ def test_constructor_owns_cmap_vmin_vmax_levels_cbar(self): cmap="plasma", vmin=-1.0, vmax=2.0, - levels=10, cbar_kwargs={"orientation": "horizontal"}, ) - for key in ("cmap", "vmin", "vmax", "levels", "cbar_kwargs"): + for key in ("cmap", "vmin", "vmax", "cbar_kwargs"): assert key in ctor, f"`{key}` must land on the constructor; ctor={ctor}" assert key not in plot, ( f"`{key}` must NOT also reach cleo.plot; plot={plot}" @@ -197,7 +196,7 @@ def test_render_call_only_kwargs_reach_plot(self): "full_bleed": True, "kind": "imshow", } - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], @@ -223,7 +222,7 @@ def test_animate_mode_merges_both_buckets_into_animate_call(self): fake_cls, ctor, _, animate, _, anim_args = self._capture_calls() rng = np.random.default_rng(303) arr = rng.random((4, 4)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], @@ -258,7 +257,7 @@ def test_facet_mode_routes_kind_to_facet_call(self): fake_cls, ctor, _, _, facet, _ = self._capture_calls() rng = np.random.default_rng(404) arr = rng.random((3, 4, 4)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], @@ -291,7 +290,7 @@ def test_split_is_driven_by_option_keys(self): fake_cls, ctor, plot, _, _, _ = self._capture_calls() rng = np.random.default_rng(505) arr = rng.random((4, 4)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], @@ -299,195 +298,54 @@ def test_split_is_driven_by_option_keys(self): add_colorbar=False, kind="imshow", ) - assert "add_colorbar" in ctor and "add_colorbar" not in plot, ( - f"`add_colorbar` is an option_keys() ctor option; ctor={ctor}" + assert "add_colorbar" in ctor, f"`add_colorbar` is a ctor option; ctor={ctor}" + assert "add_colorbar" not in plot, ( + f"`add_colorbar` must not also reach the render call; plot={plot}" ) - assert "kind" in plot and "kind" not in ctor, ( + assert "kind" in plot, ( f"`kind` must be force-routed to the render call; plot={plot}" ) + assert "kind" not in ctor, f"`kind` must not stay on the ctor; ctor={ctor}" - def test_deprecated_cbar_kwargs_fold_into_colorbar(self): - """Deprecated loose ``cbar_*`` kwargs are folded into a ColorBar, not passed loose. + def test_loose_cbar_kwarg_rejected(self): + """A loose ``cbar_*`` kwarg is rejected in favour of the typed ``ColorBar`` spec. Test scenario: - ``render_array`` translates the loose colour-bar kwargs into a - ``colorbar=ColorBar(...)`` before the cleopatra call, so neither the - constructor nor the render call ever sees a raw ``cbar_*``; the folded - ColorBar carries the mapped fields. Uses the ``_capture_calls`` fake. + cleopatra 0.30 removed the loose colour-bar styling keywords; pyramids + exposes a single typed colour-bar surface, so a loose ``cbar_label`` / + ``ticks_spacing`` raises a pyramids ``ValueError`` naming + ``pyramids.plot.ColorBar`` rather than being folded or forwarded. """ - fake_cls, ctor, plot, _, _, _ = self._capture_calls() rng = np.random.default_rng(506) arr = rng.random((4, 4)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - with pytest.warns(DeprecationWarning, match="cbar_"): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - cbar_label="mm", - ticks_spacing=2, - ) - seen = {**ctor, **plot} - assert "cbar_label" not in seen, ( - f"loose cbar_label must be folded away; seen={sorted(seen)}" - ) - assert "ticks_spacing" not in seen, ( - f"loose ticks_spacing must be folded away; seen={sorted(seen)}" - ) - colorbar = plot.get("colorbar", ctor.get("colorbar")) - assert isinstance(colorbar, ColorBar), ( - f"cbar_* must fold into a ColorBar; got {colorbar!r}" - ) - assert colorbar.label == "mm" - assert colorbar.ticks_spacing == 2 + with pytest.raises(ValueError, match="ColorBar"): + render_array( + arr=arr, + extent=[0.0, 0.0, 1.0, 1.0], + mode="plot", + cbar_label="mm", + ticks_spacing=2, + ) - def test_deprecated_point_kwargs_fold_into_pointoverlay(self): - """Deprecated loose point_* kwargs are folded into a PointOverlay carrying the array. + def test_loose_point_kwarg_rejected(self): + """A loose ``point_*`` kwarg surfaces cleopatra's removed-keyword ``ValueError``. Test scenario: - ``render_array`` translates the loose point-style kwargs into a - ``points=PointOverlay(points, ...)`` before the cleopatra call, so the - raw ``point_*`` never reach cleopatra; the overlay carries the styling. + The point-styling keywords moved onto ``PointOverlay``; pyramids wraps a + bare ``points`` array in a ``PointOverlay`` but no longer translates the + loose ``point_*`` styling kwargs, so passing one reaches the real cleopatra + render call, which rejects it. """ - fake_cls, ctor, plot, _, _, _ = self._capture_calls() rng = np.random.default_rng(507) arr = rng.random((4, 4)).astype("float32") - pts = np.array([[1.0, 2, 3]]) - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - with pytest.warns(DeprecationWarning, match="point_"): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - points=pts, - point_color="red", - point_size=9, - ) - seen = {**ctor, **plot} - assert "point_color" not in seen, ( - f"loose point_color must be folded away; seen={sorted(seen)}" - ) - overlay = plot.get("points", ctor.get("points")) - assert isinstance(overlay, PointOverlay), ( - f"point_* must fold into a PointOverlay; got {overlay!r}" - ) - assert overlay.color == "red" - assert overlay.size == 9 - - def test_deprecated_pid_kwargs_fold_into_pointoverlay_labels(self): - """Legacy ``pid_*`` label kwargs fold into ``PointOverlay.label_*``. - - Test scenario: - The oldest loose aliases ``pid_color`` / ``pid_size`` map to the - overlay's ``label_color`` / ``label_size`` — the least-obvious mapping, - so pin it explicitly rather than trusting the map by inspection. - """ - fake_cls, ctor, plot, _, _, _ = self._capture_calls() - rng = np.random.default_rng(508) - arr = rng.random((4, 4)).astype("float32") - pts = np.array([[1.0, 2, 3]]) - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - with pytest.warns(DeprecationWarning, match="pid_"): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - points=pts, - pid_color="green", - pid_size=7, - ) - seen = {**ctor, **plot} - assert "pid_color" not in seen, ( - f"legacy pid_color must fold away; seen={sorted(seen)}" - ) - assert "pid_size" not in seen, ( - f"legacy pid_size must fold away; seen={sorted(seen)}" - ) - overlay = plot.get("points", ctor.get("points")) - assert isinstance(overlay, PointOverlay), ( - f"pid_* must fold into a PointOverlay; got {overlay!r}" - ) - assert overlay.label_color == "green" - assert overlay.label_size == 7 - - def test_point_label_alias_wins_over_legacy_pid(self): - """When both ``point_label_*`` and ``pid_*`` are passed, the modern name wins. - - Test scenario: - The fold is last-write-wins over ``_POINT_TO_OVERLAY`` insertion order - (``pid_*`` before ``point_label_*``), so passing both must land the - ``point_label_*`` value on the overlay — matching cleopatra's - ``_resolve_point_overlay`` precedence. Locks the fragile dict-order - invariant against a silent reorder. - """ - fake_cls, ctor, plot, _, _, _ = self._capture_calls() - rng = np.random.default_rng(509) - arr = rng.random((4, 4)).astype("float32") - pts = np.array([[1.0, 2, 3]]) - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - with pytest.warns(DeprecationWarning, match="point_"): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - points=pts, - pid_color="green", - point_label_color="purple", - ) - overlay = plot.get("points", ctor.get("points")) - assert overlay.label_color == "purple", ( - f"point_label_color must win over pid_color; got {overlay.label_color!r}" - ) - - def test_loose_point_kwargs_ignored_when_points_is_pointoverlay(self): - """A loose ``point_*`` is dropped (with a warning) when ``points`` is already typed. - - Test scenario: - An explicit ``PointOverlay`` wins — the loose ``point_color`` is popped - and warned as "ignored", and the overlay reaches the render call - untouched (its own styling preserved, not overwritten by the loose kwarg). - """ - fake_cls, ctor, plot, _, _, _ = self._capture_calls() - rng = np.random.default_rng(510) - arr = rng.random((4, 4)).astype("float32") - overlay_in = PointOverlay(np.array([[1.0, 2, 3]]), color="black") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - with pytest.warns(DeprecationWarning, match="ignored because points="): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - points=overlay_in, - point_color="red", - ) - seen = {**ctor, **plot} - assert "point_color" not in seen, "loose point_color must be dropped" - assert plot.get("points") is overlay_in, ( - "the explicit PointOverlay must reach the render call untouched" - ) - - def test_loose_point_kwargs_dropped_when_no_points(self): - """A loose ``point_*`` with no ``points`` is a no-op (warned and dropped). - - Test scenario: - Without a ``points`` array there is nothing to style, so the loose - ``point_color`` warns "no effect" and is dropped — never reaching - cleopatra, and no ``points`` kwarg is invented. - """ - fake_cls, ctor, plot, _, _, _ = self._capture_calls() - rng = np.random.default_rng(511) - arr = rng.random((4, 4)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - with pytest.warns(DeprecationWarning, match="no effect"): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - point_color="red", - ) - seen = {**ctor, **plot} - assert "point_color" not in seen, "loose point_color must be dropped" - assert "points" not in seen, "no points= may be invented from a loose kwarg" + with pytest.raises(ValueError): + render_array( + arr=arr, + extent=[0.0, 0.0, 1.0, 1.0], + mode="plot", + points=np.array([[1.0, 2, 3]]), + point_color="red", + ) @pytest.mark.plot def test_kind_contourf_reaches_plot_not_clobbered(self): @@ -546,27 +404,37 @@ class TestStyleHillshadePresets: not _supports_style, reason="cleopatra < 0.24 has no style presets" ) def test_style_preset_renders(self): - """A valid ``style`` preset name renders end to end. + """A ``data_style=DataStyle(style=...)`` preset renders end to end. Test scenario: - ``render_array(..., style="flow_accumulation")`` auto-routes the - preset to ``ArrayGlyph`` (``style`` is in ``option_keys()``) and - returns a rendered glyph — no pyramids-side handling needed. + cleopatra 0.30 moved the preset name onto the typed ``DataStyle`` group, + passed as ``data_style=``; ``render_array`` forwards it to the render call + and returns a rendered glyph. """ + from cleopatra.styling.params import DataStyle + rng = np.random.default_rng(737) arr = rng.random((6, 6)).astype("float32") glyph = render_array( - arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", style="flow_accumulation" + arr=arr, + extent=[0.0, 0.0, 1.0, 1.0], + mode="plot", + data_style=DataStyle(style="flow_accumulation"), ) assert isinstance(glyph, ArrayGlyph) @pytest.mark.skipif(not _supports_style, reason="cleopatra < 0.24 has no hillshade") def test_hillshade_renders(self): - """A ``hillshade`` blend renders end to end.""" + """A ``data_style=DataStyle(hillshade=True)`` blend renders end to end.""" + from cleopatra.styling.params import DataStyle + rng = np.random.default_rng(738) arr = rng.random((6, 6)).astype("float32") glyph = render_array( - arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", hillshade=True + arr=arr, + extent=[0.0, 0.0, 1.0, 1.0], + mode="plot", + data_style=DataStyle(hillshade=True), ) assert isinstance(glyph, ArrayGlyph) @@ -574,14 +442,16 @@ def test_hillshade_renders(self): not _supports_style, reason="cleopatra < 0.24 has no style presets" ) def test_dict_hillshade_renders(self): - """A dict ``hillshade`` (parameter passthrough) renders end to end.""" + """A dict ``hillshade`` on the ``DataStyle`` group renders end to end.""" + from cleopatra.styling.params import DataStyle + rng = np.random.default_rng(742) arr = rng.random((6, 6)).astype("float32") glyph = render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", - hillshade={"vert_exag": 8}, + data_style=DataStyle(hillshade={"vert_exag": 8}), ) assert isinstance(glyph, ArrayGlyph) @@ -589,14 +459,15 @@ def test_dict_hillshade_renders(self): not _supports_style, reason="cleopatra < 0.24 has no style presets" ) def test_style_and_hillshade_animate_renders(self): - """``style`` + ``hillshade`` render through the real animate path. + """``data_style`` renders through the real animate path. Test scenario: - Unlike the mocked NetCDF forwarding test, this drives cleopatra - 0.24's ``ArrayGlyph.animate(style=..., hillshade=...)`` for real - over a ``(time, rows, cols)`` stack, guarding the advertised - animated-shaded path against a future cleopatra signature change. + Drives cleopatra's ``ArrayGlyph.animate(data_style=...)`` for real over a + ``(time, rows, cols)`` stack, guarding the advertised animated-shaded path + against a future cleopatra signature change. """ + from cleopatra.styling.params import DataStyle + rng = np.random.default_rng(743) stack = rng.random((3, 6, 6)).astype("float32") glyph = render_array( @@ -604,101 +475,10 @@ def test_style_and_hillshade_animate_renders(self): extent=[0.0, 0.0, 1.0, 1.0], mode="animate", animation_axis_values=[0, 1, 2], - style="flow_accumulation", - hillshade=True, + data_style=DataStyle(style="flow_accumulation", hillshade=True), ) assert isinstance(glyph, ArrayGlyph) - def test_hillshade_only_on_old_cleopatra_raises_upgrade_hint(self): - """``hillshade=`` alone on a build lacking it raises the upgrade hint. - - Test scenario: - The guard checks each key independently. Simulate a cleopatra that - supports ``style`` but not ``hillshade``; a truthy ``hillshade=`` - with no ``style=`` must still raise the >= 0.24 upgrade error. - """ - rng = np.random.default_rng(744) - arr = rng.random((5, 5)).astype("float32") - style_only = (ArrayGlyph.option_keys() - {"hillshade"}) | {"style"} - with patch.object(ArrayGlyph, "option_keys", return_value=style_only): - with pytest.raises(OptionalPackageDoesNotExist, match="cleopatra >= 0.24"): - render_array( - arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", hillshade=True - ) - - @pytest.mark.parametrize("value", [False, None]) - def test_falsy_hillshade_is_dropped_not_forwarded(self, value): - """A ``None``/``False`` ``hillshade`` is dropped, never reaching cleopatra. - - Test scenario: - A falsy hillshade requests no shading, so ``render_array`` must pop - it before the ctor/render split — making it a true no-op on any - cleopatra version, not merely one that happens to accept the kwarg. - A fake glyph captures what actually reaches the constructor / plot - call, so this is independent of the installed cleopatra. - """ - fake_cls, ctor, plot, *_ = TestRenderArrayKwargRouting._capture_calls() - rng = np.random.default_rng(745) - arr = rng.random((5, 5)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - render_array( - arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", hillshade=value - ) - assert "hillshade" not in ctor, f"hillshade={value!r} must not reach the ctor" - assert "hillshade" not in plot, f"hillshade={value!r} must not reach plot" - - def test_none_style_is_dropped_not_forwarded(self): - """An explicit ``style=None`` is dropped, never reaching cleopatra. - - Test scenario: - ``style=None`` requests no preset, so ``render_array`` must pop it - before the ctor/render split — a true no-op on any cleopatra version, - symmetric with the falsy-``hillshade`` drop. A fake glyph captures - what actually reaches the constructor / plot call. - """ - fake_cls, ctor, plot, *_ = TestRenderArrayKwargRouting._capture_calls() - rng = np.random.default_rng(748) - arr = rng.random((5, 5)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - render_array(arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", style=None) - assert "style" not in ctor, "style=None must not reach the ctor" - assert "style" not in plot, "style=None must not reach plot" - - def test_empty_dict_hillshade_is_forwarded(self): - """An affirmative ``hillshade={}`` (default params) reaches the glyph. - - Test scenario: - ``{}`` is falsy but documented as "shade with default parameters", - so unlike ``False`` it must NOT be dropped — it flows to the glyph - constructor like any other affirmative hillshade request. - """ - fake_cls, ctor, _plot, *_ = TestRenderArrayKwargRouting._capture_calls() - rng = np.random.default_rng(746) - arr = rng.random((5, 5)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): - render_array( - arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", hillshade={} - ) - assert ctor.get("hillshade") == {}, "affirmative hillshade={} must be forwarded" - - def test_empty_dict_hillshade_on_old_cleopatra_raises_upgrade_hint(self): - """``hillshade={}`` on a build lacking hillshade raises the upgrade hint. - - Test scenario: - The affirmative empty-dict form must be gated exactly like - ``hillshade=True`` — an old cleopatra (``hillshade`` absent from - ``option_keys()``) yields the clear >= 0.24 error, not the cryptic - downstream "Unknown option". - """ - rng = np.random.default_rng(747) - arr = rng.random((5, 5)).astype("float32") - old_keys = ArrayGlyph.option_keys() - {"style", "hillshade"} - with patch.object(ArrayGlyph, "option_keys", return_value=old_keys): - with pytest.raises(OptionalPackageDoesNotExist, match="cleopatra >= 0.24"): - render_array( - arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", hillshade={} - ) - @pytest.mark.skipif( not _supports_style, reason="cleopatra < 0.24 has no style presets" ) @@ -720,43 +500,6 @@ def test_unknown_style_name_surfaces_cleopatra_valueerror(self): style="definitely_not_a_style", ) - def test_style_on_old_cleopatra_raises_upgrade_hint(self): - """``style=`` on a cleopatra without preset support raises a clear hint. - - Test scenario: - Simulate cleopatra < 0.24 by hiding ``style`` from - ``option_keys()``. The scoped guard in ``render_array`` must raise - :class:`OptionalPackageDoesNotExist` pointing at the >= 0.24 upgrade - instead of letting a cryptic "Unknown option" surface deep in the - render call. - """ - rng = np.random.default_rng(740) - arr = rng.random((5, 5)).astype("float32") - old_keys = ArrayGlyph.option_keys() - {"style", "hillshade"} - with patch.object(ArrayGlyph, "option_keys", return_value=old_keys): - with pytest.raises(OptionalPackageDoesNotExist, match="cleopatra >= 0.24"): - render_array( - arr=arr, - extent=[0.0, 0.0, 1.0, 1.0], - mode="plot", - style="topography", - ) - - def test_no_preset_kwargs_unaffected_on_old_cleopatra(self): - """Basic plotting is untouched by the guard on an older cleopatra. - - Test scenario: - With ``style`` hidden from ``option_keys()`` (simulated old - cleopatra) but no ``style`` / ``hillshade`` passed, ``render_array`` - renders normally — the guard is scoped to preset use only. - """ - rng = np.random.default_rng(741) - arr = rng.random((5, 5)).astype("float32") - old_keys = ArrayGlyph.option_keys() - {"style", "hillshade"} - with patch.object(ArrayGlyph, "option_keys", return_value=old_keys): - glyph = render_array(arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot") - assert isinstance(glyph, ArrayGlyph) - @pytest.mark.skipif( not _supports_apply_style, reason="cleopatra < 0.25 has no glyph.apply_style()", @@ -770,12 +513,14 @@ def test_returned_glyph_supports_apply_style(self): can re-apply a preset by name without rebuilding — verify the round trip through the ``Dataset.plot`` facade. """ + from cleopatra.styling.params import DataStyle + rng = np.random.default_rng(750) arr = rng.random((1, 8, 8)).astype("float32") dataset = Dataset.create_from_array( arr=arr, geo=(0, 0.1, 0, 2, 0, -0.1), epsg=4326 ) - glyph = dataset.plot(band=0, style="flow_accumulation") + glyph = dataset.plot(band=0, data_style=DataStyle(style="flow_accumulation")) assert glyph.style == "flow_accumulation" glyph.apply_style("topography") assert glyph.style == "topography", ( @@ -980,7 +725,7 @@ def test_point_overlay_reaches_the_render_call(self): overlay = PointOverlay(self._points(), color="red") rng = np.random.default_rng(761) arr = rng.random((5, 5)).astype("float32") - with patch("cleopatra.array_glyph.ArrayGlyph", new=fake_cls): + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): render_array( arr=arr, extent=[0.0, 0.0, 1.0, 1.0], mode="plot", points=overlay ) @@ -1020,3 +765,68 @@ def test_frame_label_animates(self): frame_label=FrameLabel(location=(1, 3)), ) assert isinstance(glyph, ArrayGlyph) + + +class TestRenderArrayGroupParams: + """Explicit cleopatra render-group objects and their precedence in render_array. + + The typed groups (``color`` / ``contour`` / ``cells`` / ``data_style``) reach the + render call, and an explicitly-passed group wins over the one built from the loose + kwargs (``color_scale`` / ``style`` / ...). + """ + + def test_explicit_color_group_reaches_render_call(self): + """An explicit ``color=ColorScaling`` is forwarded to the render call. + + Test scenario: + The typed ``color`` group must reach ``ArrayGlyph.plot`` verbatim (the loose + ``color_scale`` kwarg it replaces was removed and now raises). + """ + from pyramids.plot import ColorScaling + + fake_cls, _ctor, plot, *_ = TestRenderArrayKwargRouting._capture_calls() + rng = np.random.default_rng(11) + arr = rng.random((4, 4)).astype("float32") + explicit = ColorScaling.power(gamma=0.9) + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): + render_array( + arr=arr, + extent=[0.0, 0.0, 1.0, 1.0], + mode="plot", + color=explicit, + ) + assert plot.get("color") is explicit, ( + "explicit color must reach the render call" + ) + + def test_explicit_groups_reach_render_call(self): + """Explicit ``contour`` / ``cells`` / ``data_style`` reach the render call. + + Test scenario: + The typed groups other than ``color`` also forward verbatim to + ``ArrayGlyph.plot``. + """ + from cleopatra.styling.params import CellValues, Contour, DataStyle + + fake_cls, _ctor, plot, *_ = TestRenderArrayKwargRouting._capture_calls() + rng = np.random.default_rng(13) + arr = rng.random((4, 4)).astype("float32") + contour, cells, data_style = ( + Contour(levels=4), + CellValues(show=True), + DataStyle(style="flow_accumulation"), + ) + with patch("cleopatra.glyphs.gridded.array_glyph.ArrayGlyph", new=fake_cls): + render_array( + arr=arr, + extent=[0.0, 0.0, 1.0, 1.0], + mode="plot", + contour=contour, + cells=cells, + data_style=data_style, + ) + assert plot.get("contour") is contour, ( + "explicit contour must reach the render call" + ) + assert plot.get("cells") is cells, "explicit cells must reach the render call" + assert plot.get("data_style") is data_style, "explicit data_style must reach it" diff --git a/tests/dataset/plot/test_plot_facade.py b/tests/dataset/plot/test_plot_facade.py index 19b1f4fddf..f77dfdd27b 100644 --- a/tests/dataset/plot/test_plot_facade.py +++ b/tests/dataset/plot/test_plot_facade.py @@ -12,7 +12,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") @@ -42,7 +42,7 @@ def test_returns_array_glyph(self): Test scenario: Calling the facade with a single-band dataset must return - an instance of :class:`cleopatra.array_glyph.ArrayGlyph` so + an instance of :class:`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph` so downstream callers can chain visual customisations. """ rng = np.random.default_rng(1337) @@ -79,64 +79,19 @@ def test_two_consecutive_calls_return_independent_figures(self): "Each call must own a distinct matplotlib Figure" ) - @pytest.mark.plot - @pytest.mark.parametrize( - "color_scale", - [ - "linear", - "power", - "sym-lognorm", - "boundary-norm", - "Power", - ], - ) - def test_color_scale_string_aliases_work(self, color_scale): - """``color_scale`` accepts the ``cleopatra.styles.ColorScale`` aliases. - - Args: - color_scale: String alias for the colour-scale enum (lookup - is case-insensitive on the cleopatra side). - - Test scenario: - The plot docstrings document the string aliases as the valid - ``color_scale`` values. Verify each alias (including a - mixed-case spelling) is forwarded to cleopatra and yields an - ``ArrayGlyph``. - - Notes: - cleopatra's ``ColorScale`` StrEnum replaced the legacy - integer codes (``1``-``5``); those are no longer accepted — - see :meth:`test_color_scale_integer_codes_rejected`. - """ - rng = np.random.default_rng(1337) - arr = rng.random((6, 6)).astype("float32") - dataset = Dataset.create_from_array( - arr, top_left_corner=(0, 0), cell_size=0.05, epsg=4326 - ) - plot_kwargs = {"color_scale": color_scale} - if color_scale.lower() == "boundary-norm": - plot_kwargs["bounds"] = [0.0, 0.25, 0.5, 0.75, 1.0] - - result = dataset.plot(**plot_kwargs) - assert isinstance(result, ArrayGlyph), ( - f"color_scale={color_scale!r} should return ArrayGlyph, " - f"got {type(result).__name__}" - ) - @pytest.mark.plot @pytest.mark.parametrize("color_scale", [1, 2, 3, 4, 5, 0]) def test_color_scale_integer_codes_rejected(self, color_scale): """Legacy integer ``color_scale`` codes raise a clear ``ValueError``. Args: - color_scale: A legacy integer code that older releases - accepted but cleopatra's ``ColorScale`` enum rejects. + color_scale: A legacy integer code that older releases accepted. Test scenario: - cleopatra now validates ``color_scale`` against the - ``ColorScale`` StrEnum and raises ``ValueError`` for anything - that is not one of the documented aliases (or a ``ColorScale`` - member). Confirm pyramids surfaces that error unchanged. + cleopatra 0.30 moved the colour scale onto the ``color=ColorScaling`` group + and rejects the loose ``color_scale`` *key* regardless of its value (an + integer code, a string alias, or anything else), so pyramids surfaces that + ``ValueError`` unchanged. """ rng = np.random.default_rng(7) arr = rng.random((5, 5)).astype("float32") @@ -221,48 +176,6 @@ def test_rgb_options_carries_all_four_keys(self): assert call_kwargs["cutoff"] == opts["cutoff"] assert call_kwargs["percentile"] == opts["percentile"] - @pytest.mark.plot - def test_loose_rgb_emits_deprecation_warning(self): - """Passing `rgb=` loose at the top level emits DeprecationWarning.""" - rng = np.random.default_rng(13) - arr = rng.random((3, 6, 6)).astype("float32") - dataset = Dataset.create_from_array( - arr, top_left_corner=(0, 0), cell_size=0.05, epsg=4326 - ) - with patch.object(type(dataset.analysis), "plot", autospec=True) as mock_plot: - mock_plot.return_value = "stub" - with pytest.warns(DeprecationWarning, match=r"rgb_options"): - dataset.plot(rgb=[0, 1, 2]) - assert mock_plot.call_args.kwargs["rgb"] == [0, 1, 2] - - @pytest.mark.plot - @pytest.mark.parametrize( - "key,value", - [ - ("surface_reflectance", 10000), - ("cutoff", [0.1, 0.9]), - ("percentile", 2), - ], - ) - def test_other_loose_kwargs_emit_deprecation_warning(self, key, value): - """Each Sentinel-loose kwarg emits a DeprecationWarning individually. - - Args: - key: One of ``"surface_reflectance"`` / ``"cutoff"`` / - ``"percentile"``. - value: Test value to pass in for that kwarg. - """ - rng = np.random.default_rng(14) - arr = rng.random((3, 6, 6)).astype("float32") - dataset = Dataset.create_from_array( - arr, top_left_corner=(0, 0), cell_size=0.05, epsg=4326 - ) - with patch.object(type(dataset.analysis), "plot", autospec=True) as mock_plot: - mock_plot.return_value = "stub" - with pytest.warns(DeprecationWarning, match=r"rgb_options"): - dataset.plot(**{key: value}) - assert mock_plot.call_args.kwargs[key] == value - @pytest.mark.plot def test_rgb_options_unknown_key_raises(self): """Unknown keys in `rgb_options` are rejected with a clear error.""" @@ -274,60 +187,6 @@ def test_rgb_options_unknown_key_raises(self): with pytest.raises(ValueError, match=r"Unknown keys"): dataset.plot(rgb_options={"bogus": True}) - @pytest.mark.plot - def test_rgb_options_overrides_loose_kwarg(self): - """When both forms are passed, `rgb_options` wins for the same key.""" - rng = np.random.default_rng(16) - arr = rng.random((3, 6, 6)).astype("float32") - dataset = Dataset.create_from_array( - arr, top_left_corner=(0, 0), cell_size=0.05, epsg=4326 - ) - with patch.object(type(dataset.analysis), "plot", autospec=True) as mock_plot: - mock_plot.return_value = "stub" - with pytest.warns(DeprecationWarning): - dataset.plot( - rgb=[2, 1, 0], - rgb_options={"rgb": [0, 1, 2]}, - ) - assert mock_plot.call_args.kwargs["rgb"] == [0, 1, 2] - - def test_collision_warning_distinguishes_from_pure_loose(self): - """A loose+grouped collision warns "rgb_options wins", not "group them" (M2). - - Test scenario: - M2 fix — when both `rgb=` (loose) and `rgb_options={"rgb": ...}` - are passed for the same key, the DeprecationWarning must say - the loose value was overridden ("`rgb_options` wins — drop - the loose form"), *not* the misleading "Group them under - `rgb_options={...}` instead" message that implies the user - forgot the grouped form. Conversely, a pure-loose call (no - `rgb_options`) still gets the original "group them" wording. - """ - rng = np.random.default_rng(17) - arr = rng.random((3, 6, 6)).astype("float32") - dataset = Dataset.create_from_array( - arr, top_left_corner=(0, 0), cell_size=0.05, epsg=4326 - ) - with patch.object(type(dataset.analysis), "plot", autospec=True) as mock_plot: - mock_plot.return_value = "stub" - with warnings.catch_warnings(record=True) as collide: - warnings.simplefilter("always") - dataset.plot(rgb=[2, 1, 0], rgb_options={"rgb": [0, 1, 2]}) - with warnings.catch_warnings(record=True) as pure: - warnings.simplefilter("always") - dataset.plot(rgb=[2, 1, 0]) - collide_msg = " ".join(str(w.message) for w in collide) - pure_msg = " ".join(str(w.message) for w in pure) - assert ( - "rgb_options` wins" in collide_msg and "drop the loose form" in collide_msg - ), f"collision warning should say rgb_options wins; got: {collide_msg!r}" - assert "Group them under" not in collide_msg, ( - f"collision warning must not use the 'group them' wording; got: {collide_msg!r}" - ) - assert "Group them under" in pure_msg, ( - f"pure-loose warning should keep the 'group them' wording; got: {pure_msg!r}" - ) - class TestDatasetPlotRgbOptionsEdges: """PR-4 / D-3 edge cases for ``rgb_options=`` not in :class:`TestDatasetPlotRgbOptions`. @@ -417,85 +276,3 @@ def test_empty_rgb_options_dict_is_noop(self, multiband_dataset): w for w in captured if issubclass(w.category, DeprecationWarning) ] assert not deprecations, "Empty rgb_options must not emit DeprecationWarning" - - @pytest.mark.plot - def test_loose_rgb_with_empty_group_still_warns(self, multiband_dataset): - """Loose `rgb=` with `rgb_options={}` still triggers the deprecation. - - Test scenario: - The warning is gated on the loose kwargs being not-None - (not on whether ``rgb_options`` is provided). An empty - group doesn't suppress the warning when a loose kwarg is - present. The loose ``rgb`` survives because the empty - group has no entries to overwrite it. - """ - with patch.object( - type(multiband_dataset.analysis), "plot", autospec=True - ) as mock_plot: - mock_plot.return_value = "stub" - with pytest.warns(DeprecationWarning, match=r"rgb_options"): - multiband_dataset.plot(rgb=[0, 1, 2], rgb_options={}) - assert mock_plot.call_args.kwargs["rgb"] == [ - 0, - 1, - 2, - ], "Loose rgb must survive when rgb_options is empty" - - @pytest.mark.plot - def test_only_loose_surface_reflectance_emits_warning_once(self, multiband_dataset): - """A single loose `surface_reflectance=` triggers exactly one warning. - - Test scenario: - The merge helper emits one warning regardless of how many - loose kwargs were passed; with only one set, exactly one - DeprecationWarning fires. Verifies the warning is not - spammed per-kwarg. - """ - with patch.object( - type(multiband_dataset.analysis), "plot", autospec=True - ) as mock_plot: - mock_plot.return_value = "stub" - with warnings.catch_warnings(record=True) as captured: - warnings.simplefilter("always") - multiband_dataset.plot(surface_reflectance=10000) - deprecations = [ - w for w in captured if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecations) == 1, ( - f"Exactly one DeprecationWarning expected, got {len(deprecations)}: " - f"{[str(w.message) for w in deprecations]}" - ) - assert "surface_reflectance" in str(deprecations[0].message), ( - f"Warning must name the loose kwarg, got: {deprecations[0].message}" - ) - - @pytest.mark.plot - def test_rgb_options_only_partial_override(self, multiband_dataset): - """`rgb_options` overrides only its own keys; other loose kwargs survive. - - Test scenario: - Pass loose ``percentile=2`` AND - ``rgb_options={"rgb": [0, 1, 2]}``. The grouped form - overrides only ``rgb`` (which wasn't set loose), and the - loose ``percentile`` propagates unchanged. The - DeprecationWarning still fires because at least one loose - kwarg was used. - """ - with patch.object( - type(multiband_dataset.analysis), "plot", autospec=True - ) as mock_plot: - mock_plot.return_value = "stub" - with pytest.warns(DeprecationWarning, match=r"percentile"): - multiband_dataset.plot( - percentile=2, - rgb_options={"rgb": [0, 1, 2]}, - ) - call_kwargs = mock_plot.call_args.kwargs - assert call_kwargs["rgb"] == [ - 0, - 1, - 2, - ], f"Grouped rgb must propagate, got: {call_kwargs.get('rgb')}" - assert call_kwargs["percentile"] == 2, ( - f"Loose percentile must survive, got: {call_kwargs.get('percentile')}" - ) diff --git a/tests/dataset/plot/test_plot_resolution.py b/tests/dataset/plot/test_plot_resolution.py index 0facee8d77..7807647167 100644 --- a/tests/dataset/plot/test_plot_resolution.py +++ b/tests/dataset/plot/test_plot_resolution.py @@ -461,19 +461,6 @@ def test_facade_routes_through_resolver(self): class TestRasterBasePlotSignatureContract: """M-1 regression: align abstract signature with concrete override.""" - def test_surface_reflectance_default_is_none(self): - """``RasterBase.plot.surface_reflectance`` defaults to ``None``. - - Test scenario: - Pre-PR-1 the abstract default was ``10000`` (Sentinel - reflectance constant). PR-1 changed it to ``None`` to mirror - the concrete ``Dataset.plot`` override and avoid a confusing - cross-class default mismatch. - """ - sig = inspect.signature(RasterBase.plot) - param = sig.parameters["surface_reflectance"] - assert param.default is None, f"Expected None default, got {param.default!r}" - def test_band_default_is_none(self): """``RasterBase.plot.band`` defaults to ``None``. @@ -485,18 +472,29 @@ def test_band_default_is_none(self): param = sig.parameters["band"] assert param.default is None, f"Expected None default, got {param.default!r}" - def test_rgb_default_is_none(self): - """``RasterBase.plot.rgb`` defaults to ``None``. + def test_rgb_options_default_is_none(self): + """``RasterBase.plot.rgb_options`` defaults to ``None``. Test scenario: - ``rgb=None`` lets the per-class facade decide whether to - apply an RGB heuristic — a concrete default would short- - circuit that decision on the abstract layer. + The loose ``rgb`` / ``surface_reflectance`` / ``cutoff`` kwargs were dropped + from the plot facades in favour of a single grouped ``rgb_options=`` dict; the + ABC declares it too, defaulting to ``None`` (no true-colour composite). """ sig = inspect.signature(RasterBase.plot) - param = sig.parameters["rgb"] + param = sig.parameters["rgb_options"] assert param.default is None, f"Expected None default, got {param.default!r}" + def test_loose_rgb_params_removed_from_signature(self): + """The deprecated loose Sentinel kwargs are gone from the ABC signature. + + Test scenario: + ``rgb`` / ``surface_reflectance`` / ``cutoff`` must no longer appear as + parameters — they are folded into ``rgb_options``. + """ + params = inspect.signature(RasterBase.plot).parameters + for name in ("rgb", "surface_reflectance", "cutoff"): + assert name not in params, f"{name!r} should have been removed from the ABC" + class TestNetCDFPlotPolicy: """Logic-only tests for :meth:`NetCDF.plot` (no cleopatra import). diff --git a/tests/dataset/plot/test_plot_specs.py b/tests/dataset/plot/test_plot_specs.py index 9d1b2a9dc9..b2a62518c8 100644 --- a/tests/dataset/plot/test_plot_specs.py +++ b/tests/dataset/plot/test_plot_specs.py @@ -18,19 +18,37 @@ class TestPlotSpecReExports: """`pyramids.plot` lazily re-exports cleopatra's plot spec classes.""" def test_all_and_dir_list_the_specs(self): - """`__all__` / `dir()` advertise the five specs, sorted.""" - expected = ["Basemap", "ColorBar", "Feature", "FrameLabel", "PointOverlay"] + """`__all__` / `dir()` advertise every re-exported spec, sorted.""" + expected = [ + "Basemap", + "CellValues", + "Classify", + "ColorBar", + "ColorScaling", + "Contour", + "DataStyle", + "Feature", + "FrameLabel", + "PanelLabels", + "PointOverlay", + ] assert plot_specs.__all__ == expected assert dir(plot_specs) == expected @pytest.mark.parametrize( "name, module_name, attribute", [ - ("ColorBar", "cleopatra.array_glyph", "ColorBar"), - ("FrameLabel", "cleopatra.array_glyph", "FrameLabel"), - ("PointOverlay", "cleopatra.array_glyph", "PointOverlay"), - ("Basemap", "cleopatra.geo", "Basemap"), - ("Feature", "cleopatra.geo", "Feature"), + ("ColorBar", "cleopatra.styling.colorbar", "ColorBar"), + ("FrameLabel", "cleopatra.glyphs.gridded.array_glyph", "FrameLabel"), + ("PointOverlay", "cleopatra.glyphs.gridded.array_glyph", "PointOverlay"), + ("PanelLabels", "cleopatra.glyphs.gridded.array_glyph", "PanelLabels"), + ("Basemap", "cleopatra.basemap.geo", "Basemap"), + ("Feature", "cleopatra.basemap.geo", "Feature"), + ("ColorScaling", "cleopatra.styling.scaling", "ColorScaling"), + ("Contour", "cleopatra.styling.params", "Contour"), + ("CellValues", "cleopatra.styling.params", "CellValues"), + ("DataStyle", "cleopatra.styling.params", "DataStyle"), + ("Classify", "cleopatra.styling.params", "Classify"), ], ) def test_spec_resolves_to_cleopatra_class(self, name, module_name, attribute): @@ -66,5 +84,5 @@ class _OldCleopatraModule: with patch( "pyramids.plot.require_optional", return_value=_OldCleopatraModule() ): - with pytest.raises(OptionalPackageDoesNotExist, match="0.28"): + with pytest.raises(OptionalPackageDoesNotExist, match="0.30"): plot_specs.ColorBar diff --git a/tests/feature/test_plot_cleopatra.py b/tests/feature/test_plot_cleopatra.py index b975ad4a5a..b072d35c7e 100644 --- a/tests/feature/test_plot_cleopatra.py +++ b/tests/feature/test_plot_cleopatra.py @@ -8,7 +8,10 @@ from __future__ import annotations +from unittest.mock import MagicMock, patch + import geopandas as gpd +import numpy as np import pytest from shapely.geometry import Point, Polygon, box @@ -20,8 +23,12 @@ # Guard every optional-dependency import (cleopatra + its matplotlib backend) # behind importorskip so the module skips cleanly in a core-only install # (e.g. the wheel-test job), rather than erroring at collection. -_pg = pytest.importorskip("cleopatra.polygon_glyph", reason="cleopatra not installed") -_sg = pytest.importorskip("cleopatra.scatter_glyph", reason="cleopatra not installed") +_pg = pytest.importorskip( + "cleopatra.glyphs.primitives.polygon_glyph", reason="cleopatra not installed" +) +_sg = pytest.importorskip( + "cleopatra.glyphs.primitives.scatter_glyph", reason="cleopatra not installed" +) _cfg = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") _mpl_axes = pytest.importorskip("matplotlib.axes", reason="matplotlib not installed") _cfg.Config.set_matplotlib_backend("agg") @@ -213,3 +220,141 @@ def test_null_geometry_raises_clean_value_error(self, valid_geom): ) with pytest.raises(ValueError, match="Point or Polygon"): fc.plot(column="v", engine="cleopatra") + + +class TestFeatureCollectionPlotAlignment: + """The raster-family plot params (colorbar/points/kind/title) on both engines. + + ``FeatureCollection.plot`` shares the raster plot signature. ``colorbar`` / ``title`` + map onto both back-ends; ``points`` / ``kind`` have no vector meaning and are accepted + but ignored. + """ + + @staticmethod + def _points_fc(): + """A two-point FeatureCollection carrying a numeric column.""" + return FeatureCollection( + gpd.GeoDataFrame( + {"v": [1.0, 2.0]}, + geometry=[Point(0, 0), Point(1, 1)], + crs="EPSG:4326", + ) + ) + + def test_cleopatra_forwards_colorbar_and_title_to_glyph_plot(self): + """``colorbar`` / ``title`` reach the glyph's ``plot`` on the cleopatra engine. + + Test scenario: + The cleopatra engine calls ``ScatterGlyph.plot`` — spy on it and assert the + hoisted ``colorbar`` / ``title`` arrive there (they used to be dropped by + ``filter_kwargs`` because they are render-call, not constructor, params). + """ + from cleopatra.styling.colorbar import ColorBar + + bar = ColorBar(label="v") + with patch.object( + ScatterGlyph, "plot", return_value=(MagicMock(), MagicMock(), MagicMock()) + ) as mock_plot: + self._points_fc().plot( + column="v", engine="cleopatra", colorbar=bar, title="values" + ) + kw = mock_plot.call_args.kwargs + assert kw.get("colorbar") is bar, "colorbar must reach the glyph plot call" + assert kw.get("title") == "values", "title must reach the glyph plot call" + + def test_cleopatra_points_and_kind_are_noops(self): + """``points`` / ``kind`` are accepted for symmetry and never reach the glyph. + + Test scenario: + The raster-only ``points`` / ``kind`` must neither raise nor be forwarded to + ``ScatterGlyph.plot`` on the cleopatra engine. + """ + with patch.object( + ScatterGlyph, "plot", return_value=(MagicMock(), MagicMock(), MagicMock()) + ) as mock_plot: + self._points_fc().plot( + column="v", + engine="cleopatra", + points=np.array([[1.0, 0, 0]]), + kind="contourf", + ) + kw = mock_plot.call_args.kwargs + assert "points" not in kw, "points must not reach the glyph plot call" + assert "kind" not in kw, "kind must not reach the glyph plot call" + + def test_geopandas_title_sets_axes_title(self): + """``title`` is set on the returned Axes for the geopandas engine. + + Test scenario: + geopandas' ``plot`` takes no ``title`` kwarg, so the facade sets it on the + returned Axes; ``ax.get_title()`` must echo it. + """ + ax = self._points_fc().plot(column="v", title="my map") + assert ax.get_title() == "my map", f"unexpected axes title: {ax.get_title()!r}" + + def test_geopandas_colorbar_toggles_legend(self): + """``colorbar`` maps to geopandas' ``legend`` (a colour-bar Axes appears). + + Test scenario: + A continuous ``legend`` adds a dedicated colour-bar Axes, so + ``colorbar=True`` yields a figure with two Axes and ``colorbar=False`` one. + """ + ax_on = self._points_fc().plot(column="v", colorbar=True) + assert len(ax_on.figure.axes) == 2, "colorbar=True must draw a legend Axes" + ax_off = self._points_fc().plot(column="v", colorbar=False) + assert len(ax_off.figure.axes) == 1, "colorbar=False must draw no legend Axes" + + +class TestFeatureCollectionGroupParams: + """Typed render groups (color/contour/classify) on ``FeatureCollection.plot``. + + ``ScatterGlyph`` / ``PolygonGlyph`` accept ``color`` / ``contour`` / ``classify``; the + facade forwards them on ``engine="cleopatra"`` and ignores them on + ``engine="geopandas"`` (no geopandas equivalent). + """ + + @staticmethod + def _points_fc(): + """A two-point FeatureCollection carrying a numeric column.""" + return FeatureCollection( + gpd.GeoDataFrame( + {"v": [1.0, 2.0]}, + geometry=[Point(0, 0), Point(1, 1)], + crs="EPSG:4326", + ) + ) + + def test_cleopatra_forwards_color_contour_classify(self): + """``color`` / ``contour`` / ``classify`` reach the glyph on the cleopatra engine.""" + from cleopatra.styling.params import Classify, Contour + from cleopatra.styling.scaling import ColorScaling + + color = ColorScaling.linear() + contour = Contour(levels=3) + classify = Classify(scheme="quantiles", k=3) + with patch.object( + ScatterGlyph, "plot", return_value=(MagicMock(), MagicMock(), MagicMock()) + ) as mock_plot: + self._points_fc().plot( + column="v", + engine="cleopatra", + color=color, + contour=contour, + classify=classify, + ) + kw = mock_plot.call_args.kwargs + assert kw.get("color") is color, "color must reach the glyph plot call" + assert kw.get("contour") is contour, "contour must reach the glyph plot call" + assert kw.get("classify") is classify, "classify must reach the glyph plot call" + + def test_geopandas_ignores_group_objects(self): + """A cleopatra group on the geopandas engine is ignored, not forwarded/erroring. + + Test scenario: + geopandas has no ``color`` kwarg, so passing a ``ColorScaling`` on the default + engine must render a plain Axes rather than raising an unexpected-kwarg error. + """ + from cleopatra.styling.scaling import ColorScaling + + result = self._points_fc().plot(column="v", color=ColorScaling.power(gamma=0.7)) + assert isinstance(result, Axes), f"expected Axes, got {type(result)}" diff --git a/tests/netcdf/plot/test_hoisted_render_params.py b/tests/netcdf/plot/test_hoisted_render_params.py new file mode 100644 index 0000000000..0c3b71f715 --- /dev/null +++ b/tests/netcdf/plot/test_hoisted_render_params.py @@ -0,0 +1,233 @@ +"""Hoisted ``colorbar`` / ``points`` params on :meth:`NetCDF.plot`. + +The cleopatra-0.29 alignment lifts ``colorbar`` and ``points`` out of the opaque +``**kwargs`` and onto the ``NetCDF.plot`` signature (parity with ``Dataset.plot`` / +``DatasetCollection.plot``). Because ``NetCDF.plot`` is a multi-mode facade +(static / animate / facet) and cleopatra's facet path rejects ``points`` outright, +the hoisted params are forwarded **only when set** — a default ``None`` must never +reach the render call. These tests pin both the parity forwarding and that +conditional-injection contract. +""" + +from __future__ import annotations + +from unittest.mock import patch + +import numpy as np +import pytest + +from pyramids.netcdf import FacetSpec +from tests.netcdf.conftest import make_plot_3d_nc + +pytestmark = pytest.mark.plot + +pytest.importorskip("cleopatra", minversion="0.29", reason="needs cleopatra >= 0.29") +_cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") +_cleo_config.Config.set_matplotlib_backend("Agg") +_cleo_array = pytest.importorskip( + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" +) +ArrayGlyph = _cleo_array.ArrayGlyph +FacetGrid = _cleo_array.FacetGrid +ColorBar = _cleo_array.ColorBar +plt = pytest.importorskip("matplotlib.pyplot", reason="cleopatra not installed") + + +@pytest.fixture(autouse=True) +def _close_matplotlib_figures(): + """Close every figure after each test so the suite does not leak them. + + Yields: + None: control returns to the test; figures are closed on teardown. + """ + yield + plt.close("all") + + +def _points() -> np.ndarray: + """Build a minimal cleopatra point overlay for the 5x5 plot grid. + + Returns: + np.ndarray: A ``(2, 3)`` array of ``(value, row, col)`` rows whose row/col + indices fall inside the ``make_plot_3d_nc`` 5x5 raster. + """ + return np.array([[1.0, 1, 1], [2.0, 2, 2]], dtype="float64") + + +class TestNetCDFHoistedRenderParamsStatic: + """The hoisted ``colorbar`` / ``points`` params render on the static path. + + cleopatra rejects an unknown keyword, so a clean :class:`ArrayGlyph` proves the + param was forwarded to ``ArrayGlyph.plot`` rather than silently dropped. + """ + + def test_colorbar_spec_renders(self): + """A ``ColorBar`` spec on the static path renders to an ``ArrayGlyph``. + + Test scenario: + ``nc.plot(variable="t2m", colorbar=ColorBar(...))`` on a 3-D variable + returns an :class:`ArrayGlyph`, proving the hoisted ``colorbar`` reached + cleopatra. + """ + nc = make_plot_3d_nc() + glyph = nc.plot(variable="t2m", colorbar=ColorBar(label="temp")) + assert isinstance(glyph, ArrayGlyph), f"expected ArrayGlyph, got {type(glyph)}" + + def test_colorbar_false_renders(self): + """``colorbar=False`` suppresses the colour bar and still renders. + + Test scenario: + The boolean ``False`` form of the hoisted param forwards cleanly and + yields an :class:`ArrayGlyph`. + """ + nc = make_plot_3d_nc() + glyph = nc.plot(variable="t2m", colorbar=False) + assert isinstance(glyph, ArrayGlyph), f"expected ArrayGlyph, got {type(glyph)}" + + def test_points_overlay_renders(self): + """A ``points`` overlay on the static path renders to an ``ArrayGlyph``. + + Test scenario: + ``nc.plot(variable="t2m", points=<(2, 3) array>)`` forwards the hoisted + ``points`` param to cleopatra and returns an :class:`ArrayGlyph`. + """ + nc = make_plot_3d_nc() + glyph = nc.plot(variable="t2m", points=_points()) + assert isinstance(glyph, ArrayGlyph), f"expected ArrayGlyph, got {type(glyph)}" + + +class TestNetCDFHoistedParamConditionalInjection: + """Defaults are not injected; explicit values are. + + The forwarding is spied at ``Analysis.plot`` — the render boundary the facade + hands off to — to assert the presence/absence of the hoisted keys. + """ + + def test_defaults_not_injected(self): + """Omitted ``colorbar`` / ``points`` never reach the render call. + + Test scenario: + A bare ``nc.plot(variable="t2m")`` must not forward ``colorbar`` or + ``points`` (their default ``None`` is dropped), so the facet path — which + rejects ``points`` — is never handed a spurious key. + """ + nc = make_plot_3d_nc() + var = nc.get_variable("t2m") + with patch.object(type(var.analysis), "plot", autospec=True) as mock_plot: + mock_plot.return_value = "ok" + nc.plot(variable="t2m") + forwarded = mock_plot.call_args.kwargs + assert "points" not in forwarded, f"points must not be injected: {forwarded}" + assert "colorbar" not in forwarded, ( + f"colorbar must not be injected by default: {forwarded}" + ) + + def test_points_injected_when_set(self): + """An explicit ``points`` value is forwarded to the render call. + + Test scenario: + ``nc.plot(variable="t2m", points=)`` forwards ``points`` verbatim + to ``Analysis.plot``. + """ + nc = make_plot_3d_nc() + var = nc.get_variable("t2m") + pts = _points() + with patch.object(type(var.analysis), "plot", autospec=True) as mock_plot: + mock_plot.return_value = "ok" + nc.plot(variable="t2m", points=pts) + forwarded = mock_plot.call_args.kwargs + assert "points" in forwarded, f"points must be forwarded when set: {forwarded}" + assert forwarded["points"] is pts, "points must forward the exact array" + + +class TestNetCDFFacetPointsContract: + """The facet path: default ``points`` is fine; an explicit one is rejected. + + This is the exact contract the conditional injection exists to preserve — a + default ``None`` must not reach cleopatra's facet (which has no ``points`` + parameter), while an explicit ``points`` correctly surfaces the error. + """ + + def test_facet_without_points_renders(self): + """A facet plot with no ``points`` renders to a ``FacetGrid``. + + Test scenario: + ``nc.plot(variable="t2m", facet=FacetSpec(col="time"))`` must succeed — + the default ``points`` is not injected, so the facet path is not handed a + keyword it rejects. + """ + nc = make_plot_3d_nc(n_times=4) + grid = nc.plot(variable="t2m", facet=FacetSpec(col="time")) + assert isinstance(grid, FacetGrid), f"expected FacetGrid, got {type(grid)}" + + def test_facet_with_explicit_points_raises(self): + """An explicit ``points`` on the facet path raises a clear error. + + Test scenario: + cleopatra's facet has no ``points`` parameter, so + ``nc.plot(variable="t2m", facet=FacetSpec(col="time"), points=)`` + raises ``ValueError`` naming the offending keyword. + """ + nc = make_plot_3d_nc(n_times=4) + facet, pts = FacetSpec(col="time"), _points() + with pytest.raises(ValueError, match="points") as exc_info: + nc.plot(variable="t2m", facet=facet, points=pts) + assert "points" in str(exc_info.value), f"unexpected error: {exc_info.value}" + + +class TestNetCDFGroupParams: + """Typed render groups (color/contour/cells/data_style) on ``NetCDF.plot``.""" + + def test_group_reaches_the_render_call(self): + """An explicit ``color`` forwards through to ``Analysis.plot``. + + Test scenario: + ``nc.plot(variable="t2m", color=ColorScaling(...))`` forwards the spec down + the facade -> NetCDFPlot.run -> Analysis.plot chain. + """ + from cleopatra.styling.scaling import ColorScaling + + nc = make_plot_3d_nc() + var = nc.get_variable("t2m") + color = ColorScaling.power(gamma=0.8) + with patch.object(type(var.analysis), "plot", autospec=True) as mock_plot: + mock_plot.return_value = "ok" + nc.plot(variable="t2m", color=color) + assert mock_plot.call_args.kwargs.get("color") is color, ( + "explicit color must reach Analysis.plot" + ) + + def test_unset_groups_not_forwarded(self): + """A bare plot forwards none of the typed render groups.""" + nc = make_plot_3d_nc() + var = nc.get_variable("t2m") + with patch.object(type(var.analysis), "plot", autospec=True) as mock_plot: + mock_plot.return_value = "ok" + nc.plot(variable="t2m") + forwarded = mock_plot.call_args.kwargs + for name in ("color", "contour", "cells", "data_style"): + assert name not in forwarded, f"unset {name} must not be forwarded" + + def test_explicit_contour_wins_over_coloropts_levels(self): + """An explicit ``contour=`` beats a ``ColorOpts(levels=...)`` (no silent override). + + Test scenario: + ``NetCDF.plot`` exposes both the hoisted ``contour=`` group and the older + ``ColorOpts(levels=...)`` bag. When both are given the caller's explicit + ``contour`` must win — the ColorOpts-derived one only fills an unset slot. + """ + from cleopatra.styling.params import Contour + + from pyramids.netcdf import ColorOpts + + nc = make_plot_3d_nc() + var = nc.get_variable("t2m") + explicit = Contour(levels=[1, 2, 3]) + with patch.object(type(var.analysis), "plot", autospec=True) as mock_plot: + mock_plot.return_value = "ok" + nc.plot(variable="t2m", contour=explicit, colour=ColorOpts(levels=9)) + forwarded = mock_plot.call_args.kwargs.get("contour") + assert forwarded is explicit, f"explicit contour must win; got {forwarded}" + assert forwarded.levels == [1, 2, 3], ( + f"the winning contour must keep its levels; got {forwarded.levels}" + ) diff --git a/tests/netcdf/plot/test_plot_animate.py b/tests/netcdf/plot/test_plot_animate.py index f85fb40c66..7f4af292e9 100644 --- a/tests/netcdf/plot/test_plot_animate.py +++ b/tests/netcdf/plot/test_plot_animate.py @@ -16,7 +16,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph # cleopatra >= 0.26 bundles the animate frame-label pair into FrameLabel. diff --git a/tests/netcdf/plot/test_plot_coords.py b/tests/netcdf/plot/test_plot_coords.py index a07c62cda3..731d07ee0c 100644 --- a/tests/netcdf/plot/test_plot_coords.py +++ b/tests/netcdf/plot/test_plot_coords.py @@ -23,7 +23,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") @@ -360,7 +360,7 @@ def test_kind_bogus_raises_value_error(self): Test scenario: cleopatra validates ``kind`` against - :data:`cleopatra.array_glyph.VALID_PLOT_KINDS`. An unknown + :data:`cleopatra.glyphs.gridded.array_glyph.VALID_PLOT_KINDS`. An unknown value triggers a ValueError that must propagate through pyramids unchanged so users see the same error message they would see calling ArrayGlyph directly. diff --git a/tests/netcdf/plot/test_plot_facet.py b/tests/netcdf/plot/test_plot_facet.py index 3ad5898df4..3ce2c1aedd 100644 --- a/tests/netcdf/plot/test_plot_facet.py +++ b/tests/netcdf/plot/test_plot_facet.py @@ -15,7 +15,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) _FacetGrid = _cleo_array.FacetGrid _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") @@ -28,7 +28,7 @@ class TestNetCDFPlotFaceting: Each test exercises the cleopatra `ArrayGlyph.facet` wiring on NetCDF.plot. The returned object is a - :class:`cleopatra.array_glyph.FacetGrid`; subplot count and the + :class:`cleopatra.glyphs.gridded.array_glyph.FacetGrid`; subplot count and the `name_dicts` index map confirm the dispatch. """ diff --git a/tests/netcdf/plot/test_plot_lazy.py b/tests/netcdf/plot/test_plot_lazy.py index 7f65ef7acc..b189aae40c 100644 --- a/tests/netcdf/plot/test_plot_lazy.py +++ b/tests/netcdf/plot/test_plot_lazy.py @@ -16,7 +16,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") diff --git a/tests/netcdf/plot/test_plot_options.py b/tests/netcdf/plot/test_plot_options.py index f5563196ab..ef2883b1e0 100644 --- a/tests/netcdf/plot/test_plot_options.py +++ b/tests/netcdf/plot/test_plot_options.py @@ -213,14 +213,13 @@ def test_facet_none_takes_static_path(self): assert "_facet_stack" not in mock_plot.call_args.kwargs def test_style_and_hillshade_forwarded_to_engine(self): - """``ColorOpts(style=..., hillshade=...)`` forwards both flattened (#737). + """``ColorOpts(style=..., hillshade=...)`` forwards a ``DataStyle`` group (#737). Test scenario: - The cleopatra data-style preset name and the hillshade blend - are ``ColorOpts`` fields; the dataclass unpacking inside - ``NetCDF.plot`` must hand the engine flat ``style=`` / - ``hillshade=`` kwargs (the raster paths already auto-route - them to the glyph via ``render_array``). + The cleopatra data-style preset name and the hillshade blend are + ``ColorOpts`` fields; cleopatra 0.30 moved them onto the typed + ``DataStyle`` group, so the unpacking inside ``NetCDF.plot`` must hand the + engine a single ``data_style=DataStyle(style=..., hillshade=...)`` kwarg. """ nc = make_plot_3d_nc() var = nc.get_variable("t2m") @@ -231,8 +230,8 @@ def test_style_and_hillshade_forwarded_to_engine(self): colour=ColorOpts(style="flow_accumulation", hillshade={"vert_exag": 8}), ) forwarded = mock_plot.call_args.kwargs - assert forwarded["style"] == "flow_accumulation" - assert forwarded["hillshade"] == {"vert_exag": 8} + assert forwarded["data_style"].style == "flow_accumulation" + assert forwarded["data_style"].hillshade == {"vert_exag": 8} def test_style_and_hillshade_default_not_forwarded(self): """An unset ``style`` / ``hillshade`` leaks no kwarg to the engine. @@ -252,15 +251,15 @@ def test_style_and_hillshade_default_not_forwarded(self): assert "hillshade" not in forwarded def test_style_and_hillshade_survive_animate(self): - """``style`` / ``hillshade`` reach the animate render call (#737). + """The ``data_style`` group reaches the animate render call (#737). Test scenario: The animate path drops the static-only kwargs listed in ``_ANIMATE_DROP_KWARGS`` before calling ``render_array`` with - ``mode="animate"``. ``style`` / ``hillshade`` are *not* in - that drop-set, so a ``ColorOpts(style=..., hillshade=...)`` - must survive into the animated frames (cleopatra >= 0.24 - shades every frame). + ``mode="animate"``. The ``data_style`` group built from a + ``ColorOpts(style=..., hillshade=...)`` is *not* in that drop-set, so it + must survive into the animated frames (cleopatra >= 0.24 shades every + frame). """ nc = make_plot_3d_nc(n_times=3) captured: dict = {} @@ -274,5 +273,5 @@ def test_style_and_hillshade_survive_animate(self): colour=ColorOpts(style="topography", hillshade=True), ) assert captured["kw"]["mode"] == "animate" - assert captured["kw"]["style"] == "topography" - assert captured["kw"]["hillshade"] is True + assert captured["kw"]["data_style"].style == "topography" + assert captured["kw"]["data_style"].hillshade is True diff --git a/tests/netcdf/plot/test_plot_render.py b/tests/netcdf/plot/test_plot_render.py index a0e3361869..5f169be787 100644 --- a/tests/netcdf/plot/test_plot_render.py +++ b/tests/netcdf/plot/test_plot_render.py @@ -18,7 +18,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") @@ -57,7 +57,12 @@ def test_robust_default_not_forwarded(self): assert "robust" not in mock_plot.call_args.kwargs def test_levels_extend_cbar_kwargs_forwarded(self): - """`levels=`, `extend=`, and `cbar_kwargs=` reach the renderer verbatim.""" + """`levels` folds into a `Contour` group; `extend`/`cbar_kwargs` stay loose. + + cleopatra 0.30 moved `levels` onto the typed `contour=Contour(levels=...)` + group, so `NetCDF.plot` builds that from `ColourOpts.levels`; `extend` and the + raw `cbar_kwargs` passthrough are still forwarded verbatim. + """ nc = make_plot_3d_nc() var = nc.get_variable("t2m") cbar = {"label": "test"} @@ -68,7 +73,7 @@ def test_levels_extend_cbar_kwargs_forwarded(self): colour=ColourOpts(levels=5, extend="both", cbar_kwargs=cbar), ) kw = mock_plot.call_args.kwargs - assert kw.get("levels") == 5 + assert kw.get("contour").levels == 5 assert kw.get("extend") == "both" assert kw.get("cbar_kwargs") == cbar diff --git a/tests/netcdf/plot/test_plot_selectors.py b/tests/netcdf/plot/test_plot_selectors.py index 3c4a162197..5073bf40e0 100644 --- a/tests/netcdf/plot/test_plot_selectors.py +++ b/tests/netcdf/plot/test_plot_selectors.py @@ -20,7 +20,7 @@ pytestmark = pytest.mark.plot _cleo_array = pytest.importorskip( - "cleopatra.array_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.array_glyph", reason="cleopatra not installed" ) ArrayGlyph = _cleo_array.ArrayGlyph _cleo_config = pytest.importorskip("cleopatra.config", reason="cleopatra not installed") diff --git a/tests/ugrid/test_plot.py b/tests/ugrid/test_plot.py index 3553a2d8fc..2c1b97f621 100644 --- a/tests/ugrid/test_plot.py +++ b/tests/ugrid/test_plot.py @@ -12,10 +12,9 @@ import pytest mesh_glyph = pytest.importorskip( - "cleopatra.mesh_glyph", reason="cleopatra not installed" + "cleopatra.glyphs.gridded.mesh_glyph", reason="cleopatra not installed" ) MeshGlyph = mesh_glyph.MeshGlyph -from pyramids.base._errors import OptionalPackageDoesNotExist from pyramids.netcdf.ugrid.dataset import UgridDataset from pyramids.netcdf.ugrid.plot import plot_mesh_data, plot_mesh_outline @@ -64,6 +63,25 @@ def test_plot_sets_mappable_im(self, triangle_mesh): result = plot_mesh_data(triangle_mesh, data, location="face") assert result.im is not None, "plot() must set the mesh mappable on .im" + def test_typed_colorbar_renders_on_mesh(self, triangle_mesh): + """A `ColorBar` spec styles the mesh colour bar (cleopatra >= 0.29, #933). + + Test scenario: + cleopatra 0.29 accepts `colorbar=ColorBar` on `MeshGlyph.plot`, so passing + a typed spec draws a labelled colour bar rather than only accepting `bool`. + """ + from cleopatra.styling.colorbar import ColorBar + + data = np.array([1.0, 2.0]) + result = plot_mesh_data( + triangle_mesh, data, location="face", colorbar=ColorBar(label="depth") + ) + assert result._cbar is not None, "a ColorBar spec must draw a colour bar" + label = result._cbar.ax.get_ylabel() or result._cbar.ax.get_xlabel() + assert label == "depth", ( + f"mesh colour bar should carry the label, got {label!r}" + ) + def test_invalid_location_raises(self, triangle_mesh): """Test that invalid location raises ValueError.""" data = np.array([1.0]) @@ -258,68 +276,81 @@ def _dataset(location="face"): not _mesh_supports_style, reason="cleopatra < 0.24 has no MeshGlyph style" ) def test_dataset_plot_style_renders(self): - """``UgridDataset.plot(style=...)`` renders a styled MeshGlyph.""" - result = self._dataset("face").plot("depth", style="flow_accumulation") + """``UgridDataset.plot(data_style=DataStyle(style=...))`` renders a styled mesh.""" + from cleopatra.styling.params import DataStyle + + result = self._dataset("face").plot( + "depth", data_style=DataStyle(style="flow_accumulation") + ) assert isinstance(result, MeshGlyph) @pytest.mark.skipif( not _mesh_supports_style, reason="cleopatra < 0.24 has no MeshGlyph hillshade" ) def test_dataset_plot_hillshade_node_renders(self): - """``hillshade=`` renders on node-centered mesh data. + """``data_style=DataStyle(hillshade=True)`` renders on node-centered mesh data. cleopatra requires node-centered elevation for hillshade, so a node-location variable is used. """ - result = self._dataset("node").plot("depth", hillshade=True) + from cleopatra.styling.params import DataStyle + + result = self._dataset("node").plot( + "depth", data_style=DataStyle(hillshade=True) + ) assert isinstance(result, MeshGlyph) @pytest.mark.skipif( not _mesh_supports_style, reason="cleopatra < 0.24 has no MeshGlyph style" ) def test_style_and_hillshade_reach_mesh_glyph_plot(self): - """The presets are actually applied to ``MeshGlyph.plot`` (not just returned). + """The ``data_style`` group is actually applied to ``MeshGlyph.plot``. Test scenario: - ``isinstance(result, MeshGlyph)`` alone would still pass if the preset + ``isinstance(result, MeshGlyph)`` alone would still pass if the group were silently dropped between ``mesh_render`` and the glyph. Spy on - ``MeshGlyph.plot`` and assert ``style`` / ``hillshade`` arrive in its - call kwargs, proving the full facade -> mesh_render -> plot_mesh_data - -> MeshGlyph.plot leg forwards them. + ``MeshGlyph.plot`` and assert the ``data_style`` group arrives in its call + kwargs, proving the full facade -> mesh_render -> plot_mesh_data -> + MeshGlyph.plot leg forwards it. """ + from cleopatra.styling.params import DataStyle + ds = self._dataset("face") with patch.object(MeshGlyph, "plot") as mock_plot: - ds.plot("depth", style="flow_accumulation", hillshade=True) + ds.plot( + "depth", + data_style=DataStyle(style="flow_accumulation", hillshade=True), + ) kw = mock_plot.call_args.kwargs - assert kw.get("style") == "flow_accumulation" - assert kw.get("hillshade") is True + data_style = kw.get("data_style") + assert data_style is not None, "the data_style group must reach MeshGlyph.plot" + assert data_style.style == "flow_accumulation" + assert data_style.hillshade is True + + def test_data_style_forwarded_to_mesh_render(self): + """``UgridDataset.plot`` forwards a ``data_style=DataStyle`` to the helper. + + The loose ``style`` / ``hillshade`` kwargs were removed (they now raise); the + typed ``data_style`` group is what reaches ``_mesh_render``. + """ + from cleopatra.styling.params import DataStyle - def test_style_forwarded_to_mesh_render(self): - """``UgridDataset.plot`` forwards ``style`` / ``hillshade`` to the helper.""" ds = self._dataset("face") + data_style = DataStyle(style="topography", hillshade=True) with patch( "pyramids.netcdf.ugrid.dataset._mesh_render", return_value="sentinel" ) as mock_render: - ds.plot("depth", style="topography", hillshade=True) - kw = mock_render.call_args.kwargs - assert kw.get("style") == "topography" - assert kw.get("hillshade") is True - - def test_style_on_old_cleopatra_raises_upgrade_hint(self): - """``style=`` on a MeshGlyph lacking preset support raises the >= 0.24 hint.""" - ds = self._dataset("face") - old_keys = MeshGlyph.option_keys() - {"style", "hillshade"} - with patch.object(MeshGlyph, "option_keys", return_value=old_keys): - with pytest.raises(OptionalPackageDoesNotExist, match="cleopatra >= 0.24"): - ds.plot("depth", style="topography") + ds.plot("depth", data_style=data_style) + assert mock_render.call_args.kwargs.get("data_style") is data_style - def test_falsy_hillshade_not_guarded_on_old_cleopatra(self): - """``hillshade=False`` is dropped, so the mesh guard does not fire.""" + def test_falsy_hillshade_is_dropped(self): + """``hillshade=False`` is dropped — no ``DataStyle`` is built for it.""" ds = self._dataset("face") - old_keys = MeshGlyph.option_keys() - {"style", "hillshade"} - with patch.object(MeshGlyph, "option_keys", return_value=old_keys): - result = ds.plot("depth", hillshade=False) - assert isinstance(result, MeshGlyph) + with patch.object(MeshGlyph, "plot") as mock_plot: + ds.plot("depth", hillshade=False) + assert mock_plot.call_args.kwargs.get("data_style") is None, ( + "a falsy hillshade must not build a DataStyle" + ) @pytest.mark.skipif( not _mesh_supports_apply_style, @@ -334,7 +365,118 @@ def test_returned_mesh_glyph_supports_apply_style(self): preset by name without rebuilding — verify the round trip through the ``UgridDataset.plot`` facade. """ - result = self._dataset("face").plot("depth", style="flow_accumulation") + from cleopatra.styling.params import DataStyle + + result = self._dataset("face").plot( + "depth", data_style=DataStyle(style="flow_accumulation") + ) assert result.style == "flow_accumulation" result.apply_style("bathymetry") assert result.style == "bathymetry", "apply_style must restyle in place" + + +class TestUgridPlotAlignment: + """The raster-family plot params (colorbar/points/kind) on ``UgridDataset.plot``. + + ``UgridDataset.plot`` shares the raster plot signature; ``colorbar`` maps onto the + mesh backend while ``points`` / ``kind`` have no mesh meaning and are accepted but + ignored. + """ + + @staticmethod + def _dataset(location="face"): + """Build a single-face UgridDataset carrying a ``depth`` variable.""" + data = np.array([5.0]) if location == "face" else np.array([0.0, 1.0, 2.0]) + return UgridDataset.create_from_arrays( + node_x=np.array([0.0, 1.0, 0.5]), + node_y=np.array([0.0, 0.0, 1.0]), + face_node_connectivity=np.array([[0, 1, 2]]), + data={"depth": data}, + data_locations={"depth": location}, + ) + + def test_colorbar_spec_reaches_mesh_glyph_plot(self): + """An explicit ``colorbar`` forwards through to ``MeshGlyph.plot``. + + Test scenario: + ``UgridDataset.plot(colorbar=ColorBar(...))`` must forward the spec down + the facade -> mesh_render -> plot_mesh_data -> ``MeshGlyph.plot`` chain. + """ + from cleopatra.styling.colorbar import ColorBar + + bar = ColorBar(label="depth") + with patch.object(MeshGlyph, "plot") as mock_plot: + self._dataset("face").plot("depth", colorbar=bar) + assert mock_plot.call_args.kwargs.get("colorbar") is bar, ( + "an explicit colorbar must reach MeshGlyph.plot" + ) + + def test_default_colorbar_preserves_mesh_default(self): + """A default ``colorbar=None`` leaves the mesh back-end default in place. + + Test scenario: + Omitting ``colorbar`` must not override ``plot_mesh_data``'s default (which + draws the bar), so ``MeshGlyph.plot`` still receives the drawn-bar default + rather than a pyramids-injected ``None``. + """ + with patch.object(MeshGlyph, "plot") as mock_plot: + self._dataset("face").plot("depth") + assert mock_plot.call_args.kwargs.get("colorbar") is True, ( + "the mesh default (bar drawn) must survive an omitted colorbar" + ) + + def test_explicit_false_colorbar_reaches_mesh_glyph_plot(self): + """An explicit ``colorbar=False`` hides the bar via ``MeshGlyph.plot``.""" + with patch.object(MeshGlyph, "plot") as mock_plot: + self._dataset("face").plot("depth", colorbar=False) + assert mock_plot.call_args.kwargs.get("colorbar") is False, ( + "colorbar=False must reach MeshGlyph.plot" + ) + + def test_points_and_kind_are_accepted_noops(self): + """``points`` / ``kind`` are accepted for symmetry and ignored for a mesh. + + Test scenario: + Passing the raster-only ``points`` / ``kind`` must neither raise nor reach + ``MeshGlyph.plot`` (the mesh has no point overlay and a fixed renderer). + """ + with patch.object(MeshGlyph, "plot") as mock_plot: + self._dataset("face").plot( + "depth", points=np.array([[1.0, 0, 0]]), kind="contourf" + ) + kw = mock_plot.call_args.kwargs + assert "points" not in kw, "points must not reach MeshGlyph.plot" + assert "kind" not in kw, "kind must not reach MeshGlyph.plot" + + +class TestUgridGroupParams: + """Typed render groups (color/contour/data_style) on ``UgridDataset.plot``. + + ``MeshGlyph.plot`` accepts ``color`` / ``contour`` / ``data_style`` (no ``cells``); + the facade forwards them when set, and an explicit group wins over the loose kwargs. + """ + + @staticmethod + def _dataset(location="face"): + """Build a single-face UgridDataset carrying a ``depth`` variable.""" + data = np.array([5.0]) if location == "face" else np.array([0.0, 1.0, 2.0]) + return UgridDataset.create_from_arrays( + node_x=np.array([0.0, 1.0, 0.5]), + node_y=np.array([0.0, 0.0, 1.0]), + face_node_connectivity=np.array([[0, 1, 2]]), + data={"depth": data}, + data_locations={"depth": location}, + ) + + def test_color_and_data_style_reach_mesh_glyph_plot(self): + """Explicit ``color`` / ``data_style`` forward to ``MeshGlyph.plot``.""" + from cleopatra.styling.params import DataStyle + from cleopatra.styling.scaling import ColorScaling + + color = ColorScaling.power(gamma=0.8) + data_style = DataStyle(style="flow_accumulation") + with patch.object(MeshGlyph, "plot") as mock_plot: + self._dataset("face").plot("depth", color=color, data_style=data_style) + kw = mock_plot.call_args.kwargs + assert kw.get("color") is color, "explicit color must reach MeshGlyph.plot" + assert kw.get("data_style") is data_style, "explicit data_style must reach it"