prouter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ name: CD
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v5
15
+
16
+ - name: Build distributions
17
+ run: uv build
18
+
19
+ - name: Upload artifacts
20
+ uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment:
29
+ name: pypi
30
+ url: https://pypi.org/project/prouter/
31
+ permissions:
32
+ # IMPORTANT: required for PyPI trusted publishing (OIDC)
33
+ id-token: write
34
+ steps:
35
+ - name: Download artifacts
36
+ uses: actions/download-artifact@v4
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+
41
+ - name: Publish to PyPI
42
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ ci:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+ python-version: "3.12"
24
+
25
+ - name: Install dependencies
26
+ run: uv sync --all-extras --dev
27
+
28
+ - name: Ruff lint
29
+ run: uv run ruff check --output-format=github .
30
+
31
+ - name: Ruff format check
32
+ run: uv run ruff format --check .
33
+
34
+ - name: Run tests
35
+ run: uv run pytest
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ *.egg
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # uv
15
+ .uv/
16
+
17
+ # Jupyter
18
+ .ipynb_checkpoints/
19
+
20
+ # macOS
21
+ .DS_Store
22
+
23
+ # IDE
24
+ .vscode/
25
+ .idea/
26
+
27
+ # Distribution
28
+ *.whl
29
+ *.tar.gz
30
+
31
+ # CI
32
+ .pytest_cache/
33
+ .ruff_cache/
34
+
prouter-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WISC Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
prouter-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: prouter
3
+ Version: 0.1.0
4
+ Summary: Route filesystem paths through pattern -> handler -> pattern rules with visibility.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: pandas>=3.0.3
8
+ Description-Content-Type: text/markdown
9
+
10
+ # prouter
11
+
12
+ [![PyPI](https://img.shields.io/pypi/v/prouter)](https://pypi.org/project/prouter)
13
+ [![WiscLab](https://img.shields.io/badge/WiscLab-kidspeech.wisc.edu-c5050c)](https://kidspeech.wisc.edu/)
14
+ ![ShredGuard](https://img.shields.io/badge/ShredGuard-ON-06B6D4?logo=git&logoColor=white&style=flat-square)
15
+
16
+ [![CI](https://github.com/WISCLab/prouter/actions/workflows/ci.yml/badge.svg)](https://github.com/WISCLab/prouter/actions/workflows/ci.yml)
17
+ [![CD](https://github.com/WISCLab/prouter/actions/workflows/cd.yml/badge.svg)](https://github.com/WISCLab/prouter/actions/workflows/cd.yml)
18
+
19
+ Route filesystem paths through `pattern -> handler -> pattern` rules with visibility.
20
+
21
+ The idea is simple. You define routes, each one an input pattern, a handler that rewrites a path, and an output pattern the result has to match. prouter walks a directory tree and runs every path through the route whose input pattern matches its basename, keeping track of what it did along the way.
22
+
23
+ It never touches the filesystem. Nothing gets moved or renamed. What you get back is a set of CSVs describing the transform it would apply, so you can look it over before committing to anything.
24
+
25
+ The tree is walked bottom-up, deepest paths first and the root last. This is deliberate: renaming children before their parents means a directory rename never invalidates paths you haven't reached yet. The CSVs preserve that order, so applying the rows top to bottom is always safe.
26
+
27
+ If the route-building syntax feels familiar, that's on purpose: it's inspired by LangGraph's way of wiring up nodes.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install prouter
33
+ ```
34
+
35
+ Requires Python 3.12+.
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ import re
41
+ from pathlib import Path
42
+ from prouter import GraphBuilder
43
+
44
+ # An input pattern, a handler (Path -> Path), and the output pattern the result must match.
45
+ draft = re.compile(r"(\d+)_draft\.wav")
46
+ final = re.compile(r"\d+_final\.wav")
47
+
48
+ def rename(path: Path) -> Path:
49
+ return path.with_name(path.name.replace("_draft", "_final"))
50
+
51
+ builder = GraphBuilder(root_path=Path("/path/to/draw/from"), results_folder=Path("/Folder/to/save/results"))
52
+ builder.add_route(draft, rename, final)
53
+
54
+ builder.build() # walk the tree, match routes, apply handlers in memory
55
+ builder.save() # write the result CSVs
56
+ ```
57
+
58
+ > [!NOTE]
59
+ > When adding a route with `add_route` You can't register the same input pattern twice (a ValueError raises). Matching against those patterns is a `fullmatch` against the whole basename, so a pattern has to account for the entire filename, not just part of it. And at build() time, only one input pattern may match a given path. If two would match, that's ambiguous and prouter raises rather than guess.
60
+
61
+ ## Output
62
+
63
+ `save()` drops four CSVs into the folder you give it:
64
+
65
+ - `routable_paths.csv` — matched a route, and the handler's output lined up with the output pattern.
66
+ - `problem_paths.csv` — matched a route, but the handler's output did **not** match the output pattern. These are the ones to look at.
67
+ - `clean_paths.csv` — matched nothing, left alone.
68
+ - `routes.csv` — the routes you configured, on their own.
69
+
70
+ The path CSVs share the same columns: `path`, `node`, `input_pattern`, `output_pattern`, `new_path`. `routes.csv` just has `input_pattern`, `node`, `output_pattern`.
71
+
72
+ > [!NOTE]
73
+ > In those columns, things show up under the names you gave them. Handlers use the function's `__name__`, so `rename` lands in the CSV as `rename`. Patterns are trickier, since a compiled regex has no name of its own, so prouter peeks at the calling frame and recovers the variable you bound it to: `draft` and `final` above come out as `draft` and `final`. Pass a bare `re.compile(...)` inline with no variable and it falls back to the raw regex source.
@@ -0,0 +1,64 @@
1
+ # prouter
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/prouter)](https://pypi.org/project/prouter)
4
+ [![WiscLab](https://img.shields.io/badge/WiscLab-kidspeech.wisc.edu-c5050c)](https://kidspeech.wisc.edu/)
5
+ ![ShredGuard](https://img.shields.io/badge/ShredGuard-ON-06B6D4?logo=git&logoColor=white&style=flat-square)
6
+
7
+ [![CI](https://github.com/WISCLab/prouter/actions/workflows/ci.yml/badge.svg)](https://github.com/WISCLab/prouter/actions/workflows/ci.yml)
8
+ [![CD](https://github.com/WISCLab/prouter/actions/workflows/cd.yml/badge.svg)](https://github.com/WISCLab/prouter/actions/workflows/cd.yml)
9
+
10
+ Route filesystem paths through `pattern -> handler -> pattern` rules with visibility.
11
+
12
+ The idea is simple. You define routes, each one an input pattern, a handler that rewrites a path, and an output pattern the result has to match. prouter walks a directory tree and runs every path through the route whose input pattern matches its basename, keeping track of what it did along the way.
13
+
14
+ It never touches the filesystem. Nothing gets moved or renamed. What you get back is a set of CSVs describing the transform it would apply, so you can look it over before committing to anything.
15
+
16
+ The tree is walked bottom-up, deepest paths first and the root last. This is deliberate: renaming children before their parents means a directory rename never invalidates paths you haven't reached yet. The CSVs preserve that order, so applying the rows top to bottom is always safe.
17
+
18
+ If the route-building syntax feels familiar, that's on purpose: it's inspired by LangGraph's way of wiring up nodes.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install prouter
24
+ ```
25
+
26
+ Requires Python 3.12+.
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import re
32
+ from pathlib import Path
33
+ from prouter import GraphBuilder
34
+
35
+ # An input pattern, a handler (Path -> Path), and the output pattern the result must match.
36
+ draft = re.compile(r"(\d+)_draft\.wav")
37
+ final = re.compile(r"\d+_final\.wav")
38
+
39
+ def rename(path: Path) -> Path:
40
+ return path.with_name(path.name.replace("_draft", "_final"))
41
+
42
+ builder = GraphBuilder(root_path=Path("/path/to/draw/from"), results_folder=Path("/Folder/to/save/results"))
43
+ builder.add_route(draft, rename, final)
44
+
45
+ builder.build() # walk the tree, match routes, apply handlers in memory
46
+ builder.save() # write the result CSVs
47
+ ```
48
+
49
+ > [!NOTE]
50
+ > When adding a route with `add_route` You can't register the same input pattern twice (a ValueError raises). Matching against those patterns is a `fullmatch` against the whole basename, so a pattern has to account for the entire filename, not just part of it. And at build() time, only one input pattern may match a given path. If two would match, that's ambiguous and prouter raises rather than guess.
51
+
52
+ ## Output
53
+
54
+ `save()` drops four CSVs into the folder you give it:
55
+
56
+ - `routable_paths.csv` — matched a route, and the handler's output lined up with the output pattern.
57
+ - `problem_paths.csv` — matched a route, but the handler's output did **not** match the output pattern. These are the ones to look at.
58
+ - `clean_paths.csv` — matched nothing, left alone.
59
+ - `routes.csv` — the routes you configured, on their own.
60
+
61
+ The path CSVs share the same columns: `path`, `node`, `input_pattern`, `output_pattern`, `new_path`. `routes.csv` just has `input_pattern`, `node`, `output_pattern`.
62
+
63
+ > [!NOTE]
64
+ > In those columns, things show up under the names you gave them. Handlers use the function's `__name__`, so `rename` lands in the CSV as `rename`. Patterns are trickier, since a compiled regex has no name of its own, so prouter peeks at the calling frame and recovers the variable you bound it to: `draft` and `final` above come out as `draft` and `final`. Pass a bare `re.compile(...)` inline with no variable and it falls back to the raw regex source.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "prouter"
3
+ version = "0.1.0"
4
+ description = "Route filesystem paths through pattern -> handler -> pattern rules with visibility."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "pandas>=3.0.3",
9
+ ]
10
+ [dependency-groups]
11
+ dev = [
12
+ "ruff>=0.9.0",
13
+ "pytest>=8.0",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/prouter"]
22
+
23
+ [tool.ruff]
24
+ line-length = 120
25
+ target-version = "py312"
26
+ src = ["src"]
27
+
28
+ [tool.ruff.lint]
29
+ select = [
30
+ "E", # pycodestyle errors
31
+ "F", # pyflakes
32
+ "I", # isort
33
+ "UP", # pyupgrade
34
+ "B", # flake8-bugbear
35
+ "W", # pycodestyle warnings
36
+ ]
37
+
38
+ [tool.shredguard]
39
+
40
+ [[tool.shredguard.patterns]]
41
+ regex = "_[MF]_"
42
+ description = "Problem Catch"
@@ -0,0 +1,5 @@
1
+ """prouter: route filesystem paths through (pattern -> handler -> pattern) routes with visibility."""
2
+
3
+ from prouter.router import GraphBuilder
4
+
5
+ __all__ = ["GraphBuilder"]
@@ -0,0 +1,334 @@
1
+ import inspect
2
+ import os
3
+ import re
4
+ import time
5
+ from collections.abc import Callable, Iterator
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+
10
+ # Metadata/junk entries to skip wholesale (never descended into, never yielded).
11
+ _SKIP_NAMES = frozenset(
12
+ {
13
+ "Thumbs.db", # Windows thumbnail cache
14
+ "Desktop.ini", # Windows folder metadata cache
15
+ ".DS_Store", # iOS/macOS Finder metadata cache
16
+ ".AppleDouble", # Apple metadata cache
17
+ ".Spotlight-V100",
18
+ ".Trashes",
19
+ "__MACOSX",
20
+ }
21
+ )
22
+
23
+
24
+ def bottom_up_traversal(path: Path) -> Iterator[Path]:
25
+ """Yield every path under ``path`` from the leaves up to the root.
26
+
27
+ Args:
28
+ path: The directory root path to traverse.
29
+
30
+ Yields:
31
+ Each path in the tree, deepest first, ending with ``path``.
32
+
33
+ Raises:
34
+ TypeError: If ``path`` is not a ``Path``.
35
+
36
+ Notes:
37
+ Uses ``os.scandir`` so each directory entry's type is read once and
38
+ reused, avoiding a separate ``stat`` per child -- a large saving on
39
+ networked volumes. Symlinks are not followed, so symlink cycles cannot
40
+ cause infinite recursion. Unreadable directories are yielded as leaves
41
+ rather than raising.
42
+ """
43
+ if not isinstance(path, Path):
44
+ raise TypeError(f"path must be a Path, got {type(path).__name__}")
45
+
46
+ yield from _bottom_up(path)
47
+
48
+
49
+ def _bottom_up(path: Path) -> Iterator[Path]:
50
+ """Recursive post-order worker for :func:`bottom_up_traversal`."""
51
+ if path.name in _SKIP_NAMES:
52
+ return
53
+
54
+ try:
55
+ scandir_it = os.scandir(path)
56
+ except NotADirectoryError:
57
+ # ``path`` is a file given as the traversal root: yield it as a leaf.
58
+ yield path
59
+ return
60
+ except OSError:
61
+ # Unreadable/vanished directory (permissions, network drop): yield it,
62
+ # but don't descend.
63
+ yield path
64
+ return
65
+
66
+ with scandir_it:
67
+ for entry in scandir_it:
68
+ if entry.name in _SKIP_NAMES:
69
+ continue
70
+ # ``entry.is_dir()`` is served from the cached type;
71
+ # no extra syscall in the common case.
72
+ if entry.is_dir(follow_symlinks=False):
73
+ yield from _bottom_up(Path(entry.path))
74
+ else:
75
+ yield Path(entry.path)
76
+ yield path
77
+
78
+
79
+ def validate_uniqueness_and_disjointness(patterns: list[re.Pattern]) -> bool:
80
+ """Validate that each pattern is unique and that the input and output pattern sets are disjoint.
81
+
82
+ Args:
83
+ patterns: List of compiled regex patterns.
84
+
85
+ Returns:
86
+ True if validation passes, otherwise False.
87
+ """
88
+ pattern_set = set(p.pattern for p in patterns)
89
+
90
+ # Check for duplicates within patterns
91
+ if len(pattern_set) != len(patterns):
92
+ return False
93
+
94
+ return True
95
+
96
+
97
+ def find_candidates(patterns: list[re.Pattern], path: Path) -> list[re.Pattern]:
98
+ """Match the base of a path against the patterns and report the match list.
99
+
100
+ Args:
101
+ patterns: List of compiled regex patterns to match against basename.
102
+ path: The path who's basename is to be matched against the patterns.
103
+
104
+ Returns:
105
+ A list of patterns that fullmatch the basename of the path, or an empty list if no patterns match.
106
+ """
107
+ matching_patterns = [] # Populated with patterns matching the basename of the path
108
+
109
+ # Check every pattern against the basename for fullmatch
110
+ for pattern in patterns:
111
+ if pattern.fullmatch(path.name):
112
+ matching_patterns.append(pattern)
113
+
114
+ return matching_patterns
115
+
116
+
117
+ class GraphBuilder:
118
+ """Builder for the graph of mappings from path -> input pattern -> node -> output pattern -> new path
119
+ with validation of output and input validation inferrable."""
120
+
121
+ def __init__(self, root_path: Path, results_folder: Path) -> None:
122
+ """Initialize the GraphBuilder with the necessary components to build the graph."""
123
+
124
+ self.root_path = root_path # All paths beneath the root pass through the graph
125
+ self.results_folder = results_folder # Where to save the graph CSVs
126
+ self.router = {} # Mapping input pattern -> (input_pattern, node, output_pattern)
127
+ self.graph = [] # List of tuples (path, input_pattern, node, output_pattern, new_path)
128
+ self.input_patterns = [] # Faster lookup and validation of input patterns
129
+ self.output_patterns = [] # Faster lookup and validation of output patterns
130
+ self.pattern_names = {} # Compiled pattern -> variable name harvested at add_route time
131
+
132
+ if not self.root_path.exists():
133
+ raise ValueError(f"Path '{self.root_path}' does not exist.")
134
+ if not self.results_folder.exists():
135
+ raise ValueError(f"Path '{self.results_folder}' does not exist.")
136
+ if not self.results_folder.is_dir():
137
+ raise ValueError(f"Path '{self.results_folder}' is not a directory.")
138
+ for filename in [
139
+ "routable_paths.csv",
140
+ "problem_paths.csv",
141
+ "clean_paths.csv",
142
+ "routes.csv",
143
+ ]:
144
+ if (self.results_folder / filename).exists():
145
+ raise ValueError(
146
+ f"File '{filename}' already exists in '{self.results_folder}'. "
147
+ "Please remove it to avoid overwriting."
148
+ )
149
+
150
+ print("Initialized GraphBuilder")
151
+
152
+ @staticmethod
153
+ def _harvest_name(pattern: re.Pattern) -> str:
154
+ """Find the variable name bound to ``pattern`` in the caller of ``add_route``.
155
+
156
+ Walks back two frames (past ``add_route``) to inspect the caller's local
157
+ and global namespaces. Returns the first matching variable name, or the
158
+ regex source if the pattern was passed as an unnamed literal.
159
+ """
160
+ caller = inspect.currentframe().f_back.f_back
161
+ if caller is not None:
162
+ for namespace in (caller.f_locals, caller.f_globals):
163
+ for name, value in namespace.items():
164
+ if value is pattern:
165
+ return name
166
+ return pattern.pattern
167
+
168
+ def _label(self, pattern: re.Pattern) -> str:
169
+ """Return the harvested variable name for ``pattern``, else its regex source."""
170
+ return self.pattern_names.get(pattern, pattern.pattern)
171
+
172
+ @staticmethod
173
+ def _format_eta(elapsed: float, completed: int, total: int) -> str:
174
+ if completed <= 0 or total <= 0:
175
+ return "unknown"
176
+ rate = elapsed / completed
177
+ remaining = max(total - completed, 0) * rate
178
+ return f"{remaining:.1f}s"
179
+
180
+ def add_route(self, input_pattern: re.Pattern, node: Callable, output_pattern: re.Pattern) -> None:
181
+ """add routes for how to transform paths when pattern is met."""
182
+ # Check if route is already present
183
+ if input_pattern in self.router:
184
+ raise ValueError(f"Route for input pattern '{input_pattern.pattern}' already exists.")
185
+
186
+ # Add input to the list of input patterns
187
+ self.input_patterns.append(input_pattern)
188
+
189
+ # Add output to the list of output patterns if not already present
190
+ if output_pattern not in self.output_patterns:
191
+ self.output_patterns.append(output_pattern)
192
+
193
+ # Validate uniqueness and disjointness of patterns
194
+ if not validate_uniqueness_and_disjointness(self.input_patterns + self.output_patterns):
195
+ self.input_patterns.pop() # Remove the last added input pattern
196
+ self.output_patterns.pop() # Remove the last added output pattern
197
+ raise ValueError(f"Input pattern '{input_pattern.pattern}' is not unique.")
198
+
199
+ # Harvest the variable names the caller used, for human-readable CSV output
200
+ self.pattern_names.setdefault(input_pattern, self._harvest_name(input_pattern))
201
+ self.pattern_names.setdefault(output_pattern, self._harvest_name(output_pattern))
202
+
203
+ # Set route for quick lookup during graph building
204
+ self.router[input_pattern] = (input_pattern, node, output_pattern)
205
+
206
+ def build(self) -> dict[Path, list[tuple[re.Pattern, Callable, re.Pattern]]]:
207
+ """Build the graph by traversing the directory tree and asserting patterns match.
208
+
209
+ Returns:
210
+ A list of tuples (path, input_pattern, node, output_pattern, new_path, valid) for each
211
+ matching route and result.
212
+ """
213
+ # Walking a (possibly networked) tree can take a while with no feedback,
214
+ # so report discovery progress as paths stream in.
215
+ discovery_start = time.perf_counter()
216
+ paths = []
217
+ for path in bottom_up_traversal(self.root_path):
218
+ paths.append(path)
219
+ print(
220
+ f"\rDiscovering paths: {len(paths)} found ({time.perf_counter() - discovery_start:.1f}s)",
221
+ end="",
222
+ flush=True,
223
+ )
224
+ print() # Finish the discovery line
225
+ total = len(paths)
226
+ start = time.perf_counter()
227
+
228
+ for index, path in enumerate(paths, start=1):
229
+ elapsed = time.perf_counter() - start
230
+ eta = self._format_eta(elapsed, index - 1, total)
231
+ print(f"\rBuilding graph: {index}/{total} ({(index / total * 100):.1f}%) - ETA {eta}", end="", flush=True)
232
+
233
+ candidates = find_candidates(self.input_patterns, path)
234
+ if candidates:
235
+ if len(candidates) > 1:
236
+ raise ValueError(
237
+ f"Ambiguous path '{path}' matches multiple patterns: {[p.pattern for p in candidates]}"
238
+ )
239
+ for candidate in candidates: # Skips if candidates is empty
240
+ valid = False
241
+ input_pattern, node, output_pattern = self.router[candidate]
242
+ new_path = node(path)
243
+ if output_pattern.fullmatch(new_path.name):
244
+ valid = True
245
+ self.graph.append((path, input_pattern, node, output_pattern, new_path, valid))
246
+ else:
247
+ self.graph.append((path, "", "", "", "", ""))
248
+
249
+ print() # Finish the progress line
250
+ print(f"Graph built with {len(self.graph)} entries in {(time.perf_counter() - start):.1f}s")
251
+ return self.graph
252
+
253
+ def save(self) -> None:
254
+ """Save the graph to CSV files at the specified folder."""
255
+ path = self.results_folder
256
+ if not self.router:
257
+ print("No routes to save. Please add routes before saving.")
258
+ return
259
+ if not self.graph:
260
+ print("No graph to save. Please run build() before saving.")
261
+ return
262
+
263
+ # Double Check for output collisons to see if we can't save before processing the graph (unlikely case)
264
+ if not path.exists():
265
+ raise ValueError(f"Path '{path}' does not exist.")
266
+ if not path.is_dir():
267
+ raise ValueError(f"Path '{path}' is not a directory.")
268
+ for filename in [
269
+ "routable_paths.csv",
270
+ "problem_paths.csv",
271
+ "clean_paths.csv",
272
+ "routes.csv",
273
+ ]:
274
+ if (path / filename).exists():
275
+ raise ValueError(
276
+ f"File '{filename}' already exists in '{path}'. Please remove it to avoid overwriting."
277
+ )
278
+
279
+ # Exactly one input_pattern is expected to match each path.
280
+ routable_paths = {"path": [], "node": [], "input_pattern": [], "output_pattern": [], "new_path": []}
281
+
282
+ # Paths that have more than one candidate pattern match are ambiguous and said to be not routable.
283
+ problem_paths = {"path": [], "node": [], "input_pattern": [], "output_pattern": [], "new_path": []}
284
+
285
+ # Paths that have no candidate pattern are ignored and not routable.
286
+ clean_paths = {"path": [], "node": [], "input_pattern": [], "output_pattern": [], "new_path": []}
287
+
288
+ total = len(self.graph)
289
+ start = time.perf_counter()
290
+
291
+ for index, (old_path, input_pattern, node, output_pattern, new_path, valid) in enumerate(self.graph, start=1):
292
+ elapsed = time.perf_counter() - start
293
+ eta = self._format_eta(elapsed, index - 1, total)
294
+ print(f"\rSaving graph: {index}/{total} ({(index / total * 100):.1f}%) - ETA {eta}", end="", flush=True)
295
+
296
+ if input_pattern and output_pattern:
297
+ if valid:
298
+ routable_paths["path"].append(str(old_path))
299
+ routable_paths["node"].append(node.__name__)
300
+ routable_paths["input_pattern"].append(self._label(input_pattern))
301
+ routable_paths["output_pattern"].append(self._label(output_pattern))
302
+ routable_paths["new_path"].append(str(new_path))
303
+ else:
304
+ problem_paths["path"].append(str(old_path))
305
+ problem_paths["node"].append(node.__name__)
306
+ problem_paths["input_pattern"].append(self._label(input_pattern))
307
+ problem_paths["output_pattern"].append(self._label(output_pattern))
308
+ problem_paths["new_path"].append(str(new_path))
309
+ else:
310
+ clean_paths["path"].append(str(old_path))
311
+ clean_paths["node"].append("")
312
+ clean_paths["input_pattern"].append("")
313
+ clean_paths["output_pattern"].append("")
314
+ clean_paths["new_path"].append("")
315
+
316
+ print() # Finish the progress line
317
+ print(f"Graph saved to {path} in {(time.perf_counter() - start):.1f}s")
318
+ print(f" Routable paths: {len(routable_paths['path'])}")
319
+ print(f" Problem paths: {len(problem_paths['path'])}")
320
+ print(f" Clean paths: {len(clean_paths['path'])}")
321
+ pd.DataFrame(routable_paths).to_csv(path / "routable_paths.csv", index=False)
322
+ pd.DataFrame(problem_paths).to_csv(path / "problem_paths.csv", index=False)
323
+ pd.DataFrame(clean_paths).to_csv(path / "clean_paths.csv", index=False)
324
+
325
+ # The set of configured routes, independent of any matched paths.
326
+ routes = {"input_pattern": [], "node": [], "output_pattern": []}
327
+ for input_pattern, node, output_pattern in self.router.values():
328
+ routes["input_pattern"].append(self._label(input_pattern))
329
+ routes["node"].append(node.__name__)
330
+ routes["output_pattern"].append(self._label(output_pattern))
331
+ pd.DataFrame(routes).to_csv(path / "routes.csv", index=False)
332
+ print(f" Routes: {len(routes['input_pattern'])}")
333
+
334
+ print(f"Saved graph to {path}")
@@ -0,0 +1,12 @@
1
+ """Smoke tests: verify the package imports and exposes its public API."""
2
+
3
+ import prouter
4
+
5
+
6
+ def test_package_imports():
7
+ assert prouter.__doc__
8
+
9
+
10
+ def test_graphbuilder_is_exported():
11
+ assert "GraphBuilder" in prouter.__all__
12
+ assert hasattr(prouter, "GraphBuilder")
prouter-0.1.0/uv.lock ADDED
@@ -0,0 +1,263 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.12"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
6
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
7
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
8
+ "python_full_version < '3.14' and sys_platform == 'win32'",
9
+ "python_full_version < '3.14' and sys_platform == 'emscripten'",
10
+ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
11
+ ]
12
+
13
+ [[package]]
14
+ name = "colorama"
15
+ version = "0.4.6"
16
+ source = { registry = "https://pypi.org/simple" }
17
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
18
+ wheels = [
19
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
20
+ ]
21
+
22
+ [[package]]
23
+ name = "iniconfig"
24
+ version = "2.3.0"
25
+ source = { registry = "https://pypi.org/simple" }
26
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
27
+ wheels = [
28
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
29
+ ]
30
+
31
+ [[package]]
32
+ name = "numpy"
33
+ version = "2.4.6"
34
+ source = { registry = "https://pypi.org/simple" }
35
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
36
+ wheels = [
37
+ { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
38
+ { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
39
+ { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
40
+ { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
41
+ { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
42
+ { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
43
+ { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
44
+ { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
45
+ { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
46
+ { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
47
+ { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
48
+ { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
49
+ { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
50
+ { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
51
+ { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
52
+ { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
53
+ { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
54
+ { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
55
+ { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
56
+ { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
57
+ { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
58
+ { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
59
+ { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
60
+ { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
61
+ { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
62
+ { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
63
+ { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
64
+ { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
65
+ { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
66
+ { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
67
+ { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
68
+ { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
69
+ { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
70
+ { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
71
+ { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
72
+ { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
73
+ { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
74
+ { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
75
+ { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
76
+ { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
77
+ { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
78
+ { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
79
+ { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
80
+ { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
81
+ { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
82
+ { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
83
+ { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
84
+ { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
85
+ { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
86
+ { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
87
+ { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
88
+ { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
89
+ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
90
+ ]
91
+
92
+ [[package]]
93
+ name = "packaging"
94
+ version = "26.2"
95
+ source = { registry = "https://pypi.org/simple" }
96
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
99
+ ]
100
+
101
+ [[package]]
102
+ name = "pandas"
103
+ version = "3.0.3"
104
+ source = { registry = "https://pypi.org/simple" }
105
+ dependencies = [
106
+ { name = "numpy" },
107
+ { name = "python-dateutil" },
108
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
109
+ ]
110
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" },
113
+ { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" },
114
+ { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" },
115
+ { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" },
116
+ { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" },
117
+ { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" },
118
+ { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" },
119
+ { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" },
120
+ { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
121
+ { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
122
+ { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
123
+ { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
124
+ { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
125
+ { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
126
+ { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
127
+ { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
128
+ { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
129
+ { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
130
+ { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
131
+ { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
132
+ { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
133
+ { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
134
+ { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
135
+ { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
136
+ { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
137
+ { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
138
+ { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
139
+ { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
140
+ { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
141
+ { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
142
+ { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
143
+ { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
144
+ { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
145
+ { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
146
+ { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
147
+ { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
148
+ { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
149
+ { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
150
+ { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
151
+ ]
152
+
153
+ [[package]]
154
+ name = "pluggy"
155
+ version = "1.6.0"
156
+ source = { registry = "https://pypi.org/simple" }
157
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
158
+ wheels = [
159
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
160
+ ]
161
+
162
+ [[package]]
163
+ name = "prouter"
164
+ version = "0.1.0"
165
+ source = { editable = "." }
166
+ dependencies = [
167
+ { name = "pandas" },
168
+ ]
169
+
170
+ [package.dev-dependencies]
171
+ dev = [
172
+ { name = "pytest" },
173
+ { name = "ruff" },
174
+ ]
175
+
176
+ [package.metadata]
177
+ requires-dist = [{ name = "pandas", specifier = ">=3.0.3" }]
178
+
179
+ [package.metadata.requires-dev]
180
+ dev = [
181
+ { name = "pytest", specifier = ">=8.0" },
182
+ { name = "ruff", specifier = ">=0.9.0" },
183
+ ]
184
+
185
+ [[package]]
186
+ name = "pygments"
187
+ version = "2.20.0"
188
+ source = { registry = "https://pypi.org/simple" }
189
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
190
+ wheels = [
191
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
192
+ ]
193
+
194
+ [[package]]
195
+ name = "pytest"
196
+ version = "9.0.3"
197
+ source = { registry = "https://pypi.org/simple" }
198
+ dependencies = [
199
+ { name = "colorama", marker = "sys_platform == 'win32'" },
200
+ { name = "iniconfig" },
201
+ { name = "packaging" },
202
+ { name = "pluggy" },
203
+ { name = "pygments" },
204
+ ]
205
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
206
+ wheels = [
207
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
208
+ ]
209
+
210
+ [[package]]
211
+ name = "python-dateutil"
212
+ version = "2.9.0.post0"
213
+ source = { registry = "https://pypi.org/simple" }
214
+ dependencies = [
215
+ { name = "six" },
216
+ ]
217
+ sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
218
+ wheels = [
219
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
220
+ ]
221
+
222
+ [[package]]
223
+ name = "ruff"
224
+ version = "0.15.16"
225
+ source = { registry = "https://pypi.org/simple" }
226
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
227
+ wheels = [
228
+ { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
229
+ { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
230
+ { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
231
+ { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
232
+ { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
233
+ { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
234
+ { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
235
+ { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
236
+ { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
237
+ { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
238
+ { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
239
+ { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
240
+ { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
241
+ { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
242
+ { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
243
+ { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
244
+ { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
245
+ ]
246
+
247
+ [[package]]
248
+ name = "six"
249
+ version = "1.17.0"
250
+ source = { registry = "https://pypi.org/simple" }
251
+ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
252
+ wheels = [
253
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
254
+ ]
255
+
256
+ [[package]]
257
+ name = "tzdata"
258
+ version = "2026.2"
259
+ source = { registry = "https://pypi.org/simple" }
260
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
261
+ wheels = [
262
+ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
263
+ ]