From eeb7e2635635c5c625005949b4311288535fe745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 27 Feb 2026 14:22:28 +0100 Subject: [PATCH 1/2] Allow adding and removing data from the widget --- changelog.md | 2 + examples/getting-started.ipynb | 695 ++++++++---------- python-wrapper/src/neo4j_viz/node.py | 2 +- python-wrapper/src/neo4j_viz/relationship.py | 4 +- .../src/neo4j_viz/visualization_graph.py | 9 +- python-wrapper/src/neo4j_viz/widget.py | 65 +- python-wrapper/tests/test_widget.py | 27 + 7 files changed, 429 insertions(+), 375 deletions(-) diff --git a/changelog.md b/changelog.md index ec98836c..4941586d 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,8 @@ ## New features +- Add convenience method `add_data` and `remove_data` to `GraphWidget`. + ## Bug fixes - Fixed a bug with the theme detection inn VSCode. diff --git a/examples/getting-started.ipynb b/examples/getting-started.ipynb index 2fd3af52..f206f9c7 100644 --- a/examples/getting-started.ipynb +++ b/examples/getting-started.ipynb @@ -53,7 +53,7 @@ " Python (nvl.py) injects a \n", - " \n", - " \n", + " `,new Error(\"window.__NEO4J_VIZ_DATA__ is not defined\");const Zle={get(r){return pE[r]},on(){},off(){},set(){},save_changes(){}},gE=document.getElementById(\"neo4j-viz-e0ca03fd272d\");if(!gE)throw new Error(\"Container element #neo4j-viz-e0ca03fd272d not found\");gE.style.width=pE.width??\"100%\";gE.style.height=pE.height??\"100vh\";Kle.render({model:Zle,el:gE});\n", + " \n", + " \n", "\n", " \n", - "
\n", + "
\n", " \n", "\n" ], @@ -1711,7 +1691,7 @@ " Python (nvl.py) injects a \n", - " \n", - " \n", + " `,new Error(\"window.__NEO4J_VIZ_DATA__ is not defined\");const Zle={get(r){return pE[r]},on(){},off(){},set(){},save_changes(){}},gE=document.getElementById(\"neo4j-viz-770853535104\");if(!gE)throw new Error(\"Container element #neo4j-viz-770853535104 not found\");gE.style.width=pE.width??\"100%\";gE.style.height=pE.height??\"100vh\";Kle.render({model:Zle,el:gE});\n", + " \n", + " \n", "\n", " \n", - "
\n", + "
\n", " \n", "\n" ], @@ -3355,8 +3315,7 @@ "new_node = Node(id=new_id, size=10, caption=\"Person\")\n", "new_rel = Relationship(source=new_id, target=target_id, caption=\"KNOWS\")\n", "\n", - "widget.nodes = widget.nodes + [new_node.to_dict()]\n", - "widget.relationships = widget.relationships + [new_rel.to_dict()]" + "widget.add_data(nodes=[new_node], relationships=[new_rel])" ] } ], diff --git a/python-wrapper/src/neo4j_viz/node.py b/python-wrapper/src/neo4j_viz/node.py index a8bdca32..f094961b 100644 --- a/python-wrapper/src/neo4j_viz/node.py +++ b/python-wrapper/src/neo4j_viz/node.py @@ -9,7 +9,7 @@ from .node_size import RealNumber from .options import CaptionAlignment -NodeIdType = Union[str, int] +NodeIdType = str | int def create_aliases(field_name: str) -> AliasChoices: diff --git a/python-wrapper/src/neo4j_viz/relationship.py b/python-wrapper/src/neo4j_viz/relationship.py index 132935f5..a08bbb6b 100644 --- a/python-wrapper/src/neo4j_viz/relationship.py +++ b/python-wrapper/src/neo4j_viz/relationship.py @@ -9,6 +9,8 @@ from .options import CaptionAlignment +RelationshipIdType = str | int + def create_aliases(field_name: str) -> AliasChoices: valid_names = [field_name] @@ -43,7 +45,7 @@ class Relationship( """ #: Unique identifier for the relationship - id: Union[str, int] = Field( + id: RelationshipIdType = Field( default_factory=lambda: uuid4().hex, description="Unique identifier for the relationship" ) #: Node ID where the relationship points from diff --git a/python-wrapper/src/neo4j_viz/visualization_graph.py b/python-wrapper/src/neo4j_viz/visualization_graph.py index d77399b3..7016e634 100644 --- a/python-wrapper/src/neo4j_viz/visualization_graph.py +++ b/python-wrapper/src/neo4j_viz/visualization_graph.py @@ -88,7 +88,7 @@ def _build_render_options( self, layout: Layout | None, layout_options: dict[str, Any] | LayoutOptions | None, - renderer: Renderer, + renderer: Renderer | str, pan_position: tuple[float, float] | None, initial_zoom: float | None, min_zoom: float, @@ -105,6 +105,9 @@ def _build_render_options( "overriding `max_allowed_nodes`, but rendering could then take a long time" ) + if isinstance(renderer, str): + renderer = Renderer(renderer) + Renderer.check(renderer, num_nodes) if not layout: @@ -133,7 +136,7 @@ def render( self, layout: Layout | None = None, layout_options: dict[str, Any] | LayoutOptions | None = None, - renderer: Renderer = Renderer.CANVAS, + renderer: Renderer | str = Renderer.CANVAS, width: str = "100%", height: str = "600px", pan_position: tuple[float, float] | None = None, @@ -207,7 +210,7 @@ def render_widget( self, layout: Layout | None = None, layout_options: dict[str, Any] | LayoutOptions | None = None, - renderer: Renderer = Renderer.CANVAS, + renderer: Renderer | str = Renderer.CANVAS, width: str = "100%", height: str = "600px", pan_position: tuple[float, float] | None = None, diff --git a/python-wrapper/src/neo4j_viz/widget.py b/python-wrapper/src/neo4j_viz/widget.py index ffc29158..1c7548c5 100644 --- a/python-wrapper/src/neo4j_viz/widget.py +++ b/python-wrapper/src/neo4j_viz/widget.py @@ -7,9 +7,9 @@ import anywidget import traitlets -from .node import Node +from .node import Node, NodeIdType from .options import RenderOptions -from .relationship import Relationship +from .relationship import Relationship, RelationshipIdType def _serialize_entity(entity: Union[Node, Relationship]) -> dict[str, Any]: @@ -79,3 +79,64 @@ def from_graph_data( options=options.to_js_options() if options else {}, theme=theme, ) + + def add_data(self, nodes: Node | list[Node] | None = None, relationships: Relationship | list[Relationship] | None = None) -> None: + """ + Add nodes or relationships to the graph widget. + + Parameters + ----------- + nodes: + Nodes to add to the graph widget. + relationships: + Relationships to add to the graph widget. + """ + if isinstance(nodes, Node): + nodes = [nodes] + if isinstance(relationships, Relationship): + relationships = [relationships] + + if nodes: + self.nodes = self.nodes + [_serialize_entity(n) for n in nodes] + if relationships: + self.relationships = self.relationships + [_serialize_entity(r) for r in relationships] + + def remove_data( + self, + nodes: Node | list[Node | NodeIdType] | NodeIdType | None = None, + relationships: Relationship | list[Relationship | RelationshipIdType] | RelationshipIdType | None = None, + ) -> None: + """ + Remove nodes or relationships from the graph widget. + + Parameters + ----------- + nodes: + Nodes to remove from the graph widget. + relationships: + Relationships to remove from the graph widget. + """ + if isinstance(nodes, Node): + node_ids_to_remove = {nodes.id} + elif isinstance(nodes, NodeIdType): + node_ids_to_remove = {nodes} + else: + node_ids_to_remove = {n.id if isinstance(n, Node) else n for n in nodes} + + if isinstance(relationships, Relationship): + rel_ids_to_remove = {relationships.id} + elif isinstance(relationships, RelationshipIdType): + rel_ids_to_remove = {relationships} + else: + rel_ids_to_remove = {r.id if isinstance(r, Relationship) else r for r in relationships} + + self.nodes = [n for n in self.nodes if n["id"] not in node_ids_to_remove] + + def keep_rel(r: dict[str, Any]) -> bool: + return ( + r["id"] not in rel_ids_to_remove + and r["from"] not in node_ids_to_remove + and r["to"] not in node_ids_to_remove + ) + + self.relationships = [r for r in self.relationships if keep_rel(r)] diff --git a/python-wrapper/tests/test_widget.py b/python-wrapper/tests/test_widget.py index 5695f4c7..1f6637af 100644 --- a/python-wrapper/tests/test_widget.py +++ b/python-wrapper/tests/test_widget.py @@ -182,6 +182,33 @@ def test_replace_all_data(self) -> None: assert len(widget.relationships) == 2 assert widget.nodes[0]["id"] == "x1" + def test_add_data(self) -> None: + """Test adding new data to existing graph.""" + nodes = [Node(id="n1"), Node(id="n2")] + rels = [Relationship(source="n1", target="n2")] + widget = GraphWidget.from_graph_data(nodes, rels) + + widget.add_data(Node(id="x1"), Relationship(source="x1", target="x2")) + + assert len(widget.nodes) == 3 + assert len(widget.relationships) == 2 + + def test_remove_data(self) -> None: + """Test removing data from the graph.""" + node_1 = Node(id="n1") + nodes = [node_1, Node(id="n2"), Node(id="n3")] + rels = [ + Relationship(source="n1", target="n2"), + Relationship(id=42, source="n2", target="n1"), + Relationship(source="n2", target="n1"), # detach delete + Relationship(id=43, source="n3", target="n3"), + ] + widget = GraphWidget.from_graph_data(nodes, rels) + + widget.remove_data(nodes=[node_1, "n2"], relationships=[rels[0], "42"]) + assert {n["id"] for n in widget.nodes} == {"n3"} + assert {r["id"] for r in widget.relationships} == {"43"} + render_widget_cases = { "default": {}, From d0ec689f9ff17aa1968a6d7d6ef17674430cb966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 27 Feb 2026 14:47:36 +0100 Subject: [PATCH 2/2] Handle none case --- justfile | 1 + python-wrapper/src/neo4j_viz/widget.py | 14 +++++-- python-wrapper/uv.lock | 54 +++++++++++++------------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/justfile b/justfile index efc7cb54..b0c33a42 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,7 @@ py-sync: cd python-wrapper && uv sync --group dev --group docs --group notebook --extra pandas --extra neo4j --extra gds --extra snowflake py-style: + just py-sync ./scripts/makestyle.sh && ./scripts/checkstyle.sh py-test: diff --git a/python-wrapper/src/neo4j_viz/widget.py b/python-wrapper/src/neo4j_viz/widget.py index 1c7548c5..99ca33ab 100644 --- a/python-wrapper/src/neo4j_viz/widget.py +++ b/python-wrapper/src/neo4j_viz/widget.py @@ -80,7 +80,9 @@ def from_graph_data( theme=theme, ) - def add_data(self, nodes: Node | list[Node] | None = None, relationships: Relationship | list[Relationship] | None = None) -> None: + def add_data( + self, nodes: Node | list[Node] | None = None, relationships: Relationship | list[Relationship] | None = None + ) -> None: """ Add nodes or relationships to the graph widget. @@ -120,6 +122,8 @@ def remove_data( node_ids_to_remove = {nodes.id} elif isinstance(nodes, NodeIdType): node_ids_to_remove = {nodes} + elif nodes is None: + node_ids_to_remove = set() else: node_ids_to_remove = {n.id if isinstance(n, Node) else n for n in nodes} @@ -127,10 +131,13 @@ def remove_data( rel_ids_to_remove = {relationships.id} elif isinstance(relationships, RelationshipIdType): rel_ids_to_remove = {relationships} + elif relationships is None: + rel_ids_to_remove = set() else: rel_ids_to_remove = {r.id if isinstance(r, Relationship) else r for r in relationships} - self.nodes = [n for n in self.nodes if n["id"] not in node_ids_to_remove] + if node_ids_to_remove: + self.nodes = [n for n in self.nodes if n["id"] not in node_ids_to_remove] def keep_rel(r: dict[str, Any]) -> bool: return ( @@ -139,4 +146,5 @@ def keep_rel(r: dict[str, Any]) -> bool: and r["to"] not in node_ids_to_remove ) - self.relationships = [r for r in self.relationships if keep_rel(r)] + if rel_ids_to_remove: + self.relationships = [r for r in self.relationships if keep_rel(r)] diff --git a/python-wrapper/uv.lock b/python-wrapper/uv.lock index 7241f324..6c3cb397 100644 --- a/python-wrapper/uv.lock +++ b/python-wrapper/uv.lock @@ -2106,7 +2106,7 @@ wheels = [ [[package]] name = "nbsphinx" -version = "0.9.7" +version = "0.9.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, @@ -2116,9 +2116,9 @@ dependencies = [ { name = "sphinx" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/84/b1856b7651ac34e965aa567a158714c7f3bd42a1b1ce76bf423ffb99872c/nbsphinx-0.9.7.tar.gz", hash = "sha256:abd298a686d55fa894ef697c51d44f24e53aa312dadae38e82920f250a5456fe", size = 180479, upload-time = "2025-03-03T19:46:08.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d1/82081750f8a78ad0399c6ed831d42623b891904e8e7b8a75878225cf1dce/nbsphinx-0.9.8.tar.gz", hash = "sha256:d0765908399a8ee2b57be7ae881cf2ea58d66db3af7bbf33e6eb48f83bea5495", size = 417469, upload-time = "2025-11-28T17:41:02.336Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/2d/8c8e635bcc6757573d311bb3c5445426382f280da32b8cd6d82d501ef4a4/nbsphinx-0.9.7-py3-none-any.whl", hash = "sha256:7292c3767fea29e405c60743eee5393682a83982ab202ff98f5eb2db02629da8", size = 31660, upload-time = "2025-03-03T19:46:06.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl", hash = "sha256:92d95ee91784e56bc633b60b767a6b6f23a0445f891e24641ce3c3f004759ccf", size = 31961, upload-time = "2025-11-28T17:41:00.796Z" }, ] [[package]] @@ -2235,13 +2235,13 @@ dev = [ { name = "palettable", specifier = "==3.3.3" }, { name = "pytest", specifier = "==8.4.2" }, { name = "pytest-mock", specifier = "==3.15.1" }, - { name = "ruff", specifier = "==0.14.13" }, + { name = "ruff", specifier = "==0.14.14" }, { name = "selenium", specifier = "==4.40.0" }, { name = "streamlit", specifier = "==1.53.0" }, ] docs = [ { name = "enum-tools", extras = ["sphinx"] }, - { name = "nbsphinx", specifier = "==0.9.7" }, + { name = "nbsphinx", specifier = "==0.9.8" }, { name = "nbsphinx-link", specifier = "==1.3.1" }, { name = "sphinx", specifier = "==8.1.3" }, ] @@ -3592,28 +3592,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]]