diff --git a/pyproject.toml b/pyproject.toml index f8a4c7b2..efa25ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,9 @@ classifiers = [ "Topic :: Scientific/Engineering", ] +[project.optional-dependencies] +matplotlib = ["matplotlib>=3.9.0"] + [project.urls] Documentation = "https://uriahf.github.io/rtichoke_python/" "Python Tutorials" = "https://uriahf.github.io/rtichoke_blog_python/" @@ -48,7 +51,6 @@ dev = [ "jupyter<2.0.0,>=1.0.0", "pytest-cov<5.0.0,>=4.0.0", "pytest<8.0.0,>=7.3.0", - "pyzmq<27.0.0,>=26.3.0", "ruff>=0.11.0", "ipykernel>=6.29.5", "lifelines>=0.30.0", diff --git a/src/rtichoke/_renderers.py b/src/rtichoke/_renderers.py new file mode 100644 index 00000000..983da1cf --- /dev/null +++ b/src/rtichoke/_renderers.py @@ -0,0 +1,171 @@ +"""Renderer selection for canonical rtichoke visualization specifications.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from importlib.resources import files +from pathlib import Path +from typing import Any, Literal + +Renderer = Literal["plotly", "matplotlib", "browser", "rtichoke_viz"] + +_SUPPORTED_RENDERERS = ("plotly", "matplotlib", "browser", "rtichoke_viz") + + +def _validate_renderer(renderer: str) -> Renderer: + """Validate and normalize a public renderer name.""" + if renderer not in _SUPPORTED_RENDERERS: + supported = ", ".join(repr(name) for name in _SUPPORTED_RENDERERS) + raise ValueError( + f"Unsupported renderer {renderer!r}. Supported renderers are: {supported}." + ) + return renderer # type: ignore[return-value] + + +@dataclass(frozen=True) +class RtichokeBrowserChart: + """A canonical v2 chart that can be written for offline browser rendering.""" + + spec: dict[str, Any] + size: int = 600 + + def write_html(self, path: str | Path) -> Path: + """Write an offline HTML page plus its packaged renderer assets. + + Parameters + ---------- + path : str or pathlib.Path + Destination for the HTML page. + + Returns + ------- + pathlib.Path + The written HTML path. + """ + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + vendor = files("rtichoke").joinpath("_vendor", "rtichoke_viz") + for asset in ("rtichoke-viz.js", "rtichoke-viz.css"): + (output.parent / asset).write_bytes(vendor.joinpath(asset).read_bytes()) + + render_export = { + "roc": "renderRocV2", + "calibration": "renderCalibrationV2", + "precision_recall": "renderPrecisionRecallV2", + "gains": "renderGainsV2", + }.get(str(self.spec.get("type"))) + if render_export is None: + raise ValueError( + f"rtichoke_viz does not support chart type {self.spec.get('type')!r}." + ) + + spec_json = json.dumps(self.spec, separators=(",", ":")).replace(" + + + + + + rtichoke {self.spec.get("type")} chart + + +
+ + + + +""" + output.write_text(html, encoding="utf-8") + return output + + +def _render_gains_matplotlib( + spec: dict[str, Any], *, size: int, color_values: list[str] +) -> Any: + """Render canonical gains quantities with an optional Matplotlib backend.""" + try: + from matplotlib.figure import Figure + except ImportError as error: + raise ImportError( + "The 'matplotlib' renderer requires the optional matplotlib dependency. " + "Install it with `pip install 'rtichoke[matplotlib]'`." + ) from error + + figure = Figure(figsize=(size / 100, size / 100), dpi=100) + axis = figure.subplots() + references = spec.get("references", []) + assert isinstance(references, list) + for reference in references: + if not isinstance(reference, dict): + continue + if reference.get("type") == "identity": + x_values, y_values = [0, 1], [0, 1] + elif reference.get("type") == "path": + points = reference.get("points", []) + x_values = [point["x"] for point in points] + y_values = [point["y"] for point in points] + else: + continue + axis.plot( + x_values, + y_values, + color="#BEBEBE", + linestyle="--", + linewidth=2, + ) + + series = spec.get("series", []) + data = spec.get("data", []) + assert isinstance(series, list) and isinstance(data, list) + display_groups = list(dict.fromkeys(item["display"]["group"] for item in series)) + colors = { + group: ( + "black" + if len(display_groups) == 1 + else color_values[index % len(color_values)] + ) + for index, group in enumerate(display_groups) + } + for item in series: + rows = [row for row in data if row["seriesId"] == item["id"]] + display = item["display"] + axis.plot( + [row["ppcr"] for row in rows], + [row["sensitivity"] for row in rows], + label=display["label"], + color=colors[display["group"]], + linewidth=2, + ) + + x_axis = spec["xAxis"] + y_axis = spec["yAxis"] + axis.set_xlabel(x_axis["label"]) + axis.set_ylabel(y_axis["label"]) + axis.set_xlim(*x_axis["domain"]) + axis.set_ylim(*y_axis["domain"]) + if len(series) > 1: + axis.legend() + figure.tight_layout() + return figure + + +def _render_gains_v2( + spec: dict[str, Any], + *, + renderer: str, + size: int, + color_values: list[str], +) -> Any: + """Render a canonical gains v2 spec with a non-default backend.""" + selected = _validate_renderer(renderer) + if selected == "matplotlib": + return _render_gains_matplotlib(spec, size=size, color_values=color_values) + if selected in {"browser", "rtichoke_viz"}: + return RtichokeBrowserChart(spec=spec, size=size) + raise ValueError("The Plotly renderer must use the existing production path.") diff --git a/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM b/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM index 8407263a..df5ec55d 100644 --- a/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM +++ b/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM @@ -1,6 +1,5 @@ repository=https://github.com/uriahf/rtichoke_viz -release=v0.2.0 -source_commit=45dc109a6a0679d0f8f3f9452d8de9306a89b906 -archive=rtichoke-viz-0.2.0.tar.gz -sha256=3861277c01b3983f8b344a9ee0237c7d09fd0ba4c3d1e0cce489962e7b559d9f -checksum=rtichoke-viz-0.2.0.tar.gz.sha256 +release=v0.3.0 +source_commit=aca9188ea856167557efb20980a0b43e0481b8c8 +archive=rtichoke-viz-0.3.0.tar.gz +sha256=558f8d9e16f9544659b84e33f72511065163291a1b97a3c5511b61d1e1f0cac1 diff --git a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-0.3.0.tar.gz b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-0.3.0.tar.gz new file mode 100644 index 00000000..4962a386 Binary files /dev/null and b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-0.3.0.tar.gz differ diff --git a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json index 10293695..95a21a72 100644 --- a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json +++ b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json @@ -160,37 +160,98 @@ "items": { "anyOf": [ { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -207,37 +268,98 @@ ] }, { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -258,37 +380,98 @@ ] }, { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -531,37 +714,98 @@ "items": { "anyOf": [ { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -578,37 +822,98 @@ ] }, { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -629,37 +934,98 @@ ] }, { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -950,37 +1316,98 @@ "items": { "anyOf": [ { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -997,37 +1424,98 @@ ] }, { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] + } }, - "value": { - "type": "number" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -1048,37 +1536,98 @@ ] }, { - "type": "object", "allOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { "const": "identity", "type": "string" }, - { + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { "const": "horizontal", "type": "string" }, - { - "const": "vertical", + "value": { + "type": "number" + }, + "label": { "type": "string" } - ] - }, - "value": { - "type": "number" + } }, - "label": { - "type": "string" + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } } - } + ] }, { "type": "object", @@ -1162,6 +1711,560 @@ } } ] + }, + { + "type": "object", + "allOf": [ + { + "type": "object", + "required": [ + "schemaVersion", + "evaluations", + "series", + "xAxis", + "yAxis" + ], + "properties": { + "schemaVersion": { + "const": "2.0", + "type": "string" + }, + "title": { + "type": "string" + }, + "evaluations": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "population" + ], + "properties": { + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "population": { + "type": "string" + }, + "label": { + "type": "string" + } + } + } + }, + "series": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "evaluationId", + "display" + ], + "properties": { + "id": { + "type": "string" + }, + "evaluationId": { + "type": "string" + }, + "horizon": { + "minimum": 0, + "type": "number" + }, + "display": { + "type": "object", + "required": [ + "label", + "group", + "role" + ], + "properties": { + "label": { + "type": "string" + }, + "group": { + "type": "string" + }, + "role": { + "anyOf": [ + { + "const": "model", + "type": "string" + }, + { + "const": "population", + "type": "string" + }, + { + "const": "evaluation", + "type": "string" + }, + { + "const": "context", + "type": "string" + } + ] + } + } + } + } + } + }, + "xAxis": { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + }, + "domain": { + "type": "array", + "items": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "additionalItems": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "yAxis": { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + }, + "domain": { + "type": "array", + "items": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "additionalItems": false, + "minItems": 2, + "maxItems": 2 + } + } + }, + "references": { + "type": "array", + "items": { + "anyOf": [ + { + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "const": "identity", + "type": "string" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "horizontal", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } + } + ] + }, + { + "type": "object", + "required": [ + "scope" + ], + "properties": { + "scope": { + "const": "global", + "type": "string" + } + } + } + ] + }, + { + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "const": "identity", + "type": "string" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "horizontal", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } + } + ] + }, + { + "type": "object", + "required": [ + "scope", + "population" + ], + "properties": { + "scope": { + "const": "population", + "type": "string" + }, + "population": { + "type": "string" + } + } + } + ] + }, + { + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "const": "identity", + "type": "string" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "horizontal", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "vertical", + "type": "string" + }, + "value": { + "type": "number" + }, + "label": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "points" + ], + "properties": { + "type": { + "const": "path", + "type": "string" + }, + "points": { + "minItems": 2, + "type": "array", + "items": { + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + } + }, + "label": { + "type": "string" + } + } + } + ] + }, + { + "type": "object", + "required": [ + "scope", + "population", + "horizon" + ], + "properties": { + "scope": { + "const": "population_horizon", + "type": "string" + }, + "population": { + "type": "string" + }, + "horizon": { + "minimum": 0, + "type": "number" + } + } + } + ] + } + ] + } + } + } + }, + { + "type": "object", + "required": [ + "type", + "data", + "x", + "y" + ], + "properties": { + "type": { + "const": "gains", + "type": "string" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "seriesId", + "cutoff", + "ppcr", + "sensitivity" + ], + "properties": { + "seriesId": { + "type": "string" + }, + "cutoff": { + "type": "number" + }, + "ppcr": { + "minimum": 0, + "maximum": 1, + "type": "number" + }, + "sensitivity": { + "minimum": 0, + "maximum": 1, + "type": "number" + } + } + } + }, + "x": { + "const": "ppcr", + "type": "string" + }, + "y": { + "const": "sensitivity", + "type": "string" + } + } + } + ] } ] } diff --git a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js index 24cba4b3..3ecf24bb 100644 --- a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js +++ b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js @@ -2715,15 +2715,31 @@ var SeriesSpecSchema = Type.Object({ horizon: Type.Optional(Type.Number({ minimum: 0 })), display: DisplayGroupingSpecSchema }); -var ReferenceGeometrySchema = Type.Object({ - type: Type.Union([ - Type.Literal("identity"), - Type.Literal("horizontal"), - Type.Literal("vertical") - ]), - value: Type.Optional(Type.Number()), - label: Type.Optional(Type.String()) +var ReferencePointSchema = Type.Object({ + x: Type.Number(), + y: Type.Number() }); +var ReferenceGeometrySchema = Type.Union([ + Type.Object({ + type: Type.Literal("identity"), + label: Type.Optional(Type.String()) + }), + Type.Object({ + type: Type.Literal("horizontal"), + value: Type.Number(), + label: Type.Optional(Type.String()) + }), + Type.Object({ + type: Type.Literal("vertical"), + value: Type.Number(), + label: Type.Optional(Type.String()) + }), + Type.Object({ + type: Type.Literal("path"), + points: Type.Array(ReferencePointSchema, { minItems: 2 }), + label: Type.Optional(Type.String()) + }) +]); var GlobalReferenceLineSpecSchema = Type.Intersect([ ReferenceGeometrySchema, Type.Object({ scope: Type.Literal("global") }) @@ -2784,6 +2800,23 @@ var CalibrationV2SpecSchema = Type.Intersect([ }) ]); +// src/spec/v2/gains.ts +var GainsV2DatumSchema = Type.Object({ + seriesId: Type.String(), + cutoff: Type.Number(), + ppcr: Type.Number({ minimum: 0, maximum: 1 }), + sensitivity: Type.Number({ minimum: 0, maximum: 1 }) +}); +var GainsV2SpecSchema = Type.Intersect([ + BaseChartV2SpecSchema, + Type.Object({ + type: Type.Literal("gains"), + data: Type.Array(GainsV2DatumSchema), + x: Type.Literal("ppcr"), + y: Type.Literal("sensitivity") + }) +]); + // src/spec/v2/precision_recall.ts var PrecisionRecallV2DatumSchema = Type.Object({ seriesId: Type.String(), @@ -2820,7 +2853,12 @@ var RocV2SpecSchema = Type.Intersect([ // src/spec/v2/chart.ts var RtichokeChartSpecV2Schema = Type.Union( - [RocV2SpecSchema, CalibrationV2SpecSchema, PrecisionRecallV2SpecSchema], + [ + RocV2SpecSchema, + CalibrationV2SpecSchema, + PrecisionRecallV2SpecSchema, + GainsV2SpecSchema + ], { $id: "https://rtichoke.dev/schema/viz/2.0.json", title: "rtichoke visualization specification v2" @@ -18605,6 +18643,18 @@ var BASE_STYLE2 = { fontFamily: "Arial, Helvetica, sans-serif", fontSize: "13px" }; +function resolveV2RenderOptions(groupCount, options = {}) { + const width = options.width ?? 600; + const height = options.height ?? 600; + if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) { + throw new Error("Renderer width and height must be positive finite numbers"); + } + const colors = groupCount <= 1 ? ["#000000"] : [...options.colors ?? RTICHOKE_COLORS3]; + if (colors.length < groupCount) { + throw new Error("Renderer colors must contain at least one color per display group"); + } + return { width, height, colors: colors.slice(0, Math.max(groupCount, 1)) }; +} function displayBySeries(spec) { return new Map(spec.series.map((series) => [series.id, series.display])); } @@ -18616,31 +18666,39 @@ function seriesRenderData(spec, data) { label: displays.get(datum2.seriesId).label })); } +function referenceMarks(spec) { + const marks2 = []; + for (const reference of spec.references ?? []) { + if (reference.type === "identity") { + marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { + x: "x", + y: "y", + stroke: "#BEBEBE", + strokeWidth: 2, + strokeDasharray: "4,4" + })); + } else if (reference.type === "path" && reference.points) { + marks2.push(line(reference.points, { + x: "x", + y: "y", + stroke: "#BEBEBE", + strokeWidth: 2, + strokeDasharray: "4,4" + })); + } + } + return marks2; +} function renderRocV2(spec) { assertV2ReferentialIntegrity(spec); const groups2 = [...new Set(spec.series.map((series) => series.display.group))]; const showLegend = groups2.length > 1; - const data = seriesRenderData(spec, spec.data).map((datum2) => ({ - ...datum2, - false_positive_rate: 1 - datum2.specificity - })); + const data = seriesRenderData(spec, spec.data).map((datum2) => ({ ...datum2, false_positive_rate: 1 - datum2.specificity })); const marks2 = []; if (spec.references?.some((reference) => reference.type === "identity")) { - marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { - x: "x", - y: "y", - stroke: "#BEBEBE", - strokeWidth: 2 - })); + marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { x: "x", y: "y", stroke: "#BEBEBE", strokeWidth: 2 })); } - marks2.push(line(data, { - x: "false_positive_rate", - y: "sensitivity", - z: "seriesId", - stroke: "group", - strokeWidth: 2, - tip: true - })); + marks2.push(line(data, { x: "false_positive_rate", y: "sensitivity", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true })); return plot({ width: 600, height: 600, @@ -18661,34 +18719,11 @@ function renderCalibrationV2(spec) { const data = seriesRenderData(spec, spec.data); const marks2 = []; if (spec.references?.some((reference) => reference.type === "identity")) { - marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { - x: "x", - y: "y", - stroke: "#BEBEBE", - strokeWidth: 2, - strokeDasharray: "4,4" - })); + marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { x: "x", y: "y", stroke: "#BEBEBE", strokeWidth: 2, strokeDasharray: "4,4" })); } - marks2.push(line(data, { - x: "predicted", - y: "observed", - z: "seriesId", - stroke: "group", - strokeWidth: 2, - tip: true - })); + marks2.push(line(data, { x: "predicted", y: "observed", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true })); const discrete = data.filter((datum2) => datum2.method === "discrete"); - if (discrete.length > 0) { - marks2.push(dot(discrete, { - x: "predicted", - y: "observed", - fill: "group", - stroke: "white", - strokeWidth: 1.5, - r: 5, - tip: true - })); - } + if (discrete.length > 0) marks2.push(dot(discrete, { x: "predicted", y: "observed", fill: "group", stroke: "white", strokeWidth: 1.5, r: 5, tip: true })); const hasDistribution = (spec.distribution?.length ?? 0) > 0; const calibration = plot({ width: 600, @@ -18713,14 +18748,7 @@ function renderCalibrationV2(spec) { x: { label: spec.xAxis.label, domain: spec.xAxis.domain, grid: false, ticks: 6 }, y: { label: null, grid: false, ticks: 3 }, color: { legend: false, range: colorRange }, - marks: [rectY(distribution, { - x1: (datum2) => datum2.midpoint - datum2.binWidth / 2, - x2: (datum2) => datum2.midpoint + datum2.binWidth / 2, - y: "count", - fill: "group", - fillOpacity: 1 / Math.max(groups2.length, 1), - tip: true - })] + marks: [rectY(distribution, { x1: (datum2) => datum2.midpoint - datum2.binWidth / 2, x2: (datum2) => datum2.midpoint + datum2.binWidth / 2, y: "count", fill: "group", fillOpacity: 1 / Math.max(groups2.length, 1), tip: true })] }); const container = document.createElement("div"); container.style.width = "600px"; @@ -18734,22 +18762,9 @@ function renderPrecisionRecallV2(spec) { const showLegend = groups2.length > 1; const data = seriesRenderData(spec, spec.data); const marks2 = []; - for (const reference of spec.references ?? []) { - if (reference.type !== "horizontal" || reference.value === void 0) continue; - marks2.push(ruleY([reference.value], { - stroke: "#BEBEBE", - strokeWidth: 2, - strokeDasharray: "4,4" - })); - } - marks2.push(line(data, { - x: "sensitivity", - y: "ppv", - z: "seriesId", - stroke: "group", - strokeWidth: 2, - tip: true - })); + for (const reference of spec.references ?? []) if (reference.type === "horizontal" && reference.value !== void 0) marks2.push(ruleY([reference.value], { stroke: "#BEBEBE", strokeWidth: 2, strokeDasharray: "4,4" })); + marks2.push(line(data, { x: "sensitivity", y: "ppv", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true })); + marks2.push(dot(data, { x: "sensitivity", y: "ppv", fill: "group", stroke: "white", strokeWidth: 1.5, r: 4, tip: true })); return plot({ width: 600, height: 600, @@ -18758,7 +18773,28 @@ function renderPrecisionRecallV2(spec) { style: BASE_STYLE2, x: { label: spec.xAxis.label, domain: spec.xAxis.domain, grid: false, ticks: 6 }, y: { label: spec.yAxis.label, domain: spec.yAxis.domain, grid: false, ticks: 6 }, - color: { legend: showLegend, range: showLegend ? RTICHOKE_COLORS3 : ["#000000"] }, + color: { legend: showLegend, domain: groups2, range: showLegend ? RTICHOKE_COLORS3 : ["#000000"] }, + marks: marks2 + }); +} +function renderGainsV2(spec, options = {}) { + assertV2ReferentialIntegrity(spec); + const groups2 = [...new Set(spec.series.map((series) => series.display.group))]; + const showLegend = groups2.length > 1; + const resolved = resolveV2RenderOptions(groups2.length, options); + const data = seriesRenderData(spec, spec.data); + const marks2 = referenceMarks(spec); + marks2.push(line(data, { x: "ppcr", y: "sensitivity", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true })); + marks2.push(dot(data, { x: "ppcr", y: "sensitivity", fill: "group", stroke: "white", strokeWidth: 1.5, r: 4, tip: true })); + return plot({ + width: resolved.width, + height: resolved.height, + marginLeft: 64, + marginBottom: 56, + style: BASE_STYLE2, + x: { label: spec.xAxis.label, domain: spec.xAxis.domain, grid: false, ticks: 6 }, + y: { label: spec.yAxis.label, domain: spec.yAxis.domain, grid: false, ticks: 6 }, + color: { legend: showLegend, domain: groups2, range: resolved.colors }, marks: marks2 }); } @@ -18768,6 +18804,7 @@ export { DisplayGroupingSpecSchema, DisplayRoleSchema, EvaluationSpecSchema, + GainsV2SpecSchema, PrecisionRecallV2SpecSchema, ReferenceLineV2SpecSchema, RocSpecSchema, @@ -18780,6 +18817,7 @@ export { calibrationV2SpecFromRtichokeRows, renderCalibration, renderCalibrationV2, + renderGainsV2, renderPrecisionRecallV2, renderRoc, renderRocV2, diff --git a/src/rtichoke/discrimination/gains.py b/src/rtichoke/discrimination/gains.py index cb131d8a..40a0fd16 100644 --- a/src/rtichoke/discrimination/gains.py +++ b/src/rtichoke/discrimination/gains.py @@ -2,7 +2,7 @@ A module for Gains Curves using Plotly helpers """ -from typing import Dict, List, Sequence, Union +from typing import Any, Dict, List, Sequence, Union from plotly.graph_objs._figure import Figure from rtichoke.processing.binary_color_values import _apply_color_values_binary from rtichoke.processing.plotly_helper_functions import ( @@ -16,6 +16,10 @@ ) import numpy as np import polars as pl +from rtichoke._renderers import _render_gains_v2, _validate_renderer +from rtichoke._viz_spec_v2 import _gains_v2_spec_from_performance_data +from rtichoke.performance_data.performance_data import prepare_performance_data +from rtichoke.processing.evaluation_semantics import _build_evaluation_metadata def _get_gains_aj_estimates_times(performance_data: pl.DataFrame) -> pl.DataFrame: @@ -90,7 +94,8 @@ def create_gains_curve( "#D1603D", "#585123", ], -) -> Figure: + renderer: str = "plotly", +) -> Any: """Creates a Gains curve. A Gains curve is a marketing and business analytics tool that evaluates @@ -113,12 +118,37 @@ def create_gains_curve( The width and height of the plot in pixels. Defaults to 600. color_values : List[str], optional A list of hex color strings for the plot lines. + renderer : {"plotly", "matplotlib", "browser", "rtichoke_viz"}, optional + Rendering backend. The default, ``"plotly"``, preserves the existing + return value and behavior. ``"matplotlib"`` requires the optional + Matplotlib dependency. ``"browser"`` and its ``"rtichoke_viz"`` alias + return an offline browser chart backed by the packaged TypeScript bundle. Returns ------- - Figure - A Plotly ``Figure`` object representing the Gains curve. + Figure or RtichokeBrowserChart + A Plotly or Matplotlib figure, or an offline browser chart, depending + on ``renderer``. """ + selected_renderer = _validate_renderer(renderer) + if selected_renderer != "plotly": + performance_data = prepare_performance_data( + probs=probs, + reals=reals, + stratified_by=stratified_by, + by=by, + ) + evaluation_metadata = _build_evaluation_metadata(probs, reals, np.array([])) + spec = _gains_v2_spec_from_performance_data( + performance_data, evaluation_metadata + ) + return _render_gains_v2( + spec, + renderer=selected_renderer, + size=size, + color_values=color_values, + ) + fig = _create_rtichoke_plotly_curve_binary( probs, reals, diff --git a/tests/test_gains_renderers.py b/tests/test_gains_renderers.py new file mode 100644 index 00000000..8acaafc4 --- /dev/null +++ b/tests/test_gains_renderers.py @@ -0,0 +1,70 @@ +from pathlib import Path + +import matplotlib.figure +import numpy as np +import plotly.graph_objects as go +import plotly.io as pio +import pytest + +from rtichoke import create_gains_curve +from rtichoke._renderers import RtichokeBrowserChart + + +def _inputs(): + return ( + { + "Model A": np.array([0.05, 0.2, 0.7, 0.95]), + "Model B": np.array([0.1, 0.4, 0.6, 0.9]), + }, + np.array([0, 0, 1, 1]), + ) + + +def test_default_and_explicit_plotly_preserve_existing_renderer(): + probs, reals = _inputs() + + default = create_gains_curve(probs, reals, by=0.25) + explicit = create_gains_curve(probs, reals, by=0.25, renderer="plotly") + + assert isinstance(default, go.Figure) + assert pio.to_json(default) == pio.to_json(explicit) + + +def test_matplotlib_renders_canonical_gains_spec(): + probs, reals = _inputs() + + figure = create_gains_curve(probs, reals, by=0.25, renderer="matplotlib") + + assert isinstance(figure, matplotlib.figure.Figure) + assert len(figure.axes[0].lines) == 4 # random, perfect, and two series + assert figure.axes[0].get_xlabel() == "Predicted Positives (Rate)" + + +@pytest.mark.parametrize("renderer", ["browser", "rtichoke_viz"]) +def test_browser_renderer_writes_offline_v2_chart(renderer: str, tmp_path: Path): + probs, reals = _inputs() + + chart = create_gains_curve(probs, reals, by=0.25, renderer=renderer) + assert isinstance(chart, RtichokeBrowserChart) + assert chart.spec["schemaVersion"] == "2.0" + assert chart.spec["type"] == "gains" + assert len(chart.spec["evaluations"]) == 2 + assert {row["seriesId"] for row in chart.spec["data"]} == { + "series-1", + "series-2", + } + + output = chart.write_html(tmp_path / "gains.html") + html = output.read_text(encoding="utf-8") + assert 'import { renderGainsV2 } from "./rtichoke-viz.js"' in html + assert '"schemaVersion":"2.0"' in html + assert (tmp_path / "rtichoke-viz.js").is_file() + assert (tmp_path / "rtichoke-viz.css").is_file() + assert "http://" not in html and "https://" not in html + + +def test_unsupported_renderer_is_clear(): + probs, reals = _inputs() + + with pytest.raises(ValueError, match="Unsupported renderer 'canvas'"): + create_gains_curve(probs, reals, renderer="canvas") diff --git a/tests/test_rtichoke_viz_vendor.py b/tests/test_rtichoke_viz_vendor.py index ce36ad7f..aa069e3c 100644 --- a/tests/test_rtichoke_viz_vendor.py +++ b/tests/test_rtichoke_viz_vendor.py @@ -1,19 +1,35 @@ +import hashlib +import tarfile from pathlib import Path _VENDOR = Path(__file__).parents[1] / "src" / "rtichoke" / "_vendor" / "rtichoke_viz" -def test_vendored_rtichoke_viz_v020_provenance_and_schemas(): +def test_vendored_rtichoke_viz_v030_provenance_archive_and_schemas(): provenance = (_VENDOR / "VENDORED_FROM").read_text() - assert "release=v0.2.0" in provenance - assert "source_commit=45dc109a6a0679d0f8f3f9452d8de9306a89b906" in provenance - assert "archive=rtichoke-viz-0.2.0.tar.gz" in provenance + assert "release=v0.3.0" in provenance + assert "source_commit=aca9188ea856167557efb20980a0b43e0481b8c8" in provenance + assert "archive=rtichoke-viz-0.3.0.tar.gz" in provenance assert ( - "sha256=3861277c01b3983f8b344a9ee0237c7d09fd0ba4c3d1e0cce489962e7b559d9f" + "sha256=558f8d9e16f9544659b84e33f72511065163291a1b97a3c5511b61d1e1f0cac1" in provenance ) + archive = _VENDOR / "rtichoke-viz-0.3.0.tar.gz" + assert hashlib.sha256(archive.read_bytes()).hexdigest() == ( + "558f8d9e16f9544659b84e33f72511065163291a1b97a3c5511b61d1e1f0cac1" + ) + with tarfile.open(archive, "r:gz") as release: + assert set(release.getnames()) == { + "rtichoke-viz-0.3.0", + "rtichoke-viz-0.3.0/MANIFEST", + "rtichoke-viz-0.3.0/rtichoke-viz.css", + "rtichoke-viz-0.3.0/rtichoke-viz.js", + "rtichoke-viz-0.3.0/rtichoke-viz.schema.json", + "rtichoke-viz-0.3.0/rtichoke-viz-v2.schema.json", + } + assert (_VENDOR / "rtichoke-viz.js").stat().st_size > 0 assert (_VENDOR / "rtichoke-viz.css").stat().st_size > 0 @@ -23,11 +39,13 @@ def test_vendored_rtichoke_viz_v020_provenance_and_schemas(): assert '"$id": "https://rtichoke.dev/schema/viz/2.0.json"' in v2_schema -def test_v020_bundle_keeps_v1_browser_exports(): - bundle = (_VENDOR / "rtichoke-viz.js").read_text() +def test_v030_bundle_keeps_v1_and_adds_v2_browser_exports(): + bundle = (_VENDOR / "rtichoke-viz.js").read_text(encoding="utf-8") for export_name in ( "renderRoc", "renderCalibration", "RtichokeChartSpecSchema", + "renderGainsV2", + "RtichokeChartSpecV2Schema", ): assert export_name in bundle diff --git a/uv.lock b/uv.lock index 14353270..d921c5a3 100644 --- a/uv.lock +++ b/uv.lock @@ -2366,43 +2366,50 @@ wheels = [ [[package]] name = "pyzmq" -version = "26.4.0" +version = "27.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293, upload-time = "2025-04-04T12:05:44.049Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/44/a778555ebfdf6c7fc00816aad12d185d10a74d975800341b1bc36bad1187/pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", size = 1341586, upload-time = "2025-04-04T12:03:41.954Z" }, - { url = "https://files.pythonhosted.org/packages/9c/4f/f3a58dc69ac757e5103be3bd41fb78721a5e17da7cc617ddb56d973a365c/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", size = 665880, upload-time = "2025-04-04T12:03:43.45Z" }, - { url = "https://files.pythonhosted.org/packages/fe/45/50230bcfb3ae5cb98bee683b6edeba1919f2565d7cc1851d3c38e2260795/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", size = 902216, upload-time = "2025-04-04T12:03:45.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/59/56bbdc5689be5e13727491ad2ba5efd7cd564365750514f9bc8f212eef82/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", size = 859814, upload-time = "2025-04-04T12:03:47.188Z" }, - { url = "https://files.pythonhosted.org/packages/81/b1/57db58cfc8af592ce94f40649bd1804369c05b2190e4cbc0a2dad572baeb/pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", size = 855889, upload-time = "2025-04-04T12:03:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/e8/92/47542e629cbac8f221c230a6d0f38dd3d9cff9f6f589ed45fdf572ffd726/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", size = 1197153, upload-time = "2025-04-04T12:03:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/07/e5/b10a979d1d565d54410afc87499b16c96b4a181af46e7645ab4831b1088c/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", size = 1507352, upload-time = "2025-04-04T12:03:52.473Z" }, - { url = "https://files.pythonhosted.org/packages/ab/58/5a23db84507ab9c01c04b1232a7a763be66e992aa2e66498521bbbc72a71/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", size = 1406834, upload-time = "2025-04-04T12:03:54Z" }, - { url = "https://files.pythonhosted.org/packages/22/74/aaa837b331580c13b79ac39396601fb361454ee184ca85e8861914769b99/pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", size = 577992, upload-time = "2025-04-04T12:03:55.815Z" }, - { url = "https://files.pythonhosted.org/packages/30/0f/55f8c02c182856743b82dde46b2dc3e314edda7f1098c12a8227eeda0833/pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", size = 640466, upload-time = "2025-04-04T12:03:57.231Z" }, - { url = "https://files.pythonhosted.org/packages/e4/29/073779afc3ef6f830b8de95026ef20b2d1ec22d0324d767748d806e57379/pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", size = 556342, upload-time = "2025-04-04T12:03:59.218Z" }, - { url = "https://files.pythonhosted.org/packages/d7/20/fb2c92542488db70f833b92893769a569458311a76474bda89dc4264bd18/pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", size = 1339484, upload-time = "2025-04-04T12:04:00.671Z" }, - { url = "https://files.pythonhosted.org/packages/58/29/2f06b9cabda3a6ea2c10f43e67ded3e47fc25c54822e2506dfb8325155d4/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", size = 666106, upload-time = "2025-04-04T12:04:02.366Z" }, - { url = "https://files.pythonhosted.org/packages/77/e4/dcf62bd29e5e190bd21bfccaa4f3386e01bf40d948c239239c2f1e726729/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", size = 902056, upload-time = "2025-04-04T12:04:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cf/b36b3d7aea236087d20189bec1a87eeb2b66009731d7055e5c65f845cdba/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", size = 860148, upload-time = "2025-04-04T12:04:05.581Z" }, - { url = "https://files.pythonhosted.org/packages/18/a6/f048826bc87528c208e90604c3bf573801e54bd91e390cbd2dfa860e82dc/pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", size = 855983, upload-time = "2025-04-04T12:04:07.096Z" }, - { url = "https://files.pythonhosted.org/packages/0a/27/454d34ab6a1d9772a36add22f17f6b85baf7c16e14325fa29e7202ca8ee8/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", size = 1197274, upload-time = "2025-04-04T12:04:08.523Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3d/7abfeab6b83ad38aa34cbd57c6fc29752c391e3954fd12848bd8d2ec0df6/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", size = 1507120, upload-time = "2025-04-04T12:04:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/13/ff/bc8d21dbb9bc8705126e875438a1969c4f77e03fc8565d6901c7933a3d01/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", size = 1406738, upload-time = "2025-04-04T12:04:12.509Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5d/d4cd85b24de71d84d81229e3bbb13392b2698432cf8fdcea5afda253d587/pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", size = 577826, upload-time = "2025-04-04T12:04:14.289Z" }, - { url = "https://files.pythonhosted.org/packages/c6/6c/f289c1789d7bb6e5a3b3bef7b2a55089b8561d17132be7d960d3ff33b14e/pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", size = 640406, upload-time = "2025-04-04T12:04:15.757Z" }, - { url = "https://files.pythonhosted.org/packages/b3/99/676b8851cb955eb5236a0c1e9ec679ea5ede092bf8bf2c8a68d7e965cac3/pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", size = 556216, upload-time = "2025-04-04T12:04:17.212Z" }, - { url = "https://files.pythonhosted.org/packages/65/c2/1fac340de9d7df71efc59d9c50fc7a635a77b103392d1842898dd023afcb/pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", size = 1333769, upload-time = "2025-04-04T12:04:18.665Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c7/6c03637e8d742c3b00bec4f5e4cd9d1c01b2f3694c6f140742e93ca637ed/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", size = 658826, upload-time = "2025-04-04T12:04:20.405Z" }, - { url = "https://files.pythonhosted.org/packages/a5/97/a8dca65913c0f78e0545af2bb5078aebfc142ca7d91cdaffa1fbc73e5dbd/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", size = 891650, upload-time = "2025-04-04T12:04:22.413Z" }, - { url = "https://files.pythonhosted.org/packages/7d/7e/f63af1031eb060bf02d033732b910fe48548dcfdbe9c785e9f74a6cc6ae4/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", size = 849776, upload-time = "2025-04-04T12:04:23.959Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fa/1a009ce582802a895c0d5fe9413f029c940a0a8ee828657a3bb0acffd88b/pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", size = 842516, upload-time = "2025-04-04T12:04:25.449Z" }, - { url = "https://files.pythonhosted.org/packages/6e/bc/f88b0bad0f7a7f500547d71e99f10336f2314e525d4ebf576a1ea4a1d903/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", size = 1189183, upload-time = "2025-04-04T12:04:27.035Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8c/db446a3dd9cf894406dec2e61eeffaa3c07c3abb783deaebb9812c4af6a5/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", size = 1495501, upload-time = "2025-04-04T12:04:28.833Z" }, - { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540, upload-time = "2025-04-04T12:04:30.562Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074, upload-time = "2026-08-20T19:06:40.601Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" }, + { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450, upload-time = "2026-08-20T19:06:52.307Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633, upload-time = "2026-08-20T19:06:53.809Z" }, + { url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006, upload-time = "2026-08-20T19:06:55.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/d0541596b48c5a19f85dcbea83d6673d8e91681cdf853eb194c31fc9766e/pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", size = 1127193, upload-time = "2026-08-20T19:06:56.545Z" }, + { url = "https://files.pythonhosted.org/packages/50/9f/8c7411bb283982d46e6d56dca6a095678c87eb0398daead12776d9881ac2/pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", size = 1166833, upload-time = "2026-08-20T19:06:58.011Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452, upload-time = "2026-08-20T19:06:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520, upload-time = "2026-08-20T19:07:01.283Z" }, + { url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289, upload-time = "2026-08-20T19:07:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103, upload-time = "2026-08-20T19:07:13.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215, upload-time = "2026-08-20T19:07:15.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252, upload-time = "2026-08-20T19:07:16.582Z" }, + { url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295, upload-time = "2026-08-20T19:07:17.94Z" }, + { url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492, upload-time = "2026-08-20T19:07:19.476Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992, upload-time = "2026-08-20T19:07:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" }, + { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" }, + { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" }, + { url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073, upload-time = "2026-08-20T19:07:31.341Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701, upload-time = "2026-08-20T19:07:32.895Z" }, + { url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985, upload-time = "2026-08-20T19:07:34.19Z" }, ] [[package]] @@ -2579,6 +2586,11 @@ dependencies = [ { name = "smoothstate" }, ] +[package.optional-dependencies] +matplotlib = [ + { name = "matplotlib" }, +] + [package.dev-dependencies] dev = [ { name = "dcurves" }, @@ -2589,7 +2601,6 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pyzmq" }, { name = "ruff" }, { name = "scikit-learn" }, { name = "ty" }, @@ -2602,12 +2613,14 @@ docs = [ [package.metadata] requires-dist = [ { name = "great-tables", specifier = ">=0.18.0" }, + { name = "matplotlib", marker = "extra == 'matplotlib'", specifier = ">=3.9.0" }, { name = "plotly", specifier = ">=6.0.0,<7.0.0" }, { name = "polars", specifier = ">=1.31.0" }, { name = "polarstate", specifier = "==0.1.8" }, { name = "reactable", specifier = ">=0.1.5" }, { name = "smoothstate", specifier = ">=0.1.1" }, ] +provides-extras = ["matplotlib"] [package.metadata.requires-dev] dev = [ @@ -2619,7 +2632,6 @@ dev = [ { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pytest", specifier = ">=7.3.0,<8.0.0" }, { name = "pytest-cov", specifier = ">=4.0.0,<5.0.0" }, - { name = "pyzmq", specifier = ">=26.3.0,<27.0.0" }, { name = "ruff", specifier = ">=0.11.0" }, { name = "scikit-learn", specifier = ">=1.6.1" }, { name = "ty", specifier = ">=0.0.1a12" },