From 11f6e043dad07690889a50fc2412401f140e4f6a Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 29 Oct 2025 17:57:36 +0000 Subject: [PATCH 01/12] Remove empty mypy overrides --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16233da..b5792ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,9 +76,6 @@ line_length = 99 warn_unused_ignores = true warn_redundant_casts = true -[[tool.mypy.overrides]] -# Ignore packages with no stubs. -ignore_missing_imports = true [tool.uv.sources] import-linter = { workspace = true } From 171a3bf081b8bc6c854364ea2fd6f09a85f85bbb Mon Sep 17 00:00:00 2001 From: David Seddon Date: Wed, 29 Oct 2025 17:56:57 +0000 Subject: [PATCH 02/12] Define Edge type --- src/impulse/application/use_cases.py | 2 +- src/impulse/dotfile.py | 18 +++++++++++++----- tests/unit/application/test_use_cases.py | 13 +++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/impulse/application/use_cases.py b/src/impulse/application/use_cases.py index d9ab4d2..257505c 100644 --- a/src/impulse/application/use_cases.py +++ b/src/impulse/application/use_cases.py @@ -45,7 +45,7 @@ def draw_graph( label = str(number_of_imports) else: label = "" - dot.add_edge(source=downstream, destination=upstream, label=label) + dot.add_edge(dotfile.Edge(source=downstream, destination=upstream, label=label)) viewer.view(dot) diff --git a/src/impulse/dotfile.py b/src/impulse/dotfile.py index e796632..7ea22a7 100644 --- a/src/impulse/dotfile.py +++ b/src/impulse/dotfile.py @@ -1,4 +1,12 @@ from textwrap import dedent +from dataclasses import dataclass + + +@dataclass(frozen=True, order=True) +class Edge: + source: str + destination: str + label: str class DotGraph: @@ -11,13 +19,13 @@ class DotGraph: def __init__(self, title: str) -> None: self.title = title self.nodes: set[str] = set() - self.edges: set[tuple[str, str, str]] = set() + self.edges: set[Edge] = set() def add_node(self, name: str) -> None: self.nodes.add(name) - def add_edge(self, *, source: str, destination: str, label: str) -> None: - self.edges.add((source, destination, label)) + def add_edge(self, edge: Edge) -> None: + self.edges.add(edge) def render(self) -> str: # concentrate=true means that we merge the lines together. @@ -33,8 +41,8 @@ def _render_nodes(self) -> str: def _render_edges(self) -> str: return "\n".join( - f'"{self._render_module(source)}" -> "{self._render_module(destination)}"{self._render_label(label)}\n' - for source, destination, label in sorted(self.edges) + f'"{self._render_module(edge.source)}" -> "{self._render_module(edge.destination)}"{self._render_label(edge.label)}\n' + for edge in sorted(self.edges) ) def _render_module(self, module: str) -> str: diff --git a/tests/unit/application/test_use_cases.py b/tests/unit/application/test_use_cases.py index 347c37d..5f50795 100644 --- a/tests/unit/application/test_use_cases.py +++ b/tests/unit/application/test_use_cases.py @@ -1,6 +1,7 @@ from impulse.application import use_cases from copy import copy from impulse import dotfile +from impulse.dotfile import Edge import grimp from grimp.adaptors.graph import ImportGraph from impulse import ports @@ -83,9 +84,9 @@ def test_draw_graph(self): "mypackage.foo.red", } assert viewer.called_with_dot.edges == { - ("mypackage.foo.blue", "mypackage.foo.green", ""), - ("mypackage.foo.green", "mypackage.foo.yellow", ""), - ("mypackage.foo.blue", "mypackage.foo.red", ""), + Edge("mypackage.foo.blue", "mypackage.foo.green", ""), + Edge("mypackage.foo.green", "mypackage.foo.yellow", ""), + Edge("mypackage.foo.blue", "mypackage.foo.red", ""), } def test_draw_graph_show_import_totals(self): @@ -108,7 +109,7 @@ def test_draw_graph_show_import_totals(self): "mypackage.foo.red", } assert viewer.called_with_dot.edges == { - ("mypackage.foo.blue", "mypackage.foo.green", "1"), - ("mypackage.foo.green", "mypackage.foo.yellow", "1"), - ("mypackage.foo.blue", "mypackage.foo.red", "4"), + Edge("mypackage.foo.blue", "mypackage.foo.green", "1"), + Edge("mypackage.foo.green", "mypackage.foo.yellow", "1"), + Edge("mypackage.foo.blue", "mypackage.foo.red", "4"), } From 76dd555d5659a99420ec8a9bb1d185ab33ed1e4f Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 07:41:41 +0000 Subject: [PATCH 03/12] Move rendering to Edge class --- src/impulse/dotfile.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/impulse/dotfile.py b/src/impulse/dotfile.py index 7ea22a7..f19b88a 100644 --- a/src/impulse/dotfile.py +++ b/src/impulse/dotfile.py @@ -8,6 +8,13 @@ class Edge: destination: str label: str + def __str__(self) -> str: + return f'"{DotGraph.render_module(self.source)}" -> "{DotGraph.render_module(self.destination)}"{self._render_label(self.label)}\n' + + @staticmethod + def _render_label(label: str) -> str: + return f" [label={label}]" if label else "" + class DotGraph: """ @@ -37,16 +44,12 @@ def render(self) -> str: }}""") def _render_nodes(self) -> str: - return "\n".join(f'"{self._render_module(node)}"\n' for node in sorted(self.nodes)) + return "\n".join(f'"{self.render_module(node)}"\n' for node in sorted(self.nodes)) def _render_edges(self) -> str: - return "\n".join( - f'"{self._render_module(edge.source)}" -> "{self._render_module(edge.destination)}"{self._render_label(edge.label)}\n' - for edge in sorted(self.edges) - ) + return "\n".join(str(edge) for edge in sorted(self.edges)) - def _render_module(self, module: str) -> str: - return module.rsplit(self.title)[1] - - def _render_label(self, label: str) -> str: - return f" [label={label}]" if label else "" + @staticmethod + def render_module(module: str) -> str: + # Render as relative module. + return f".{module.split('.')[-1]}" From 61af5caac82ebc380128de994e4cf79abf54db22 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 07:47:01 +0000 Subject: [PATCH 04/12] Refactor edge rendering to prepare for more attributes --- src/impulse/dotfile.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/impulse/dotfile.py b/src/impulse/dotfile.py index f19b88a..31f9378 100644 --- a/src/impulse/dotfile.py +++ b/src/impulse/dotfile.py @@ -9,11 +9,17 @@ class Edge: label: str def __str__(self) -> str: - return f'"{DotGraph.render_module(self.source)}" -> "{DotGraph.render_module(self.destination)}"{self._render_label(self.label)}\n' - - @staticmethod - def _render_label(label: str) -> str: - return f" [label={label}]" if label else "" + return f'"{DotGraph.render_module(self.source)}" -> "{DotGraph.render_module(self.destination)}"{self._render_attrs()}\n' + + def _render_attrs(self) -> str: + attrs: dict[str, str] = {} + if self.label: + attrs["label"] = self.label + if attrs: + joined_attrs = ", ".join([f'{key}="{value}"' for key, value in attrs.items()]) + return f" [{joined_attrs}]" + else: + return "" class DotGraph: From 25d3427d4db35756e7f7c81d3d2bfa858d93b866 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 07:48:11 +0000 Subject: [PATCH 05/12] Support passing in emphasis for edges --- src/impulse/dotfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/impulse/dotfile.py b/src/impulse/dotfile.py index 31f9378..7d2f846 100644 --- a/src/impulse/dotfile.py +++ b/src/impulse/dotfile.py @@ -7,6 +7,7 @@ class Edge: source: str destination: str label: str + emphasized: bool = False def __str__(self) -> str: return f'"{DotGraph.render_module(self.source)}" -> "{DotGraph.render_module(self.destination)}"{self._render_attrs()}\n' @@ -15,6 +16,8 @@ def _render_attrs(self) -> str: attrs: dict[str, str] = {} if self.label: attrs["label"] = self.label + if self.emphasized: + attrs["color"] = "red" if attrs: joined_attrs = ", ".join([f'{key}="{value}"' for key, value in attrs.items()]) return f" [{joined_attrs}]" From ef1bfb36da77a7efbaf2ab5b22ab7958cf795731 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 07:55:03 +0000 Subject: [PATCH 06/12] Add cycle in test --- tests/unit/application/test_use_cases.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/application/test_use_cases.py b/tests/unit/application/test_use_cases.py index 5f50795..903e978 100644 --- a/tests/unit/application/test_use_cases.py +++ b/tests/unit/application/test_use_cases.py @@ -44,6 +44,11 @@ def build_fake_graph(package_name: str) -> grimp.ImportGraph: importer=f"{SOME_MODULE}.blue.delta", imported=f"{SOME_MODULE}.red.epsilon", ) + # Add a cycle. + graph.add_import( + importer=f"{SOME_MODULE}.red.epsilon", + imported=f"{SOME_MODULE}.blue.alpha", + ) return graph @@ -87,6 +92,7 @@ def test_draw_graph(self): Edge("mypackage.foo.blue", "mypackage.foo.green", ""), Edge("mypackage.foo.green", "mypackage.foo.yellow", ""), Edge("mypackage.foo.blue", "mypackage.foo.red", ""), + Edge("mypackage.foo.red", "mypackage.foo.blue", ""), } def test_draw_graph_show_import_totals(self): @@ -112,4 +118,5 @@ def test_draw_graph_show_import_totals(self): Edge("mypackage.foo.blue", "mypackage.foo.green", "1"), Edge("mypackage.foo.green", "mypackage.foo.yellow", "1"), Edge("mypackage.foo.blue", "mypackage.foo.red", "4"), + Edge("mypackage.foo.red", "mypackage.foo.blue", "1"), } From 793424c8f51ac23e1a4216f06ecd6295c50e65a7 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 08:10:15 +0000 Subject: [PATCH 07/12] Refactor use case --- src/impulse/application/use_cases.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/impulse/application/use_cases.py b/src/impulse/application/use_cases.py index 257505c..595484d 100644 --- a/src/impulse/application/use_cases.py +++ b/src/impulse/application/use_cases.py @@ -26,27 +26,42 @@ def draw_graph( # Add current directory to the path, as this doesn't happen automatically. sys_path.insert(0, current_directory) + # Build a Grimp graph, squashing the children. module = grimp.Module(module_name) - graph = build_graph(module.package_name) - module_children = graph.find_children(module.name) + grimp_graph = build_graph(module.package_name) + dot = _build_dot(grimp_graph, module_name, show_import_totals) + + viewer.view(dot) + + +def _build_dot( + grimp_graph: grimp.ImportGraph, + module_name: str, + show_import_totals: bool, +) -> dotfile.DotGraph: dot = dotfile.DotGraph(title=module_name) + module_children = grimp_graph.find_children(module_name) + for module_child in module_children: dot.add_node(module_child) # Dependencies between children. for upstream, downstream in itertools.permutations(module_children, r=2): - if graph.direct_import_exists(importer=downstream, imported=upstream, as_packages=True): + if grimp_graph.direct_import_exists( + importer=downstream, imported=upstream, as_packages=True + ): if show_import_totals: number_of_imports = _count_imports_between_packages( - graph, importer=downstream, imported=upstream + grimp_graph, importer=downstream, imported=upstream ) label = str(number_of_imports) else: label = "" dot.add_edge(dotfile.Edge(source=downstream, destination=upstream, label=label)) - viewer.view(dot) + + return dot def _count_imports_between_packages( From ad67650791e2c1fdf91ee5602accc5d9b71d44b1 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 08:42:21 +0000 Subject: [PATCH 08/12] Refactor to use strategy pattern --- src/impulse/application/use_cases.py | 113 ++++++++++++++++++++------- src/impulse/dotfile.py | 2 +- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/src/impulse/application/use_cases.py b/src/impulse/application/use_cases.py index 595484d..68af463 100644 --- a/src/impulse/application/use_cases.py +++ b/src/impulse/application/use_cases.py @@ -1,3 +1,4 @@ +from collections.abc import Set from typing import Callable import itertools import grimp @@ -26,7 +27,6 @@ def draw_graph( # Add current directory to the path, as this doesn't happen automatically. sys_path.insert(0, current_directory) - # Build a Grimp graph, squashing the children. module = grimp.Module(module_name) grimp_graph = build_graph(module.package_name) @@ -35,43 +35,102 @@ def draw_graph( viewer.view(dot) -def _build_dot( - grimp_graph: grimp.ImportGraph, - module_name: str, - show_import_totals: bool, -) -> dotfile.DotGraph: - dot = dotfile.DotGraph(title=module_name) +class _DotGraphBuildStrategy: + def build(self, module_name: str, grimp_graph: grimp.ImportGraph) -> dotfile.DotGraph: + children = grimp_graph.find_children(module_name) + + self.prepare_graph(grimp_graph, children) + + dot = dotfile.DotGraph(title=module_name) + for child in children: + dot.add_node(child) + for upstream, downstream in itertools.permutations(children, r=2): + if edge := self.build_edge(grimp_graph, upstream, downstream): + dot.add_edge(edge) - module_children = grimp_graph.find_children(module_name) + return dot - for module_child in module_children: - dot.add_node(module_child) + def prepare_graph(self, grimp_graph: grimp.ImportGraph, children: Set[str]) -> None: + pass - # Dependencies between children. - for upstream, downstream in itertools.permutations(module_children, r=2): + def build_edge( + self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str + ) -> dotfile.Edge | None: + raise NotImplementedError + + +class _ModuleSquashingBuildStrategy(_DotGraphBuildStrategy): + """Fast builder for when we don't need additional data about the imports.""" + + def prepare_graph(self, grimp_graph: grimp.ImportGraph, children: Set[str]) -> None: + for child in children: + grimp_graph.squash_module(child) + + def build_edge( + self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str + ) -> dotfile.Edge | None: + if grimp_graph.direct_import_exists(importer=downstream, imported=upstream): + return dotfile.Edge(source=downstream, destination=upstream) + return None + + +class _ImportExpressionBuildStrategy(_DotGraphBuildStrategy): + """Slower builder for when we want to work on the whole graph, + without squashing children. + """ + + def __init__(self, show_import_totals: bool) -> None: + self.show_import_totals = show_import_totals + + def build_edge( + self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str + ) -> dotfile.Edge | None: if grimp_graph.direct_import_exists( importer=downstream, imported=upstream, as_packages=True ): - if show_import_totals: - number_of_imports = _count_imports_between_packages( + if self.show_import_totals: + number_of_imports = self._count_imports_between_packages( grimp_graph, importer=downstream, imported=upstream ) label = str(number_of_imports) else: label = "" - dot.add_edge(dotfile.Edge(source=downstream, destination=upstream, label=label)) + return dotfile.Edge(source=downstream, destination=upstream, label=label) + return None - return dot + @staticmethod + def _count_imports_between_packages( + graph: grimp.ImportGraph, *, importer: str, imported: str + ) -> int: + return ( + len(graph.find_matching_direct_imports(import_expression=f"{importer} -> {imported}")) + + len( + graph.find_matching_direct_imports( + import_expression=f"{importer} -> {imported}.**" + ) + ) + + len( + graph.find_matching_direct_imports( + import_expression=f"{importer}.** -> {imported}" + ) + ) + + len( + graph.find_matching_direct_imports( + import_expression=f"{importer}.** -> {imported}.**" + ) + ) + ) -def _count_imports_between_packages( - graph: grimp.ImportGraph, *, importer: str, imported: str -) -> int: - return ( - len(graph.find_matching_direct_imports(import_expression=f"{importer} -> {imported}")) - + len(graph.find_matching_direct_imports(import_expression=f"{importer} -> {imported}.**")) - + len(graph.find_matching_direct_imports(import_expression=f"{importer}.** -> {imported}")) - + len( - graph.find_matching_direct_imports(import_expression=f"{importer}.** -> {imported}.**") - ) - ) +def _build_dot( + grimp_graph: grimp.ImportGraph, + module_name: str, + show_import_totals: bool, +) -> dotfile.DotGraph: + strategy: _DotGraphBuildStrategy + if show_import_totals: + strategy = _ImportExpressionBuildStrategy(show_import_totals=True) + else: + strategy = _ModuleSquashingBuildStrategy() + + return strategy.build(module_name, grimp_graph) diff --git a/src/impulse/dotfile.py b/src/impulse/dotfile.py index 7d2f846..10f5f3c 100644 --- a/src/impulse/dotfile.py +++ b/src/impulse/dotfile.py @@ -6,7 +6,7 @@ class Edge: source: str destination: str - label: str + label: str = "" emphasized: bool = False def __str__(self) -> str: From 2fbca62306b08030f3c0a2d6a67ef38f3bac60f7 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 09:09:23 +0000 Subject: [PATCH 09/12] Add support for cycle breakers to use case --- pyproject.toml | 2 +- src/impulse/application/use_cases.py | 70 ++++++- src/impulse/cli.py | 1 + src/impulse/dotfile.py | 7 +- tests/unit/application/test_use_cases.py | 58 ++++-- uv.lock | 226 +++++++++++------------ 6 files changed, 225 insertions(+), 139 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b5792ec..eeaa928 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ requires-python = ">=3.9" dependencies = [ "click>=6", - "grimp>=3.12", + "grimp==3.13", "pytest>=8.4.2", ] classifiers = [ diff --git a/src/impulse/application/use_cases.py b/src/impulse/application/use_cases.py index 68af463..da9c2b6 100644 --- a/src/impulse/application/use_cases.py +++ b/src/impulse/application/use_cases.py @@ -8,6 +8,7 @@ def draw_graph( module_name: str, show_import_totals: bool, + show_cycle_breakers: bool, sys_path: list[str], current_directory: str, build_graph: Callable[[str], grimp.ImportGraph], @@ -18,6 +19,7 @@ def draw_graph( Args: module_name: the package or subpackage name of any importable Python package. show_import_totals: whether to label the arrows with the total number of imports they represent. + show_cycle_breakers: marks a set of dependencies that, if removed, would make the graph acyclic. sys_path: the sys.path list (or a test double). current_directory: the current working directory. build_graph: the function which builds the graph of the supplied package @@ -30,7 +32,7 @@ def draw_graph( module = grimp.Module(module_name) grimp_graph = build_graph(module.package_name) - dot = _build_dot(grimp_graph, module_name, show_import_totals) + dot = _build_dot(grimp_graph, module_name, show_import_totals, show_cycle_breakers) viewer.view(dot) @@ -41,7 +43,7 @@ def build(self, module_name: str, grimp_graph: grimp.ImportGraph) -> dotfile.Dot self.prepare_graph(grimp_graph, children) - dot = dotfile.DotGraph(title=module_name) + dot = dotfile.DotGraph(title=module_name, concentrate=self.should_concentrate()) for child in children: dot.add_node(child) for upstream, downstream in itertools.permutations(children, r=2): @@ -50,6 +52,9 @@ def build(self, module_name: str, grimp_graph: grimp.ImportGraph) -> dotfile.Dot return dot + def should_concentrate(self) -> bool: + return True + def prepare_graph(self, grimp_graph: grimp.ImportGraph, children: Set[str]) -> None: pass @@ -79,8 +84,46 @@ class _ImportExpressionBuildStrategy(_DotGraphBuildStrategy): without squashing children. """ - def __init__(self, show_import_totals: bool) -> None: + def __init__( + self, *, module_name: str, show_import_totals: bool, show_cycle_breakers: bool + ) -> None: + self.module_name = module_name self.show_import_totals = show_import_totals + self.show_cycle_breakers = show_cycle_breakers + self.cycle_breakers: set[tuple[str, str]] | None = None + + def should_concentrate(self) -> bool: + # We need to see edge direction emphasized separately. + return not (self.show_import_totals or self.show_cycle_breakers) + + def prepare_graph(self, grimp_graph: grimp.ImportGraph, children: Set[str]) -> None: + super().prepare_graph(grimp_graph, children) + + if self.show_cycle_breakers: + self.cycle_breakers = self._get_coarse_grained_cycle_breakers(grimp_graph, children) + + def _get_coarse_grained_cycle_breakers( + self, grimp_graph: grimp.ImportGraph, children: Set[str] + ) -> set[tuple[str, str]]: + # In the form (importer, imported). + coarse_grained_cycle_breakers: set[tuple[str, str]] = set() + + for fine_grained_cycle_breaker in grimp_graph.nominate_cycle_breakers(self.module_name): + importer, imported = fine_grained_cycle_breaker + importer_ancestor = self._get_self_or_ancestor(candidate=importer, ancestors=children) + imported_ancestor = self._get_self_or_ancestor(candidate=imported, ancestors=children) + + if importer_ancestor and imported_ancestor: + coarse_grained_cycle_breakers.add((importer_ancestor, imported_ancestor)) + + return coarse_grained_cycle_breakers + + @staticmethod + def _get_self_or_ancestor(candidate: str, ancestors: Set[str]) -> str | None: + for ancestor in ancestors: + if candidate == ancestor or candidate.startswith(f"{ancestor}."): + return ancestor + return None def build_edge( self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str @@ -95,7 +138,17 @@ def build_edge( label = str(number_of_imports) else: label = "" - return dotfile.Edge(source=downstream, destination=upstream, label=label) + + if self.show_cycle_breakers: + assert self.cycle_breakers is not None + is_cycle_breaker = (downstream, upstream) in self.cycle_breakers + emphasized = is_cycle_breaker + else: + emphasized = False + + return dotfile.Edge( + source=downstream, destination=upstream, label=label, emphasized=emphasized + ) return None @staticmethod @@ -126,10 +179,15 @@ def _build_dot( grimp_graph: grimp.ImportGraph, module_name: str, show_import_totals: bool, + show_cycle_breakers: bool, ) -> dotfile.DotGraph: strategy: _DotGraphBuildStrategy - if show_import_totals: - strategy = _ImportExpressionBuildStrategy(show_import_totals=True) + if show_import_totals or show_cycle_breakers: + strategy = _ImportExpressionBuildStrategy( + module_name=module_name, + show_import_totals=show_import_totals, + show_cycle_breakers=show_cycle_breakers, + ) else: strategy = _ModuleSquashingBuildStrategy() diff --git a/src/impulse/cli.py b/src/impulse/cli.py index 2caa9e1..53ad0ce 100644 --- a/src/impulse/cli.py +++ b/src/impulse/cli.py @@ -24,6 +24,7 @@ def drawgraph(module_name: str, show_import_totals: bool) -> None: use_cases.draw_graph( module_name=module_name, show_import_totals=show_import_totals, + show_cycle_breakers=False, sys_path=sys.path, current_directory=os.getcwd(), build_graph=grimp.build_graph, diff --git a/src/impulse/dotfile.py b/src/impulse/dotfile.py index 10f5f3c..fa40d2f 100644 --- a/src/impulse/dotfile.py +++ b/src/impulse/dotfile.py @@ -17,7 +17,7 @@ def _render_attrs(self) -> str: if self.label: attrs["label"] = self.label if self.emphasized: - attrs["color"] = "red" + attrs["style"] = "dashed" if attrs: joined_attrs = ", ".join([f'{key}="{value}"' for key, value in attrs.items()]) return f" [{joined_attrs}]" @@ -32,10 +32,11 @@ class DotGraph: https://en.wikipedia.org/wiki/DOT_(graph_description_language) """ - def __init__(self, title: str) -> None: + def __init__(self, title: str, concentrate: bool = True) -> None: self.title = title self.nodes: set[str] = set() self.edges: set[Edge] = set() + self.concentrate = concentrate def add_node(self, name: str) -> None: self.nodes.add(name) @@ -47,7 +48,7 @@ def render(self) -> str: # concentrate=true means that we merge the lines together. return dedent(f"""digraph {{ node [fontname=helvetica] - concentrate=true + {"concentrate=true" if self.concentrate else ""} {self._render_nodes()} {self._render_edges()} }}""") diff --git a/tests/unit/application/test_use_cases.py b/tests/unit/application/test_use_cases.py index 903e978..026dcea 100644 --- a/tests/unit/application/test_use_cases.py +++ b/tests/unit/application/test_use_cases.py @@ -3,7 +3,6 @@ from impulse import dotfile from impulse.dotfile import Edge import grimp -from grimp.adaptors.graph import ImportGraph from impulse import ports SOME_ROOT_PACKAGE = "mypackage" @@ -11,7 +10,7 @@ def build_fake_graph(package_name: str) -> grimp.ImportGraph: - graph = ImportGraph() + graph = grimp.ImportGraph() graph.add_module(package_name) graph.add_module(SOME_MODULE) @@ -71,6 +70,7 @@ def test_draw_graph(self): use_cases.draw_graph( SOME_MODULE, show_import_totals=False, + show_cycle_breakers=False, sys_path=sys_path, current_directory=current_directory, build_graph=build_fake_graph, @@ -82,6 +82,7 @@ def test_draw_graph(self): # The image generation function was called. assert viewer.called_with_dot, "Viewer not called." assert viewer.called_with_dot.title == SOME_MODULE + assert viewer.called_with_dot.concentrate is True assert viewer.called_with_dot.nodes == { "mypackage.foo.green", "mypackage.foo.blue", @@ -89,10 +90,10 @@ def test_draw_graph(self): "mypackage.foo.red", } assert viewer.called_with_dot.edges == { - Edge("mypackage.foo.blue", "mypackage.foo.green", ""), - Edge("mypackage.foo.green", "mypackage.foo.yellow", ""), - Edge("mypackage.foo.blue", "mypackage.foo.red", ""), - Edge("mypackage.foo.red", "mypackage.foo.blue", ""), + Edge("mypackage.foo.blue", "mypackage.foo.green"), + Edge("mypackage.foo.green", "mypackage.foo.yellow"), + Edge("mypackage.foo.blue", "mypackage.foo.red"), + Edge("mypackage.foo.red", "mypackage.foo.blue"), } def test_draw_graph_show_import_totals(self): @@ -101,22 +102,47 @@ def test_draw_graph_show_import_totals(self): use_cases.draw_graph( SOME_MODULE, show_import_totals=True, + show_cycle_breakers=False, sys_path=[], current_directory="/cwd", build_graph=build_fake_graph, viewer=viewer, ) - assert viewer.called_with_dot.title == SOME_MODULE - assert viewer.called_with_dot.nodes == { - "mypackage.foo.green", - "mypackage.foo.blue", - "mypackage.foo.yellow", - "mypackage.foo.red", + assert viewer.called_with_dot.concentrate is False + assert viewer.called_with_dot.edges == { + Edge("mypackage.foo.blue", "mypackage.foo.green", label="1"), + Edge("mypackage.foo.green", "mypackage.foo.yellow", label="1"), + Edge("mypackage.foo.blue", "mypackage.foo.red", label="4"), + Edge("mypackage.foo.red", "mypackage.foo.blue", label="1"), } + + def test_draw_graph_show_cycle_breakers(self): + viewer = SpyGraphViewer() + + use_cases.draw_graph( + SOME_MODULE, + show_import_totals=False, + show_cycle_breakers=True, + sys_path=[], + current_directory="/cwd", + build_graph=build_fake_graph, + viewer=viewer, + ) + + assert viewer.called_with_dot.concentrate is False assert viewer.called_with_dot.edges == { - Edge("mypackage.foo.blue", "mypackage.foo.green", "1"), - Edge("mypackage.foo.green", "mypackage.foo.yellow", "1"), - Edge("mypackage.foo.blue", "mypackage.foo.red", "4"), - Edge("mypackage.foo.red", "mypackage.foo.blue", "1"), + Edge( + "mypackage.foo.blue", + "mypackage.foo.green", + ), + Edge( + "mypackage.foo.green", + "mypackage.foo.yellow", + ), + Edge( + "mypackage.foo.blue", + "mypackage.foo.red", + ), + Edge("mypackage.foo.red", "mypackage.foo.blue", emphasized=True), } diff --git a/uv.lock b/uv.lock index 6ffb963..5ded3cb 100644 --- a/uv.lock +++ b/uv.lock @@ -261,123 +261,123 @@ wheels = [ [[package]] name = "grimp" -version = "3.12" +version = "3.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/a4/463903a1cfbc19d3e7125d6614bb900df2b34dd675c7d93544d154819d2b/grimp-3.12.tar.gz", hash = "sha256:1a733b1d719c42bd2fada58240975fa7d09936b57120c34b64cfb31e42701010", size = 845594, upload-time = "2025-10-09T09:51:02.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/b3/ff0d704cdc5cf399d74aabd2bf1694d4c4c3231d4d74b011b8f39f686a86/grimp-3.13.tar.gz", hash = "sha256:759bf6e05186e6473ee71af4119ec181855b2b324f4fcdd78dee9e5b59d87874", size = 847508, upload-time = "2025-10-29T13:04:57.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/88/37d9a1d2498190807ef593ef9e082208209ad1962db34587d1bde470e3e4/grimp-3.12-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5df1383d70606448ec095c6651974a2df070d3958ea00196042829408ad87e66", size = 2061955, upload-time = "2025-10-09T09:49:55.712Z" }, - { url = "https://files.pythonhosted.org/packages/20/8f/c58083fc367fbe768e95ec42c886106aa8fda82ffd765a05bfb6397dc650/grimp-3.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f2216a08026a23f03ab5ce0681837b5727aa4ed7b367062a313e382372e42558", size = 1981207, upload-time = "2025-10-09T09:49:47.892Z" }, - { url = "https://files.pythonhosted.org/packages/13/1d/79be86b43e3cacbd510d5cddf0a50c2cd11d0dfef524e973621e6512275c/grimp-3.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33b9c2fb3e2515df7147bfea50f054e5de11c13b227470259649a80c4508cae0", size = 2130772, upload-time = "2025-10-09T09:48:33.743Z" }, - { url = "https://files.pythonhosted.org/packages/a7/34/9fce25da7669c17ed039fdf9c31559afd48a26ba020c2f31a67ceea43333/grimp-3.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5daa9dcd41228e46ccd07b7366139cad02eadf9d137ff5767ece4a1cf6478703", size = 2092256, upload-time = "2025-10-09T09:48:50.353Z" }, - { url = "https://files.pythonhosted.org/packages/fb/80/88e34b8c8ec17e162f18bc09329247633478daf94024646ef8eb981ee85e/grimp-3.12-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd74e1d344748b3726a2db57c685a733b7108774be08f78bd921dbc175b943", size = 2241118, upload-time = "2025-10-09T09:49:29.677Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6f/73d81021df42ff2bcb43311b5a46b40781a2b4bb765254b0893b52b207db/grimp-3.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43fee43c5d7da591bc2fc80fcd02c1102cbb07821e277becf88fb1870b008a52", size = 2423515, upload-time = "2025-10-09T09:49:03.984Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/7819f8e07e5b261549ecf9dfe3d95a84729f3b1af3abd236e2658921fd24/grimp-3.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be8ad098c7f1462e95e692b837112721016ea6b0abd451f8c08bdc791728ae29", size = 2303898, upload-time = "2025-10-09T09:49:17.466Z" }, - { url = "https://files.pythonhosted.org/packages/af/22/479f729ca6b866e56d614e3107545664b7e8e1cba5c263f9961edf3f78ad/grimp-3.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:797d8cea180222b64d24bcfca6337bac5cf63a5a1c5bfd654c9c324c9d3a1fc1", size = 2169286, upload-time = "2025-10-09T09:49:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/d0/01/8c7e004a2e29def0aa57a10b0bbfd5e18b42857d40649e23210adcf311de/grimp-3.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:268da8ddacf35fb7febd1a01d491ec40e23b2a2a060dd7f6b3b5ed2e438107f8", size = 2311336, upload-time = "2025-10-09T09:50:03.798Z" }, - { url = "https://files.pythonhosted.org/packages/e8/54/dc8068ec7e6784c7a11ddda5908fe387124dcf1b981708eb0813dcdc96af/grimp-3.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6bb73c8c33487851063f64b0c0e33f8d2abd162e6b240ff31a0d87718dcb2104", size = 2354002, upload-time = "2025-10-09T09:50:16.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/87/31ace17fd09a67feb39bdd9bcc4e2f563ec02afee5e1f64d768aa168aeb3/grimp-3.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d3039fa8e5656faa65533928d8a39a7cce48c39c875a03a71b93ff06d15eed73", size = 2350155, upload-time = "2025-10-09T09:50:30.555Z" }, - { url = "https://files.pythonhosted.org/packages/c7/02/b0e8738493da9154bac5d89870aafeefb60f488eb91b2dc6154ec48f2881/grimp-3.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a60f5d190140829d58aef8642906511c77cc130495a0c7e07c7d1b76284f40d7", size = 2361754, upload-time = "2025-10-09T09:50:48.399Z" }, - { url = "https://files.pythonhosted.org/packages/90/ab/d85c9e5eeb715150b182c93b1f1ff81088694b2ed2fd96a99afd5a83e2bf/grimp-3.12-cp310-cp310-win32.whl", hash = "sha256:684272675ae0c6ef5030e9b584c47d5f8ac04cecda5db37fadb9025e073216f9", size = 1749490, upload-time = "2025-10-09T09:51:12.105Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d5/fac594f04fa10d19901be7c0ee1d7a29222c005398d367209ba92a6184ee/grimp-3.12-cp310-cp310-win_amd64.whl", hash = "sha256:d63da104af326de30ec30b66cea4835e9695691812e19edab39ca697a2e72cfa", size = 1850980, upload-time = "2025-10-09T09:51:03.202Z" }, - { url = "https://files.pythonhosted.org/packages/0f/b5/1c89600bf181d41502aed51b73b3a5889158dee35c534f51df3666779587/grimp-3.12-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e6c02e51eebfcf71146d42f47c9ce353ac1902ae446e18d0e663ab9fdaa0496c", size = 2062043, upload-time = "2025-10-09T09:49:57.035Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/bab32c5e26949a82299853ccb28ee30a7899d0355b0d209b535eb03bc04e/grimp-3.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79bc2b0ff6072c43c0ddc4479b25b7a8198795486478cfe3be0503b2c7d32c7f", size = 1981378, upload-time = "2025-10-09T09:49:49.237Z" }, - { url = "https://files.pythonhosted.org/packages/b5/03/b9f7e465488e8593de9a1e88355c3cfba04c02c3a34a6b02cbe946e0d587/grimp-3.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3986f11a9dd4167a2943cf6e80b458c0a825b48609713736cc8f2de135000810", size = 2130579, upload-time = "2025-10-09T09:48:36.035Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d0/81c776327354f32f86f321dd8468b32ba6b52dc3511d912d24c4fac96da4/grimp-3.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a2abe55844f9dad25499ff9456d680496f390d160b6b3a4e5aeabc0183813b4", size = 2091201, upload-time = "2025-10-09T09:48:52.57Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/116ac4c1e4407a123fba4bb076b2e880643d70b3f4f1621c3323b5d66e12/grimp-3.12-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e59112d0f557335b619bcf10263d11873579230bd3df4a4b19224ec18e7212d6", size = 2240782, upload-time = "2025-10-09T09:49:30.915Z" }, - { url = "https://files.pythonhosted.org/packages/06/7f/89bbec1241a8504499975f0f08befea0cf3d27c52f9808602fff8075c639/grimp-3.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b858e2e5a489c36710322970aa82bfbd3f1c4107c8564960629a59d2f17a53d0", size = 2423143, upload-time = "2025-10-09T09:49:05.18Z" }, - { url = "https://files.pythonhosted.org/packages/86/d7/2f416439b624b2a91bf2e0e456f58d74d51aa7ad239099cf4a8911d952c0/grimp-3.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d46cc1222dd301e0be371b97f0cdecae178089704e8a285e3edd4750ec46270a", size = 2303850, upload-time = "2025-10-09T09:49:19.073Z" }, - { url = "https://files.pythonhosted.org/packages/60/bd/8c2f48c26151eb9a65bc41f01004b43cb1b31791ffb61758d40d2f6b485a/grimp-3.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef06822f75856af28e7fcc580034043c543b1c99b07d2bd467bd173a7f10691", size = 2168571, upload-time = "2025-10-09T09:49:39.844Z" }, - { url = "https://files.pythonhosted.org/packages/5a/45/01a839434ff88be24317aa52cc1ba158833bd1d071efe0da1b14838af024/grimp-3.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4c19f1cba8a95c898473dd18f9c81358019d67f87f140b0b8401550e6d21c5a3", size = 2310869, upload-time = "2025-10-09T09:50:05.153Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7b/0dc45fdc15562c2faf8a95a8685d3805d27decdef6fcfb66d9b577ed2f12/grimp-3.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:600e8dbc1cd9c6decbc22089730221c65591b7ba5f89751d07fc7ad014d99aa1", size = 2353397, upload-time = "2025-10-09T09:50:17.755Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ec/07734ecc4f1489ffc071417f7bc881c939bcfdfba10eb585bce510ede1b2/grimp-3.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:259ba53b82cfb9c2c2d097b2237970c4e9903fa2d0b664b7e12329d9a64924f9", size = 2350166, upload-time = "2025-10-09T09:50:32.237Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f5/45d80e2fa205066a484f0c1a667a249408a49bb3b665d62677f879920aa0/grimp-3.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a593549b1f66b1c12574e71f9e8c0073b372888c6b6706e2617bba2713ae28c2", size = 2360590, upload-time = "2025-10-09T09:50:49.961Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f2/7ab1bc4d613189183c17741ff0d03490d9749eb5130b8b56e82ed77098b0/grimp-3.12-cp311-cp311-win32.whl", hash = "sha256:356ee969443f06c6c3a270f5a7221f946f0cb135a8b8ece2009990b293504bb3", size = 1748183, upload-time = "2025-10-09T09:51:13.503Z" }, - { url = "https://files.pythonhosted.org/packages/91/62/195f37a68d07fab40c8934ae8e39f9ff1f9a5bf3e375059b9cf14ccba302/grimp-3.12-cp311-cp311-win_amd64.whl", hash = "sha256:75e1f0d74f3a242a1c34e464d775c36b1c8b9d8c92b35f46f221e73e9b2f0065", size = 1851099, upload-time = "2025-10-09T09:51:04.747Z" }, - { url = "https://files.pythonhosted.org/packages/12/ac/0f55980a59c07439a965d3975f1cf3a6574f7d773910b9d6924790e0dddf/grimp-3.12-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:af399fc0ffddfbd7ea6c2e8546be1ab5284ee800f15a445705bdda5d63501b34", size = 2058862, upload-time = "2025-10-09T09:49:58.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b1/5fdcb1db7cb3253c78d87a0b8c3f7f9c5214b273861300b51c897c55e6b8/grimp-3.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f08358acbaf9a4b324537bf344fd2d76b5f9b6f1bfaf9a431e9453fc0eaee5f", size = 1977586, upload-time = "2025-10-09T09:49:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b9/e5f6d265b71430f9641daa9476cde8c23549e396c558b39a0bdc7fee824f/grimp-3.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eeb1616cafe9074fcb390fcfc01e6e5a0e0ddd5acb9dd37579985b2879c239a", size = 2130610, upload-time = "2025-10-09T09:48:38.472Z" }, - { url = "https://files.pythonhosted.org/packages/da/e1/2d0601c9aac2ab7340504e85ca4cd55f2991501a03e421bec78f53a07478/grimp-3.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99e648e299f7cd3daaee2cb745192e7ea159c7d38df76b4dcca12a2ef68a3ede", size = 2092775, upload-time = "2025-10-09T09:48:53.841Z" }, - { url = "https://files.pythonhosted.org/packages/db/a1/e63315477127ed8f31a1a93911d084bf704d6e126ca27650e3c3389701a6/grimp-3.12-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b24c5ce351030d1f83e69acd76a06863dd87041ceb25572339f7334e210cbc4", size = 2239336, upload-time = "2025-10-09T09:49:32.185Z" }, - { url = "https://files.pythonhosted.org/packages/f2/09/cd76d35121f053a95a58fc5830756c62e5c9de74aa4e16b4dc27ce6ada2c/grimp-3.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd40a5ec09d1dfafaae88b53231ab79378183e2e9a03e7b26b7a30133d027d8a", size = 2421851, upload-time = "2025-10-09T09:49:06.893Z" }, - { url = "https://files.pythonhosted.org/packages/40/46/e8390a7c5ed85b4dbeff4e873f1ece8d9acf72d72f084b397ccc2facfa3b/grimp-3.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aebdfad66d6f4e8b0f7364ce0429d208be3510918097f969428165074d3103e", size = 2304849, upload-time = "2025-10-09T09:49:20.695Z" }, - { url = "https://files.pythonhosted.org/packages/bd/81/f73edbc48a283f634233b6153ac43e4e7b9f58108ffc19da803b0015cb60/grimp-3.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76fd06be98d6bea9ea8a804da22c80accf1d277fe04abd5f3dff05d087f056f7", size = 2168655, upload-time = "2025-10-09T09:49:41.118Z" }, - { url = "https://files.pythonhosted.org/packages/84/1a/8fa5752f725b8872010627bd10e1aedccdb406c3b4118ec3fe127155284e/grimp-3.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a73a42a43e268ac5b196386beae1ec646f4572409e731bccf2a99ab4ed5c46bf", size = 2311124, upload-time = "2025-10-09T09:50:06.477Z" }, - { url = "https://files.pythonhosted.org/packages/83/a0/02d6b2a86289a4ac73f44f59aaee43c1dc936c984204c73d2affe4570eb6/grimp-3.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:af990af7d5e64f484d12cdefacfaaed4ea9418ac4d0a5a928953fd91aaf8df80", size = 2354216, upload-time = "2025-10-09T09:50:19.114Z" }, - { url = "https://files.pythonhosted.org/packages/7b/48/0368289f5bbdf943a48305824b30411b35ef2c7cd8edf2bad48d67b3897e/grimp-3.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:82ee28c1e9835572af2c733f7e5913a44193c53ae8ca488039164593b4a750fa", size = 2348372, upload-time = "2025-10-09T09:50:37.479Z" }, - { url = "https://files.pythonhosted.org/packages/26/73/b4f90b4926791d720f6069fc8c8b3e204721d1db839a1c00fbcee1e2a36d/grimp-3.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afdceaea00e305909cb30d68e91b94fcf71d1a7234052549ea31148785a03a52", size = 2361167, upload-time = "2025-10-09T09:50:51.733Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ae/94d34c732d531c7165c8942d7995495aac64e9bb5c28cc6751349eacdcde/grimp-3.12-cp312-cp312-win32.whl", hash = "sha256:40f8e048254d2437dffcd383d2301a82c35d9a3082e878b707d87a6e8c539614", size = 1747179, upload-time = "2025-10-09T09:51:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/5b/cd/48bc396ee2f36e72d5c50ba8b4d7f817fc2cdac7b9ab77d2b097f50a4447/grimp-3.12-cp312-cp312-win_amd64.whl", hash = "sha256:199172d17f22199bf400a0bd5c4985784622201e887a023fe799ca3f3437dedf", size = 1850691, upload-time = "2025-10-09T09:51:05.984Z" }, - { url = "https://files.pythonhosted.org/packages/63/ed/67dd73f74fba30a05265baadc99db8d52db04b90eb63b45c04c60bddaf67/grimp-3.12-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8a52a1f719b5b97e184eeeb1a22a7ad20960baf096b7fc2c3012d3378d4429ca", size = 2058986, upload-time = "2025-10-09T09:49:59.781Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5f/0ce92fc8e3c24a594363e92a442f718f0770408bdc62917deaea5c009231/grimp-3.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a46094a2cd7bbd172a4ebe846b42eefe626b29d298875108d9e59485284d181b", size = 1977620, upload-time = "2025-10-09T09:49:51.773Z" }, - { url = "https://files.pythonhosted.org/packages/33/12/c7e80a124fdc8154cd45c6c448c30a0bfbfa52dc094f22f63fa273ed6100/grimp-3.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447f0b1f6a9c7245367ddca7be4204213d3d20ff63487edbadb6e7d7b712f9c", size = 2130417, upload-time = "2025-10-09T09:48:40.089Z" }, - { url = "https://files.pythonhosted.org/packages/b0/96/2358f0f8288f9d531f96c2c109bd737401db382161ae64ce00e84ed754c7/grimp-3.12-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc7157c74416e8709db5d1e1051ceccbd7721bcdd8fe5983aea4ae88025d1e27", size = 2092507, upload-time = "2025-10-09T09:48:55.136Z" }, - { url = "https://files.pythonhosted.org/packages/06/eb/3768639f3d19037e828b08120a5e95eaa933318f47a21faebfbe82cf6666/grimp-3.12-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a5e3143f97a7803848c677f66868624865ef08e73b6638cfcc938152f5045c0", size = 2239042, upload-time = "2025-10-09T09:49:33.416Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/0f744cd15c948b440c558abd9a39f030361e09a13d6f982930560e2fe2c6/grimp-3.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbf3d1e51a45aef2bc2b31a9d97c7356a44aae82926b6a614015439223b9d945", size = 2423335, upload-time = "2025-10-09T09:49:08.21Z" }, - { url = "https://files.pythonhosted.org/packages/03/00/8b3ef543b5111bb25a90073f5b8101676cc998fc37f840c0dc768c368cf6/grimp-3.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99c1c9550404821f305e04e7008e890207b534efce086ecd0d5db1450eb8c0b0", size = 2304626, upload-time = "2025-10-09T09:49:21.918Z" }, - { url = "https://files.pythonhosted.org/packages/c7/6e/a52905c97b65f891ce6cc63d75438efdfdfbadc633ba727dca00b7b08371/grimp-3.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3db397a4c1d27578b6a0f0f6d53521eae693bcc8758cfca02e0ed73827fe3c3", size = 2168143, upload-time = "2025-10-09T09:49:42.388Z" }, - { url = "https://files.pythonhosted.org/packages/da/ce/7db15a400cfa46f877fcf6f553cbf3205fd0543a4e7a34bcf3c41b29b800/grimp-3.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e95c99896add10cb4180540bd970b3c540505516b2b85808bc70d3b160127f5c", size = 2310729, upload-time = "2025-10-09T09:50:07.856Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9e/08ac938c65d5547994f8884a7ba03f9cc28166a7562a21c0e223119decba/grimp-3.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:37888db154fca7d63cb27f7d866b5db02ff50281875a9410e2507d62bdecbddf", size = 2354012, upload-time = "2025-10-09T09:50:20.493Z" }, - { url = "https://files.pythonhosted.org/packages/cf/77/e45c1168365ece9c00416348ef7946964feb20155e3158d722cc2332f7f3/grimp-3.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:674191f4a2399b9ee15e7604055d0640b3d7120276bc48680eb935436ec8f7e2", size = 2347790, upload-time = "2025-10-09T09:50:38.797Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fc/3065b91400ccf86cecad5cd8eeda464446d2da582fabe6da3562e2e861fc/grimp-3.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8b0e87e6bf6def0651d0c13d5161337741044a9451db7f8e09fafc1d494e774", size = 2361100, upload-time = "2025-10-09T09:50:53.177Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/7585f2a2b87777941ca352f27df3d7b30c61d4a51be7c546719064302d95/grimp-3.12-cp313-cp313-win32.whl", hash = "sha256:3f5315758c3b731162d6c0e309f3aef3538d439a4dc4e718c0569fb02e87276e", size = 1747312, upload-time = "2025-10-09T09:51:16.526Z" }, - { url = "https://files.pythonhosted.org/packages/55/2e/c563249c9f9139a72fa45718fb51124aa6175fcacb7444d35ed602636b15/grimp-3.12-cp313-cp313-win_amd64.whl", hash = "sha256:964d878f72d5afa03adff0bfecc02ead51b754a2575d67a48334c6f5b1fd3c75", size = 1850489, upload-time = "2025-10-09T09:51:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ed/f17f45af9d31ec003b82d5f3bc3f6517bbadeed0d8112a5d2265aea50fe4/grimp-3.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a706c66b577751e6b0769b56e4056c34348af87db887dae762129bf7e8e2a2", size = 2126363, upload-time = "2025-10-09T09:48:41.692Z" }, - { url = "https://files.pythonhosted.org/packages/f0/fa/f1104beb3f1ee51967566f02a28ef999d679779992252eb283345ceb7793/grimp-3.12-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c85a9287aa667a4149565f74910c36a31c6025c481347c49f3e598f91c2634a", size = 2088211, upload-time = "2025-10-09T09:48:56.638Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6d/fc0c22dfea4ee54771fa94fbc9947ac0fcea2498d6a7b89b34135fdc6507/grimp-3.12-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0dfa79fa8ea37ea8c8bd76337ae09c87fa3e10f0af65f7a1bfa27c7d8b83154c", size = 2422799, upload-time = "2025-10-09T09:49:09.787Z" }, - { url = "https://files.pythonhosted.org/packages/cb/24/8a5101bfce368ce4d41694ce7f650eead8dfca7910b65a7d159e99769c9c/grimp-3.12-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef5cd5bc08f6c8f0698bc8f63560adb4320d87a644da8ff06c07c7db3f3fca37", size = 2302748, upload-time = "2025-10-09T09:49:23.149Z" }, - { url = "https://files.pythonhosted.org/packages/98/9a/a3098718652e53280a30e6ba4e3cf9049abec2ea10d603a0e743c5d4be6d/grimp-3.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:351ab71624f4eff3b32c8cc2e28aa6820ebf89d79cbb287b69d1ca2cf5991042", size = 2308077, upload-time = "2025-10-09T09:50:09.399Z" }, - { url = "https://files.pythonhosted.org/packages/79/86/03a6260b9337e7fffcd60204aefc673acf6e628974c053af80147cf8ed72/grimp-3.12-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:7bc3a7d00630f8cef3e23fc44a0a2c09dd889ea9934f179cfb90e07a6298c44c", size = 2351443, upload-time = "2025-10-09T09:50:23.133Z" }, - { url = "https://files.pythonhosted.org/packages/36/77/ab4b9004330136d04999e107a89788f7fd026afd75a66be262fb5ebac072/grimp-3.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2f9ba24c528ae64ae35db992b16b1041cc8bbb595a47c6400c472bcfa497e2be", size = 2347348, upload-time = "2025-10-09T09:50:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a1/6fc616342e7120c28de67201b165fa27a52460fc32fb3951997bee06e83a/grimp-3.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c68087cc491892efcfdda147076a61a6aea5ac5c9355beb843ebb6cb759095d3", size = 2361672, upload-time = "2025-10-09T09:50:54.704Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3c/6b9818b4351c60834a677c39ade430ad2ba2247c7943687f4a3a52927f00/grimp-3.12-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:19bcff63ebf742ccb120c2c1a9fdeba46d7822b7efe23f28ae20a77238834d6c", size = 2057514, upload-time = "2025-10-09T09:50:01.065Z" }, - { url = "https://files.pythonhosted.org/packages/41/15/c335200f219f4c3284f25763e899ad449e3bc39e19366254c0668777a12d/grimp-3.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5836d5e1b4740a0ed7f4d763099f1cb099fa8bcdb7a7a7e7818e61f355af323b", size = 1977131, upload-time = "2025-10-09T09:49:53.014Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b1/cb9fe7f74598d94b8a2e8e718f608dc2107717180ab19f2d16fa4aaf2e85/grimp-3.12-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:021de8d120b64a0e77eefafc13580124095f6cced03c2340c1ced3551f8fee93", size = 2240666, upload-time = "2025-10-09T09:49:34.623Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1a/b2e5b2f463a88ca41ed81f4cb64ec063f08ec5298ffd247accda50834dbf/grimp-3.12-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:faf8a61ac6b846675367fcbb3be1bf964ad2971c0ffe4982f43e77f089592b06", size = 2167891, upload-time = "2025-10-09T09:49:43.622Z" }, - { url = "https://files.pythonhosted.org/packages/94/42/6ba3df3e89d637134526d86442f810f402cb8acd81c1c3ac62e915326814/grimp-3.12-cp314-cp314-win32.whl", hash = "sha256:b993e00121f821cbfec2854193aba46c559a7c685af2d882c73c9e2cc7aff6a8", size = 1748804, upload-time = "2025-10-09T09:51:18.254Z" }, - { url = "https://files.pythonhosted.org/packages/ac/24/ddb548ed1d3c675b5c67816d7aa40b32e7cccac9ca764b55e5fb88164c7a/grimp-3.12-cp314-cp314-win_amd64.whl", hash = "sha256:92e222fabbe022639eb84fca1506fa5b99d8d0ac1ff35ed8fab1001fd702c27b", size = 1850991, upload-time = "2025-10-09T09:51:08.737Z" }, - { url = "https://files.pythonhosted.org/packages/a3/74/f97c416642868ccf20f3538959de22b215c0cd805f8b1830848b5eaccf30/grimp-3.12-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:feb98d9f2d96baf2055ad307a55e01a90aae5cdc5b8b655464fa46dd0e920697", size = 2063474, upload-time = "2025-10-09T09:50:02.557Z" }, - { url = "https://files.pythonhosted.org/packages/e6/29/814cbf127bbef22a174b210a2c7633821de9d15214bd13c7b8c40ad0b4da/grimp-3.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b724635c05d882fdb29ecdfa5ee277be1841b1dfe0b3f5d8d60c2b970714fa5", size = 1982385, upload-time = "2025-10-09T09:49:54.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/af/47c9f90ce416949bb10e0d5cbd50955d739e482bed284218b95aa6074b5f/grimp-3.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdde09ee480681d82ba4a866f370c65a17995214c55518af6dc1e82b7447caa9", size = 2132816, upload-time = "2025-10-09T09:48:43.248Z" }, - { url = "https://files.pythonhosted.org/packages/98/f3/f23473ff40d0e9832a9e72b0907cf693f0864c009e05c6dac29851bc5afa/grimp-3.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a7d8fdce440638de315d25d1b9ce8c3f929e1f17561adf85458ffb89d75e2094", size = 2093659, upload-time = "2025-10-09T09:48:58.223Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2a/8aeca6d369cee26b8d0bcb3973028fa059dfd47a6b55a24572c5a7b1148e/grimp-3.12-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a40164666a2d6e40440cf3e40bb19d7e63e5361b70224db7818dbe9950fd487", size = 2243906, upload-time = "2025-10-09T09:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/9a41b22a65474ab12b19182ac659d5ae985dd51cdf072c2a4cd4add44c71/grimp-3.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c35ea26b58f58d426745cecd66ae74e24261a94093e9ba99b69d8f7c79bda3f", size = 2426425, upload-time = "2025-10-09T09:49:11.112Z" }, - { url = "https://files.pythonhosted.org/packages/71/a3/95f17ad7d77a119631425a1ebc5f868146de920bf1d605954d5437881218/grimp-3.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2e46a25b6b01ae3cc93cb4c651b6f995e0b0e1f8053f09bdd324344e5d1950e", size = 2306210, upload-time = "2025-10-09T09:49:24.482Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/f7ab7ff71e56d7f3e5819d4a1b6846037bdecdfc15a1c6129b98c6c4aa42/grimp-3.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5172937a7a65db32c63832feb1a5deb6ee6abb9f594b20982b8a4357763250a5", size = 2170206, upload-time = "2025-10-09T09:49:44.991Z" }, - { url = "https://files.pythonhosted.org/packages/e4/08/19004b22213bc6acc1072e9082683237322fbafbe44f21ef36d7e6464c3d/grimp-3.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:54ede855ca703f7020a91d984f15bca4a3722fb62d5e438e0dd80b4eaa679091", size = 2313425, upload-time = "2025-10-09T09:50:11.05Z" }, - { url = "https://files.pythonhosted.org/packages/e9/17/da6b12caac0670b7457cc5369c73ed0b3e451bd058b79ba8b90e7e26b517/grimp-3.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c8a9e2f197c7dec8297627a0ddcd1c8e4cf62de3829c296c80f91afa6eaecec3", size = 2355482, upload-time = "2025-10-09T09:50:24.497Z" }, - { url = "https://files.pythonhosted.org/packages/af/b0/86e90f812a86a3ddcf346211cad1844ac2de65732d4b1533e56cb1cfaf91/grimp-3.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3e8088a05881bb975efcbf4ee31bfca7bd015fb2510658c40c3952469c56955f", size = 2351870, upload-time = "2025-10-09T09:50:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/a3/99/38498b500f2ac2cdbd3a28b26637c2bbb410e513a2a02e545828b279e386/grimp-3.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80f08f2ee0b9757d846f7f66521a489b053c6a0a46c48add21c76490e09e51ce", size = 2363445, upload-time = "2025-10-09T09:50:55.939Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2f/490250a726ffcaeb6817e0112cd9f0e851413b43c4ad9e71f2259854f912/grimp-3.12-cp39-cp39-win32.whl", hash = "sha256:cf6a34a7e6fedaf5c11c3bc24f9e6e36914d8193bc1ba51b00012202b1851b93", size = 1751022, upload-time = "2025-10-09T09:51:19.911Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/acfa8126b756d326705e165ce08ff1cbb7173b670fd69268adc391ee1ce2/grimp-3.12-cp39-cp39-win_amd64.whl", hash = "sha256:ff4476c7e79b50c7ba6324970da9425b27cb26e2890953568175a277a2168092", size = 1852085, upload-time = "2025-10-09T09:51:10.446Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bc/46ea3c9caf366847c4b010b71a0c52996033df0f486b1a6587adccf46f42/grimp-3.12-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3ba4e9f7a48baf65c2c1f5d06e4ac365d799c174d6ea1883621a163afd159a3", size = 2133434, upload-time = "2025-10-09T09:48:44.751Z" }, - { url = "https://files.pythonhosted.org/packages/0c/81/9ac948a77a1fa3f3a13c3693bfb71ee16a4fa982a6e414db90d3e5f851ba/grimp-3.12-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a2ac07b2ae00b9522c14eefac60590e1b8a61562c331579b1e534fcc7cbe0936", size = 2094460, upload-time = "2025-10-09T09:48:59.428Z" }, - { url = "https://files.pythonhosted.org/packages/97/83/6a58fb2abe68c41ebc4897777e88914a73b4774b0fb23322e1ce775c4311/grimp-3.12-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28213ba72d258b8817eb032a5d52b5bcb00f5e89fb670d660e57e70b3fa2f6b", size = 2423505, upload-time = "2025-10-09T09:49:12.402Z" }, - { url = "https://files.pythonhosted.org/packages/05/ef/b4be8904f76cf074557a8b489a2c0950310307e40b2af3ab37908b4714e5/grimp-3.12-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1641a7979a7253c8468b15172d5fd5b6c909ad00a78040408783a771b350618", size = 2305093, upload-time = "2025-10-09T09:49:25.681Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ea/c85eff54195f969cc67f4f4a07aed1d354d852e2eb6661cfd6ab470fe5a7/grimp-3.12-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3140e83980e1672fef3657ab92edd807bf4c5d80a8446253e78da7e1c604b031", size = 2313799, upload-time = "2025-10-09T09:50:12.248Z" }, - { url = "https://files.pythonhosted.org/packages/3d/35/e21a5a2a1cf6c1ecdcb6b027b8fb62d79e0b1f5c5ddd6ef279fef1cdd616/grimp-3.12-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fb805b993d50d856c64ee5c81dce6a11f19bf95a6676fe0138d4f54bf03bda27", size = 2356112, upload-time = "2025-10-09T09:50:25.775Z" }, - { url = "https://files.pythonhosted.org/packages/ab/7d/01f789845893f5cb45301df64ce5c00e1c9e059c63f7192d833a854af0f1/grimp-3.12-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3f89b445c5d6a94f3e9b1b63b79d883bfe619ee9477695c4e0fe6769dde99368", size = 2351801, upload-time = "2025-10-09T09:50:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/40/12/664f0134616993e047ded85dbd7bdd6f288e1a3738bee2fa6e017f006acb/grimp-3.12-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:719e4a62f4370d47e7d35a3a0300c4554ad24aa624cc53c61f1810adcb0190ec", size = 2364164, upload-time = "2025-10-09T09:50:57.259Z" }, - { url = "https://files.pythonhosted.org/packages/d9/31/c72e53a46692dc8358cff1af1a9494430a0fecd4c3f2d0d8e9c2eb5e828d/grimp-3.12-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:567d037a3db083e54bee621daba59a2e01fd1391364ae0a0c737995f6eed910b", size = 2131392, upload-time = "2025-10-09T09:48:46.857Z" }, - { url = "https://files.pythonhosted.org/packages/39/10/15e43be32734baaebeee090dca16f06ea5ba933b209b8e1c0d5986dabb32/grimp-3.12-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b4cc756c91c3d8582ee70b5e013c0e34fdb31c7f808cefe9d15509c45fec31e", size = 2092481, upload-time = "2025-10-09T09:49:00.754Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4a/c9349dee284c2d9384714741896f0f84a1d66011a69cdc364e4d94e188b1/grimp-3.12-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bd47f9a8619cb8966f18cb6faf5f6cb8d35ade99312477dd8e9de3a9ae4cb7", size = 2242260, upload-time = "2025-10-09T09:49:37.183Z" }, - { url = "https://files.pythonhosted.org/packages/d8/63/3935823f89c12320840bbf018858eeaca7d5285f9769a48921587a88adeb/grimp-3.12-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f30e01855c67a39857c87e6c0eafe5e8891010a35e06cf2145f2cfce8ea9780", size = 2422371, upload-time = "2025-10-09T09:49:14.616Z" }, - { url = "https://files.pythonhosted.org/packages/71/8e/5a75c2335a2dc61738b19318dcdd16392015a984211e3d0b9f6679dc6c89/grimp-3.12-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d07e825f6b052186dabd8dbbcc7e008a3b56e551725e2ba47169fe1e4bde76ac", size = 2304257, upload-time = "2025-10-09T09:49:26.908Z" }, - { url = "https://files.pythonhosted.org/packages/40/99/462d86bc9401a39859f272b867331a678f4b5324a539dc771bdae6d36309/grimp-3.12-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f1a1289d4282be2891ada75ec5d3099e856518c4236b1196e367b630485f8ce", size = 2169360, upload-time = "2025-10-09T09:49:46.575Z" }, - { url = "https://files.pythonhosted.org/packages/d0/07/6d2929f05dae189265633588819d990df35644ad74b6ec74207091dff18d/grimp-3.12-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:85136b555aeb7d3965fdb40af4e4af2011f911b0fde8c20979bf4db7b06455f5", size = 2312280, upload-time = "2025-10-09T09:50:13.491Z" }, - { url = "https://files.pythonhosted.org/packages/5c/47/7e49417e2c496da0b6141e711dca40726d2b30a0adc6db9d04b74c7bafa7/grimp-3.12-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:963efd6ec86e7b47fde835b2526b6be7a3f489857a1cd47a747c94b3e670550a", size = 2354449, upload-time = "2025-10-09T09:50:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/2c/08/2e1db56797e4e26334b3ee4ef1a5fbf56155d74a0318215ed4dcad02ef43/grimp-3.12-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:c9e2ee478b66f0e20c92af6123142ffd6b604c36e9b3a8d391ea9172cc18b6b3", size = 2350545, upload-time = "2025-10-09T09:50:45.623Z" }, - { url = "https://files.pythonhosted.org/packages/37/78/53594064f11b0ae9e72b3e9df5c055f00c5bff44962f7b777846504fc50d/grimp-3.12-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e8826362d4e403aa2e03d480e3e4d64284a6b6ccafc2c5777bb2bed2535bdc4e", size = 2361926, upload-time = "2025-10-09T09:50:58.605Z" }, - { url = "https://files.pythonhosted.org/packages/45/06/d03950c6539e9cfde3f3fe3b24a3a209c831d3807110bfc2e7b664ac1321/grimp-3.12-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5b935475db40664613bf6d3b6c9a6af7ea287db15ceef878b5583295396bd0", size = 2133014, upload-time = "2025-10-09T09:48:48.39Z" }, - { url = "https://files.pythonhosted.org/packages/8b/de/e706e94b515b4383285a123684798ca9c6f4a52277610c6942fe2a66864e/grimp-3.12-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b6799cdf9b9959aca3ecd034a4e332a69293f592a2325bfa60279ebfa90e1bf", size = 2094504, upload-time = "2025-10-09T09:49:02.31Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/45a6e1e21bb179c72b2d07a7762ea2e854eab0d5439090fcf337aaa8e13a/grimp-3.12-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94f412e23b5a55533078c2d8daa604eb3efd04ba8482ddd09342810ec19a21c3", size = 2425727, upload-time = "2025-10-09T09:49:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3e/d1df954ef608b3b8154c6bde87efdae58cb7c83f57793c1b9af902c18218/grimp-3.12-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45791f1cec0fea3a8c0fac6bea061b7b50c9a501a84442310723fb23825783c5", size = 2304712, upload-time = "2025-10-09T09:49:28.39Z" }, - { url = "https://files.pythonhosted.org/packages/38/bf/071f9afd9b78ab68e32f6124ab8e89b8b1c3488d067b2d04fc4a2140ea88/grimp-3.12-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:247770c6b966ed93b6ab1ce77f36deb2ad698dadea9cfee74a1c23abc89e08d3", size = 2313548, upload-time = "2025-10-09T09:50:14.928Z" }, - { url = "https://files.pythonhosted.org/packages/1b/89/a7519ef8dd1ddeb04491eb71f919b1ccd1f1aca407f154b69298a10905da/grimp-3.12-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:6587b7b048592c2ef369ef59d4e161588d8a54840501e2c18210f6322bdefba3", size = 2356379, upload-time = "2025-10-09T09:50:28.89Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c0/eb9c3d064926300989ea63ed1007ded19d2af20e26aeceb8d456ba22bbc5/grimp-3.12-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:1c97c25d70b7cd3044717d4efafac3ae67eca159a61f95c807a59c6c2b4c8b5c", size = 2351756, upload-time = "2025-10-09T09:50:46.985Z" }, - { url = "https://files.pythonhosted.org/packages/89/0d/d5d4e1dfeef37a7a434a2ced6da7c7f808d737682e6ff2ed87e10c60d509/grimp-3.12-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:95c4311a71648de0d7aa2533ddea569b2bc6796fd71752770cd45c3c5292a5f8", size = 2363869, upload-time = "2025-10-09T09:51:00.646Z" }, + { url = "https://files.pythonhosted.org/packages/4a/51/40eb636a196a56ae4cfa4d3d22e2b3ffe0a5c1b89e20c4d7b1b84c3aacf3/grimp-3.13-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0ad0b51c8216cdffd60c68b54f6f3c803a0729dbe476fe9cd81642f6f0a3008e", size = 2075249, upload-time = "2025-10-29T13:03:56.856Z" }, + { url = "https://files.pythonhosted.org/packages/2f/96/a654d58c80fda485d73c3f2bcdbe9c5890246ce9854997a98b9bf890fd1e/grimp-3.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c4b5382964055e2fbae38a9e67c1d805bd4368c87341eff09a2ef29a095f39e", size = 1988372, upload-time = "2025-10-29T13:03:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/91/b5/9234384775d2dba068334a0e2da4266b10c2e718d42dcf4d93e5af27d619/grimp-3.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87673986e66c0cfe5280744a5c28613453def1ddb849338c76a425f2eeab4460", size = 2145347, upload-time = "2025-10-29T13:02:32.966Z" }, + { url = "https://files.pythonhosted.org/packages/d0/33/7315cf6d45ac26fdb0012245313dc0fffa9302040701636dbf34b9803502/grimp-3.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbc8d42013036ce3cba4c919d8f2a10309f33e80dbbbc5b280814c7bb159a459", size = 2107232, upload-time = "2025-10-29T13:02:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5e/ea9e2c9fe2ea7f4e8cf113c5f3dfb1707012f90368156fc33decae819fc0/grimp-3.13-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b9a1ba890854b457a3df82f9c341c1f131e9617ee7ed8058e8016c57b963f8", size = 2256874, upload-time = "2025-10-29T13:03:26.516Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/5e48a7234bca52e1bcd6d177fc3378d879ec23d6b8b2831d69422188c03a/grimp-3.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50c6025df388d004dd34cbb3c4a914c8e933b0bdfeb44a5598b9126f27fc164e", size = 2443208, upload-time = "2025-10-29T13:03:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b0/73964fe0e4e792cef9449b0c1fe48b1f7ede219367c104694061aca5f872/grimp-3.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c9b89b62b6381dc8de1ca524c0764df00bd7f1d823892c8ac1e9c10a8e87ab4", size = 2317452, upload-time = "2025-10-29T13:03:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/53/f3/955acd1a5269ad3c98fbb4d58d2bf8d44e2cf0a7d733711cba8d6ef70e42/grimp-3.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dc76e193ff53cd9722148bde0fc327781ae4b58080316e900b6b29be2b62bd", size = 2179667, upload-time = "2025-10-29T13:03:39.568Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f0/9cc6485d0acd365f92dce559d542ed9efaa0187e1c73d41553ce76d02730/grimp-3.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b5884d0f94fd24b4ab6a51a81a830f4f0752e4c73f44dc579ac2ef5de186b599", size = 2327943, upload-time = "2025-10-29T13:04:05.371Z" }, + { url = "https://files.pythonhosted.org/packages/36/0a/eb10d8cfcc0f7e37d3a5906d179cc1940dac8d6fc510347706fc07d31ee2/grimp-3.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4523c7d4eaff96a37104cd9d2bedc0c805421144d81dac38ec975720eca5eded", size = 2368415, upload-time = "2025-10-29T13:04:20.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/08/bd01ac8fd143ebbc8121228c38aa47f257c899bdcde2e917b9e2d761b279/grimp-3.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e5be25e3bd5c2029ae8016a8baefcc4842baa7e49a613b91b45881673f1f7a6e", size = 2358744, upload-time = "2025-10-29T13:04:32.825Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b5/415243f32a55d825b35f6236a5e26bf72cadd312f7eb8cdc72ccae732048/grimp-3.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:876f6e5cfc8e92eef6b626cf9df1779be10cf84003363a9170322114688b24e5", size = 2381480, upload-time = "2025-10-29T13:04:45.617Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c1/693914d790e255c5426878f0307f8b7ff278a59a945bcdeb7b5b8edbdb54/grimp-3.13-cp310-cp310-win32.whl", hash = "sha256:fcb36fcf0a4756d9ce8fbcc3cf2bf6a7cfda83bf5a6c26e7e727b0cd9766a0a2", size = 1759335, upload-time = "2025-10-29T13:05:08.692Z" }, + { url = "https://files.pythonhosted.org/packages/e1/20/89bcbd400cca5e891a2da128889a24c6e182a3cfbce987c205fb210fb8f0/grimp-3.13-cp310-cp310-win_amd64.whl", hash = "sha256:cc8dc66768e903ca40b70691a8a9335045e622747ee94f11c8a413a7dbe52a46", size = 1859693, upload-time = "2025-10-29T13:04:59.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/cc/d272cf87728a7e6ddb44d3c57c1d3cbe7daf2ffe4dc76e3dc9b953b69ab1/grimp-3.13-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:57745996698932768274a2ed9ba3e5c424f60996c53ecaf1c82b75be9e819ee9", size = 2074518, upload-time = "2025-10-29T13:03:58.51Z" }, + { url = "https://files.pythonhosted.org/packages/06/11/31dc622c5a0d1615b20532af2083f4bba2573aebbba5b9d6911dfd60a37d/grimp-3.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca29f09710342b94fa6441f4d1102a0e49f0b463b1d91e43223baa949c5e9337", size = 1988182, upload-time = "2025-10-29T13:03:50.129Z" }, + { url = "https://files.pythonhosted.org/packages/aa/83/a0e19beb5c42df09e9a60711b227b4f910ba57f46bea258a9e1df883976c/grimp-3.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adda25aa158e11d96dd27166300b955c8ec0c76ce2fd1a13597e9af012aada06", size = 2145832, upload-time = "2025-10-29T13:02:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/13752205e290588e970fdc019b4ab2c063ca8da352295c332e34df5d5842/grimp-3.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03e17029d75500a5282b40cb15cdae030bf14df9dfaa6a2b983f08898dfe74b6", size = 2106762, upload-time = "2025-10-29T13:02:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/ff/30/c4d62543beda4b9a483a6cd5b7dd5e4794aafb511f144d21a452467989a1/grimp-3.13-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cbfc9d2d0ebc0631fb4012a002f3d8f4e3acb8325be34db525c0392674433b8", size = 2256674, upload-time = "2025-10-29T13:03:27.923Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ea/d07ed41b7121719c3f7bf30c9881dbde69efeacfc2daf4e4a628efe5f123/grimp-3.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:161449751a085484608c5b9f863e41e8fb2a98e93f7312ead5d831e487a94518", size = 2442699, upload-time = "2025-10-29T13:03:04.451Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/1923f0480756effb53c7e6cef02a3918bb519a86715992720838d44f0329/grimp-3.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:119628fbe7f941d1e784edac98e8ced7e78a0b966a4ff2c449e436ee860bd507", size = 2317145, upload-time = "2025-10-29T13:03:15.941Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d9/aef4c8350090653e34bc755a5d9e39cc300f5c46c651c1d50195f69bf9ab/grimp-3.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ca1ac776baf1fa105342b23c72f2e7fdd6771d4cce8d2903d28f92fd34a9e8f", size = 2180288, upload-time = "2025-10-29T13:03:41.023Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2e/a206f76eccffa56310a1c5d5950ed34923a34ae360cb38e297604a288837/grimp-3.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:941ff414cc66458f56e6af93c618266091ea70bfdabe7a84039be31d937051ee", size = 2328696, upload-time = "2025-10-29T13:04:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/40/3b/88ff1554409b58faf2673854770e6fc6e90167a182f5166147b7618767d7/grimp-3.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:87ad9bcd1caaa2f77c369d61a04b9f2f1b87f4c3b23ae6891b2c943193c4ec62", size = 2367574, upload-time = "2025-10-29T13:04:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/e9c99ecd94567465a0926ae7136e589aed336f6979a4cddcb8dfba16d27c/grimp-3.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:751fe37104a4f023d5c6556558b723d843d44361245c20f51a5d196de00e4774", size = 2358842, upload-time = "2025-10-29T13:04:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/74/65/a5fffeeb9273e06dfbe962c8096331ba181ca8415c5f9d110b347f2c0c34/grimp-3.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b561f79ec0b3a4156937709737191ad57520f2d58fa1fc43cd79f67839a3cd7", size = 2382268, upload-time = "2025-10-29T13:04:46.864Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/2f3b4323184329b26b46de2b6d1bd64ba1c26e0a9c3cfa0aaecec237b75e/grimp-3.13-cp311-cp311-win32.whl", hash = "sha256:52405ea8c8f20cf5d2d1866c80ee3f0243a38af82bd49d1464c5e254bf2e1f8f", size = 1759345, upload-time = "2025-10-29T13:05:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ce/e86cf73e412a6bf531cbfa5c733f8ca48b28ebea23a037338be763f24849/grimp-3.13-cp311-cp311-win_amd64.whl", hash = "sha256:6a45d1d3beeefad69717b3718e53680fb3579fe67696b86349d6f39b75e850bf", size = 1859382, upload-time = "2025-10-29T13:05:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/1d/06/ff7e3d72839f46f0fccdc79e1afe332318986751e20f65d7211a5e51366c/grimp-3.13-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3e715c56ffdd055e5c84d27b4c02d83369b733e6a24579d42bbbc284bd0664a9", size = 2070161, upload-time = "2025-10-29T13:03:59.755Z" }, + { url = "https://files.pythonhosted.org/packages/58/2f/a95bdf8996db9400fd7e288f32628b2177b8840fe5f6b7cd96247b5fa173/grimp-3.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f794dea35a4728b948ab8fec970ffbdf2589b34209f3ab902cf8a9148cf1eaad", size = 1984365, upload-time = "2025-10-29T13:03:51.805Z" }, + { url = "https://files.pythonhosted.org/packages/1f/45/cc3d7f3b7b4d93e0b9d747dc45ed73a96203ba083dc857f24159eb6966b4/grimp-3.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69571270f2c27e8a64b968195aa7ecc126797112a9bf1e804ff39ba9f42d6f6d", size = 2145486, upload-time = "2025-10-29T13:02:36.591Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/a6e493b71cb5a9145ad414cc4790c3779853372b840a320f052b22879606/grimp-3.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f7b226398ae476762ef0afb5ef8f838d39c8e0e2f6d1a4378ce47059b221a4a", size = 2106747, upload-time = "2025-10-29T13:02:53.084Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/36a09f39fe14ad8843ef3ff81090ef23abbd02984c1fcc1cef30e5713d82/grimp-3.13-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5498aeac4df0131a1787fcbe9bb460b52fc9b781ec6bba607fd6a7d6d3ea6fce", size = 2257027, upload-time = "2025-10-29T13:03:29.44Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7a/90f78787f80504caeef501f1bff47e8b9f6058d45995f1d4c921df17bfef/grimp-3.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4be702bb2b5c001a6baf709c452358470881e15e3e074cfc5308903603485dcb", size = 2441208, upload-time = "2025-10-29T13:03:05.733Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/0fbd3a3e914512b9602fa24c8ebc85a8925b101f04f8a8c1d1e220e0a717/grimp-3.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fcf988f3e3d272a88f7be68f0c1d3719fee8624d902e9c0346b9015a0ea6a65", size = 2318758, upload-time = "2025-10-29T13:03:17.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/e9/29c685e88b3b0688f0a2e30c0825e02076ecdf22bc0e37b1468562eaa09a/grimp-3.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ede36d104ff88c208140f978de3345f439345f35b8ef2b4390c59ef6984deba", size = 2180523, upload-time = "2025-10-29T13:03:42.3Z" }, + { url = "https://files.pythonhosted.org/packages/86/bc/7cc09574b287b8850a45051e73272f365259d9b6ca58d7b8773265c6fe35/grimp-3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b35e44bb8dc80e0bd909a64387f722395453593a1884caca9dc0748efea33764", size = 2328855, upload-time = "2025-10-29T13:04:08.111Z" }, + { url = "https://files.pythonhosted.org/packages/34/86/3b0845900c8f984a57c6afe3409b20638065462d48b6afec0fd409fd6118/grimp-3.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:becb88e9405fc40896acd6e2b9bbf6f242a5ae2fd43a1ec0a32319ab6c10a227", size = 2367756, upload-time = "2025-10-29T13:04:22.736Z" }, + { url = "https://files.pythonhosted.org/packages/06/2d/4e70e8c06542db92c3fffaecb43ebfc4114a411505bff574d4da7d82c7db/grimp-3.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a66585b4af46c3fbadbef495483514bee037e8c3075ed179ba4f13e494eb7899", size = 2358595, upload-time = "2025-10-29T13:04:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/dd/06/c511d39eb6c73069af277f4e74991f1f29a05d90cab61f5416b9fc43932f/grimp-3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29f68c6e2ff70d782ca0e989ec4ec44df73ba847937bcbb6191499224a2f84e2", size = 2381464, upload-time = "2025-10-29T13:04:48.265Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/42197d69e4c9e2e7eed091d06493da3824e07c37324155569aa895c3b5f7/grimp-3.13-cp312-cp312-win32.whl", hash = "sha256:cc996dcd1a44ae52d257b9a3e98838f8ecfdc42f7c62c8c82c2fcd3828155c98", size = 1758510, upload-time = "2025-10-29T13:05:11.74Z" }, + { url = "https://files.pythonhosted.org/packages/30/dd/59c5f19f51e25f3dbf1c9e88067a88165f649ba1b8e4174dbaf1c950f78b/grimp-3.13-cp312-cp312-win_amd64.whl", hash = "sha256:e2966435947e45b11568f04a65863dcf836343c11ae44aeefdaa7f07eb1a0576", size = 1859530, upload-time = "2025-10-29T13:05:02.638Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2f/98c8501310d8112eeb21259f6e94c15ec75dd24af7ee990f120059ee59db/grimp-3.13-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1dd9be203e856b8c725b9af4100c6619c82672e6e0c5101e1154b3c8bffde2df", size = 2070173, upload-time = "2025-10-29T13:04:01.092Z" }, + { url = "https://files.pythonhosted.org/packages/41/98/5a3489f374d42a967ea84044c2f5e32449a284e602fd3933f2d31e2c5161/grimp-3.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:921222b6ba969fb292f5014f8ab3da9068427ebc3d6ff7f79bf46a07958529fe", size = 1984392, upload-time = "2025-10-29T13:03:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/dd0a8d6982248d66c22e81ca4bf20fbe06cc2ff52c08541f3de5b8bd08cf/grimp-3.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56f1128efd6e222990d3fbc0cf911096272bd333a086f3b485950bc022e767d9", size = 2145460, upload-time = "2025-10-29T13:02:38.106Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/2eb22eae16253c9b0cfaefce171c9f37105cd765364526dcfb68b61047e9/grimp-3.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7742743183f3a2676774a341f460f34c80c3c80f6cbf9e9e421769dc69c170a0", size = 2106201, upload-time = "2025-10-29T13:02:54.289Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ec/96920a95fd5cf60ffa493028d69fb889de21247f5945abc7f1c944762640/grimp-3.13-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88bbaa0305f4ed3d7ee3e12eced83ede249eb2690cf9b623bde75c6650f39c13", size = 2257206, upload-time = "2025-10-29T13:03:31.278Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/be57981a2cfbb38575d92ffa3e5b9f3467aca153fc38469c66185d98d1e2/grimp-3.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0c0ffbc579a42281ac54af446b4c2e35e5be9a7f64171de8b806c8c3e624919", size = 2443064, upload-time = "2025-10-29T13:03:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c5/13/420968d134ec22bf7b5822228cdcff47ca64acd0627b341ca5b62ed45460/grimp-3.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e07fb97ac153c01ecd5790b27c47b8cc4a5a70ab2f6618ec8f9b69ade31d5f81", size = 2318238, upload-time = "2025-10-29T13:03:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/08/45/936a589e018b6f20dc4b2e7ae32acbe4bc2854c0360f96fb6289aa75acab/grimp-3.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c3d8f5fd5a962793c4cd9e4089a8ae8edcaf1ebabc28b151d4015a5736ca79", size = 2179976, upload-time = "2025-10-29T13:03:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bc/6db600e8eef20005be237d3fc3071c6b1b6c0a54c774ff4dbc3e5f4b06ec/grimp-3.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a5b323710e586b5db37d962d2b948eca47ceb70713c7aacb5cff01e7e7fc1b30", size = 2328734, upload-time = "2025-10-29T13:04:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4f/147bf98eb5db8ef601cb9424d1cdfdccea25edd7d4c70d3ef28f6d68b9a3/grimp-3.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:fc9933269b4b390d5d256fbee065eeea549e837ad43daf8f9cce832bd692cd5f", size = 2367729, upload-time = "2025-10-29T13:04:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/062e9571778351ac82e736c423518dbd87024f24aa9d0eebe8bf57985d1d/grimp-3.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c683645a4f61e62f8c99f3f643500b8660136666e44151b7491989e41407c86b", size = 2358865, upload-time = "2025-10-29T13:04:36.915Z" }, + { url = "https://files.pythonhosted.org/packages/7c/76/53a12e622c4cf7ad8f42732ee3eade3e89b434ebe3d2f3fa283c6f9ce607/grimp-3.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd07ab467fd6ed44db8cef047269ebc18e2b1811d30e25c54fc5818c573c3542", size = 2380827, upload-time = "2025-10-29T13:04:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/8f/76/4a755e23387f00c0f6db7dc03b463bc44f5ffcdca2d23524af2c0e3ce869/grimp-3.13-cp313-cp313-win32.whl", hash = "sha256:ab88dfb6a2df6e362b75be19e803c71b02f5cb22ee5fb0459dce5b180cef0b7f", size = 1758642, upload-time = "2025-10-29T13:05:13.021Z" }, + { url = "https://files.pythonhosted.org/packages/1c/31/0795cb3a6ae4bb373667d400a92522c56c82e8b4c70ae0f4468a270e01b6/grimp-3.13-cp313-cp313-win_amd64.whl", hash = "sha256:79a48b88b9b13de4ebe93a7d32e47e1155e85d4f67baef4b92bb34a65ea2bdb3", size = 1859029, upload-time = "2025-10-29T13:05:04.344Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/e8524607d4a65d0137635ede785fc0bdae499f99bdf5639d4d349b7e6bc5/grimp-3.13-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70094984cef4de1d59dad1a073a2e3e3e9cfd9874bd4c53146b3c66144800d39", size = 2141776, upload-time = "2025-10-29T13:02:39.639Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/6a72863557b384cf9b75c46237536ee3e8991ca3d26576501ec8608a3dfa/grimp-3.13-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a62b8f2073277a051d59a5973695b7e571a8225c433278777d9500070150a138", size = 2100620, upload-time = "2025-10-29T13:02:55.946Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f4/a6fa44cda8b1e0eddef0df3033a8eb534b47a3f46864119596e5610252a4/grimp-3.13-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13d1c158bae4cc0d24107389bf526ee02088df6f0a3caa413dd4e35c83d76f63", size = 2441482, upload-time = "2025-10-29T13:03:08.286Z" }, + { url = "https://files.pythonhosted.org/packages/24/d9/2f23aa778b94fd11d21375c76541c2b15553afdb001c91779f6bb130a8b0/grimp-3.13-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77d900c13d32c4aed91f6aab7ceb485f7bd0e3d2f88ca6c190ec5dc9143703b", size = 2316968, upload-time = "2025-10-29T13:03:19.903Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/95b9d5b1ae820109ad4e84f06d37e86cc3c296c62449bfa66b04e3f3b2bb/grimp-3.13-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21aeef8f1b7fc5f74e5513358f7774bd5bbb3159bd83eab7fe908c058fec3836", size = 2324014, upload-time = "2025-10-29T13:04:11.275Z" }, + { url = "https://files.pythonhosted.org/packages/22/67/224c527c1335ef25ba1de55c6e3244965ed2d2a43faa6256b2862915e895/grimp-3.13-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0eef1697f5b608e8b615a72957474e76231105cdd79a8a546390045ba904a654", size = 2364064, upload-time = "2025-10-29T13:04:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/f9/51/866e93410513560a132acf311d1739d4e722e5ac87ee99439137ed527168/grimp-3.13-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3f733a57be87b772cc21f63b062566cde4c90db6f3f338774a1567b11f7cb569", size = 2357771, upload-time = "2025-10-29T13:04:38.309Z" }, + { url = "https://files.pythonhosted.org/packages/56/69/41b617037dab37d67d50122d633b1deced4b684cdb9bb45dd85126a5931d/grimp-3.13-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d00b611249f0370f811433c1d81173be27a34ecb588364a371e00f182e4fe88b", size = 2378298, upload-time = "2025-10-29T13:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/e0/47/f469b953ce61e17c93c8d4114c7e10e0f256d243c427362a964d239e78dd/grimp-3.13-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:db63d787ac9665374a09a6cc00f7adb3f63c3f462c68ea87d5efe186db9c9ad7", size = 2070291, upload-time = "2025-10-29T13:04:02.399Z" }, + { url = "https://files.pythonhosted.org/packages/98/7b/7f334264aa3e9e12c29fd53c5b125308f2b15c9c0f232364b8bcce5d1447/grimp-3.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba6f46658b5cf48b902f054a29e09b3af62893cf24df2951e300a0cf13cb2e71", size = 1984093, upload-time = "2025-10-29T13:03:54.404Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cf/ed7ff0f8e4e8b76afef341d2378a942e83f85ddbfbf10c338192df04dd0f/grimp-3.13-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:472aa20a42b8486a5bf46720fdb21c58f81a86646057f727afab39c0d6f5e6aa", size = 2257847, upload-time = "2025-10-29T13:03:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/63/89/aba55a76c7d556a0c4c8e36a1eeece136a2fb7a1a8c817ac5176fabe0d10/grimp-3.13-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f54bafaebc94db0a6e38a6c9a58fa9a8339d88acca99095d326104ee2747335", size = 2179246, upload-time = "2025-10-29T13:03:44.66Z" }, + { url = "https://files.pythonhosted.org/packages/db/d6/83470164232c15c6bb925d16047f5570ebcacd41a955078e6161eaf7d540/grimp-3.13-cp314-cp314-win32.whl", hash = "sha256:b8b7450e1ad2494540ac39bc97b9f8f0a38ff1b0588b7722d93468e6ac1c9473", size = 1758653, upload-time = "2025-10-29T13:05:14.266Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/00753bab41719e7db573929090aa83372ec34958e1d4d7330dfa14902f03/grimp-3.13-cp314-cp314-win_amd64.whl", hash = "sha256:b70ca87be9e2df7212b7e3ebf65310051def6d2bb037a9b6a799397f56ac68df", size = 1860304, upload-time = "2025-10-29T13:05:05.792Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6c/1a97d75247e76d8defdaa92cc0f427511ad24dc3efc52f3b6a6e4158774e/grimp-3.13-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82eeb220124f22fc145685b34430a98143f994844d0f0899ec1e8ff82e5c48cf", size = 2077048, upload-time = "2025-10-29T13:04:03.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/73/a9c4d4eb3e0534084e1d12a404d5b9852ae5e57d2dfbf4c7081896d976ef/grimp-3.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1bf47f43b9cd35d50c4777e56ccf7a0a9703dfca262da5c25b7fcc1a0007545d", size = 1989695, upload-time = "2025-10-29T13:03:55.612Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f3/3b23d299c16a4d24ed2e9d54780169b55c75e3d4305f8889f65273d627d7/grimp-3.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64dab41b6da5bbd2ca3c5ece4dcb0470981925664f3e1a310f1e57e78b777318", size = 2146541, upload-time = "2025-10-29T13:02:40.961Z" }, + { url = "https://files.pythonhosted.org/packages/15/3f/e3ada53fc34d0eaa5d422f35581cc50e8993235e376330c7379f0dd63233/grimp-3.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de1cdf18d35f6d1c3f63f480c7a9d1d0abef61cc35f4dd5fe22731a38ee35fa4", size = 2108785, upload-time = "2025-10-29T13:02:57.107Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/8eadf19795ca2eb8760ad45cb0687450f0a62a61180758a5365b9b9766a4/grimp-3.13-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb1d58b57c99a020b961dc07391b567b20ffd13e5a6aec44e4ed22ad260964c6", size = 2257622, upload-time = "2025-10-29T13:03:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/acde72bb91915e4bdc809f9c8ffa7620099272bd5e68642b4357b58274d5/grimp-3.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d70179ddd4e2e9acaebf04239f45468189c9cbe14893e1958b48409eea572f", size = 2445934, upload-time = "2025-10-29T13:03:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/53941910348de29f3b83ce8443d3d46456013d07fe707eaf87f4b6bec8bf/grimp-3.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e8272214e3af42316d85f7938cb5ffbdb653041490c7f6db3cd99285eb150d3", size = 2318523, upload-time = "2025-10-29T13:03:21.311Z" }, + { url = "https://files.pythonhosted.org/packages/dc/56/266222d1b32cc2b43ddbf2558b53b9c583a52180dc843d42539c50721334/grimp-3.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e75333974b1a7318251314b650ffa8c5a250e8dc2ae45f94bd51418d0c3764d", size = 2180870, upload-time = "2025-10-29T13:03:45.896Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/2c5622bc90ff92daf2543acc445c9cda2523d75b339ceb1c02e57ebf69b5/grimp-3.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a4d8308e404517a68429b34749f8d44bc0f46ef04dad16998105c60033f90951", size = 2329689, upload-time = "2025-10-29T13:04:12.961Z" }, + { url = "https://files.pythonhosted.org/packages/55/e5/cb2494ce39f6df70340d6a15955a958ea233f24138e7661ee21ba923d69d/grimp-3.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4d20e54075f337637d4ddc4e4f174db1530bc257a0cc3991ff8b38b39372f1b8", size = 2370430, upload-time = "2025-10-29T13:04:26.776Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7a/335e41e8c44795eb3d31829bf0dc705d55cc68176238227f03ac6bcb5b8b/grimp-3.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:02b329cdb6beb4159db4c74bf7e89ab0a0964524f56e463a2a3564a9e9ce25f5", size = 2359859, upload-time = "2025-10-29T13:04:39.78Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a1/72e98ae961e03502424023530bc9e2bd95489c542406c00c28f7547270f4/grimp-3.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c3a5e13f4b3619375b203a617ad377c2d099abb05a42f2792f2634bc24bfae1d", size = 2383366, upload-time = "2025-10-29T13:04:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/ff2ae1c3bb1bdaab87b5af0376b7ed68b8440e8ddd08c81acee83be4523f/grimp-3.13-cp39-cp39-win32.whl", hash = "sha256:cdb6e793bb83c901db8664ec3f6741786fb884a8dcac1d064592f083ab12f643", size = 1760335, upload-time = "2025-10-29T13:05:15.554Z" }, + { url = "https://files.pythonhosted.org/packages/d8/12/7b225d3c6193b4e0f47359432594b10efd4241d9c64edf50dd86ae815827/grimp-3.13-cp39-cp39-win_amd64.whl", hash = "sha256:f27219649bb2610ce4a1db0761ce4a0651373d38b138b9fea4cfae77872f52e8", size = 1861620, upload-time = "2025-10-29T13:05:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/81c996264277214b048c0874f0abafd1e188a55792c0f819a1ad14196eb7/grimp-3.13-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cbf5bdf82dd076c08583d689b1f40a042430401caa1e234b76198fe7333b91", size = 2147509, upload-time = "2025-10-29T13:02:46.354Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f2/b9ef0727b3df1e00d7ee78767328fb9ff0a644d30147a7f473c8405d2ec0/grimp-3.13-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c98f23be5c5c9a0d882bb5c58f190c7ac787fda00082618181b2b3b35181064", size = 2109415, upload-time = "2025-10-29T13:02:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/24/20f7648d8f7091815516eaea20e929181a08348314be65a113eb2ad51986/grimp-3.13-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5738fad8fd82800564daab85d484b3dfc59bb38e5bc2879b96c906592d05aa97", size = 2442055, upload-time = "2025-10-29T13:03:10.804Z" }, + { url = "https://files.pythonhosted.org/packages/59/ce/72da5dc802a5bd602e41fee8ca42d62090fcb7d5a1fee2fa089ee3eb148a/grimp-3.13-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e997676334afc838a2403efb633f271e52a76b1e50f970e7cfe367a1a9394550", size = 2319062, upload-time = "2025-10-29T13:03:22.511Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f1/d3e921ad43461804cd0d39d8e2b7ce668d72c6e04196859a76531d79b70d/grimp-3.13-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b2f5ed0b657b41b99dd47dcafcb2af2d135aaacbb030be541aff7bd8df4ea61d", size = 2330904, upload-time = "2025-10-29T13:04:14.195Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d1/f22dc8b4c9ef16806a4ba52b23d9b8026201ff32feae4e004c68c816a33d/grimp-3.13-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:99c8dd4c93a55554f66d388553e928f302d68f04f2d1dff6375a27d9c01e2b18", size = 2370818, upload-time = "2025-10-29T13:04:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/f5/96/33137293f8f2b707ab009b24ed3d5799a4bc4ec133d9dcfc66baa1034436/grimp-3.13-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2af34b44c1a8b6ead1a74e63cfb3b3ee299d31e71780031d43ae807bec145f18", size = 2359627, upload-time = "2025-10-29T13:04:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6d/d3b4d9ec83493e45a915b4f130b7ad1602c12aa2c70089530b0b70f9b622/grimp-3.13-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ca96c2b2fac81ec2c518ae45986f122a388167ea93e89f71e851a508d3c92575", size = 2384687, upload-time = "2025-10-29T13:04:53.721Z" }, + { url = "https://files.pythonhosted.org/packages/e5/81/82de1b5d82701214b1f8e32b2e71fde8e1edbb4f2cdca9beb22ee6c8796d/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6a3c76525b018c85c0e3a632d94d72be02225f8ada56670f3f213cf0762be4", size = 2145955, upload-time = "2025-10-29T13:02:47.559Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/ada18cb73bdf97094af1c60070a5b85549482a57c509ee9a23fdceed4fc3/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239e9b347af4da4cf69465bfa7b2901127f6057bc73416ba8187fb1eabafc6ea", size = 2107150, upload-time = "2025-10-29T13:02:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/10/5e/6d8c65643ad5a1b6e00cc2cd8f56fc063923485f07c59a756fa61eefe7f2/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6db85ce2dc2f804a2edd1c1e9eaa46d282e1f0051752a83ca08ca8b87f87376", size = 2257515, upload-time = "2025-10-29T13:03:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/b2/62/72cbfd7d0f2b95a53edd01d5f6b0d02bde38db739a727e35b76c13e0d0a8/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e000f3590bcc6ff7c781ebbc1ac4eb919f97180f13cc4002c868822167bd9aed", size = 2441262, upload-time = "2025-10-29T13:03:12.158Z" }, + { url = "https://files.pythonhosted.org/packages/18/00/b9209ab385567c3bddffb5d9eeecf9cb432b05c30ca8f35904b06e206a89/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2374c217c862c1af933a430192d6a7c6723ed1d90303f1abbc26f709bbb9263", size = 2318557, upload-time = "2025-10-29T13:03:23.925Z" }, + { url = "https://files.pythonhosted.org/packages/11/4d/a3d73c11d09da00a53ceafe2884a71c78f5a76186af6d633cadd6c85d850/grimp-3.13-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ed0ff17d559ff2e7fa1be8ae086bc4fedcace5d7b12017f60164db8d9a8d806", size = 2180811, upload-time = "2025-10-29T13:03:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9a/1cdfaa7d7beefd8859b190dfeba11d5ec074e8702b2903e9f182d662ed63/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:43960234aabce018c8d796ec8b77c484a1c9cbb6a3bc036a0d307c8dade9874c", size = 2329205, upload-time = "2025-10-29T13:04:15.845Z" }, + { url = "https://files.pythonhosted.org/packages/86/73/b36f86ef98df96e7e8a6166dfa60c8db5d597f051e613a3112f39a870b4c/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:44420b638b3e303f32314bd4d309f15de1666629035acd1cdd3720c15917ac85", size = 2368745, upload-time = "2025-10-29T13:04:29.706Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/0ce37872fad5c4b82d727f6e435fd5bc76f701279bddc9666710318940cf/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:f6127fdb982cf135612504d34aa16b841f421e54751fcd54f80b9531decb2b3f", size = 2358753, upload-time = "2025-10-29T13:04:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/bb/23/935c888ac9ee71184fe5adf5ea86648746739be23c85932857ac19fc1d17/grimp-3.13-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:69893a9ef1edea25226ed17e8e8981e32900c59703972e0780c0e927ce624f75", size = 2383066, upload-time = "2025-10-29T13:04:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6d/ee405cc90245a970a14556f0de87f5267dc8fe2f4e7aa392c0367715305c/grimp-3.13-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c522d4ee6e921c040b7f932343d983b5a9521aab07685eedc49a6fc9001bd8c1", size = 2147200, upload-time = "2025-10-29T13:02:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/68/8d/70178d957fb4a9d556b90b76b5b87ed8a18398c973c26f4df7a6fc727fe0/grimp-3.13-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32f4d60aa056640b0a3547521363329b6a5292df4ce4bc67a355d841303587dd", size = 2109410, upload-time = "2025-10-29T13:03:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9a/71a137b2ec757feda15cab059033e172d8d4a939a150d63b588cb10fe371/grimp-3.13-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e1b558ac78f82fa44a1c3b6b4fa1ad04ab41b4445353b6aebaff8cca4a9245c", size = 2444012, upload-time = "2025-10-29T13:03:13.302Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b0/7e89a09c255ce15969121e403af7758470dc845ef81a232f56ef60ac20d7/grimp-3.13-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27644460a503aea1df0f2e39d683395d45def73ebd5d61efc43e4dc8ad0cd4e0", size = 2318466, upload-time = "2025-10-29T13:03:25.105Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/1b8d754311b34050f12d82c57afde5a2702e486ef5a29338c5d9586f4d95/grimp-3.13-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:20b56bbf2ff00ea97701bf2ef48e79aed61790f01a0465a065787ca300057d74", size = 2330934, upload-time = "2025-10-29T13:04:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/4f/53/862bcf1a16ed489360b21efa851723636d0c2f28eeb0f2ce1095a1618c96/grimp-3.13-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fce5f24e792f8bb3dbe32e3329df53b54c0facd8181fa219ce0874ae4af7febd", size = 2370693, upload-time = "2025-10-29T13:04:31.144Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bf/eb25e0806c88ce28d660698fb01eb5f5693497862f6bc4a1fb98eec301d5/grimp-3.13-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:71e100238833264cbd1408e1e8636c16574ede09b9840fabed58ac2f4754d4e3", size = 2359317, upload-time = "2025-10-29T13:04:44.299Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/7ac3c08c12137810fb5dee621f48875a359a92efd6090e859180f2786961/grimp-3.13-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7cbfde149e00986270b3c3552c67fefa101282f4ec9c5bfd08e7fd1878596629", size = 2384379, upload-time = "2025-10-29T13:04:56.408Z" }, ] [[package]] @@ -446,7 +446,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=6" }, - { name = "grimp", specifier = ">=3.12" }, + { name = "grimp", specifier = "==3.13" }, { name = "pytest", specifier = ">=8.4.2" }, ] From 47795c8227039a2de56abf0a0d5276385af48492 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 09:37:18 +0000 Subject: [PATCH 10/12] Enable show-cycle-breakers in CLI --- justfile | 1 + src/impulse/cli.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 72ccfa9..c41a162 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,7 @@ test: @uv run pytest @uv run impulse drawgraph grimp @uv run impulse drawgraph grimp --show-import-totals + @uv run --with=django impulse drawgraph django.db --show-cycle-breakers # Run tests under all supported Python versions. diff --git a/src/impulse/cli.py b/src/impulse/cli.py index 53ad0ce..0bbf695 100644 --- a/src/impulse/cli.py +++ b/src/impulse/cli.py @@ -19,12 +19,20 @@ def main(): is_flag=True, help="Label arrows with the number of imports they represent.", ) +@click.option( + "--show-cycle-breakers", + is_flag=True, + help=( + "Identify a set of dependencies that, if removed, would make the graph acyclic, " + "and display them as dashed lines." + ), +) @click.argument("module_name", type=str) -def drawgraph(module_name: str, show_import_totals: bool) -> None: +def drawgraph(module_name: str, show_import_totals: bool, show_cycle_breakers: bool) -> None: use_cases.draw_graph( module_name=module_name, show_import_totals=show_import_totals, - show_cycle_breakers=False, + show_cycle_breakers=show_cycle_breakers, sys_path=sys.path, current_directory=os.getcwd(), build_graph=grimp.build_graph, From 05bfaf7c61585512e8c6052f0d00f78555784afa Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 10:21:39 +0000 Subject: [PATCH 11/12] Add docs for show-cycle-breakers --- CHANGELOG.rst | 5 ++++ README.rst | 24 ++++++++++++++++-- .../images/django.db.show-cycle-breakers.png | Bin 0 -> 26364 bytes 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 docs/_static/images/django.db.show-cycle-breakers.png diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d436ed..0c0d54d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +latest +------ + +* Add --show-cycle-breakers flag. + 2.0 (2025-10-20) ---------------- diff --git a/README.rst b/README.rst index 70919dd..7556f9f 100644 --- a/README.rst +++ b/README.rst @@ -58,8 +58,12 @@ There is currently only one command. Usage: impulse drawgraph [OPTIONS] MODULE_NAME Options: - --show-import-totals Label arrows with the number of imports they represent. - --help Show this message and exit. + --show-import-totals Label arrows with the number of imports they + represent. + --show-cycle-breakers Identify a set of dependencies that, if removed, + would make the graph acyclic, and display them as + dashed lines. + --help Show this message and exit. Draw a graph of the dependencies within any installed Python package or subpackage. @@ -99,3 +103,19 @@ In this example, there is an arrow from ``.models`` to Here you can see that there are two imports from modules within ``django.db.models`` of modules within ``django.db.utils``. +\ +\ + +**Example with cycle breakers** + +.. code-block:: text + + impulse drawgraph django.db --show-cycle-breakers + +.. image:: https://raw.githubusercontent.com/seddonym/impulse/master/docs/_static/images/django.db.show-cycle-breakers.png + :align: center + :alt: Graph of django.db package with cycle breakers. + +Here you can see that two of the dependencies are shown as a dashed line. If these dependencies were to be +removed, the graph would be acyclic. To decide on the cycle breakers, Impulse uses the +`nominate_cycle_breakers method provided by Grimp `_. \ No newline at end of file diff --git a/docs/_static/images/django.db.show-cycle-breakers.png b/docs/_static/images/django.db.show-cycle-breakers.png new file mode 100644 index 0000000000000000000000000000000000000000..d5146731015a36d4d390cc57b81d9240267330fb GIT binary patch literal 26364 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 literal 0 HcmV?d00001 From 37a715b7c2424c3117bdd1ecc4cdfd0329eab2c8 Mon Sep 17 00:00:00 2001 From: David Seddon Date: Thu, 30 Oct 2025 10:26:24 +0000 Subject: [PATCH 12/12] Make sure it works in Python 3.9 --- src/impulse/application/use_cases.py | 11 ++++++----- tests/unit/application/test_use_cases.py | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/impulse/application/use_cases.py b/src/impulse/application/use_cases.py index da9c2b6..f329ffd 100644 --- a/src/impulse/application/use_cases.py +++ b/src/impulse/application/use_cases.py @@ -3,6 +3,7 @@ import itertools import grimp from impulse import ports, dotfile +from typing import Optional def draw_graph( @@ -60,7 +61,7 @@ def prepare_graph(self, grimp_graph: grimp.ImportGraph, children: Set[str]) -> N def build_edge( self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str - ) -> dotfile.Edge | None: + ) -> Optional[dotfile.Edge]: raise NotImplementedError @@ -73,7 +74,7 @@ def prepare_graph(self, grimp_graph: grimp.ImportGraph, children: Set[str]) -> N def build_edge( self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str - ) -> dotfile.Edge | None: + ) -> Optional[dotfile.Edge]: if grimp_graph.direct_import_exists(importer=downstream, imported=upstream): return dotfile.Edge(source=downstream, destination=upstream) return None @@ -90,7 +91,7 @@ def __init__( self.module_name = module_name self.show_import_totals = show_import_totals self.show_cycle_breakers = show_cycle_breakers - self.cycle_breakers: set[tuple[str, str]] | None = None + self.cycle_breakers: Optional[set[tuple[str, str]]] = None def should_concentrate(self) -> bool: # We need to see edge direction emphasized separately. @@ -119,7 +120,7 @@ def _get_coarse_grained_cycle_breakers( return coarse_grained_cycle_breakers @staticmethod - def _get_self_or_ancestor(candidate: str, ancestors: Set[str]) -> str | None: + def _get_self_or_ancestor(candidate: str, ancestors: Set[str]) -> Optional[str]: for ancestor in ancestors: if candidate == ancestor or candidate.startswith(f"{ancestor}."): return ancestor @@ -127,7 +128,7 @@ def _get_self_or_ancestor(candidate: str, ancestors: Set[str]) -> str | None: def build_edge( self, grimp_graph: grimp.ImportGraph, upstream: str, downstream: str - ) -> dotfile.Edge | None: + ) -> Optional[dotfile.Edge]: if grimp_graph.direct_import_exists( importer=downstream, imported=upstream, as_packages=True ): diff --git a/tests/unit/application/test_use_cases.py b/tests/unit/application/test_use_cases.py index 026dcea..8fbac2d 100644 --- a/tests/unit/application/test_use_cases.py +++ b/tests/unit/application/test_use_cases.py @@ -1,3 +1,4 @@ +from typing import Optional from impulse.application import use_cases from copy import copy from impulse import dotfile @@ -54,7 +55,7 @@ def build_fake_graph(package_name: str) -> grimp.ImportGraph: class SpyGraphViewer(ports.GraphViewer): def __init__(self) -> None: - self.called_with_dot: dotfile.DotGraph | None = None + self.called_with_dot: Optional[dotfile.DotGraph] = None def view(self, dot: dotfile.DotGraph) -> None: self.called_with_dot = dot