Compare commits

...

65 commits
v0.1.0 ... main

Author SHA1 Message Date
Thomas (Tom) C. Gorordo
6fcdae8792
Update README 2026-06-05 14:49:08 -07:00
Thomas (Tom) C. Gorordo
c324dc9c9f
Update README 2026-06-05 14:48:08 -07:00
e0d0134fdd
add IRV smith-set resolution method 2026-06-05 14:39:41 -07:00
Thomas (Tom) C. Gorordo
fc0569e252
Update README.md 2026-06-04 15:23:03 -07:00
Thomas (Tom) C. Gorordo
51d651bee6
Update README.md 2026-06-04 15:07:22 -07:00
Thomas (Tom) C. Gorordo
bc146d523d
Update README.md 2026-06-04 13:23:05 -07:00
Thomas (Tom) C. Gorordo
17c121f0af
Update README.md 2026-06-04 13:04:49 -07:00
Thomas (Tom) C. Gorordo
e9c263a8c5
Update README.md 2026-06-04 12:48:24 -07:00
Thomas (Tom) C. Gorordo
fa24eb5758
Update README.md 2026-06-04 12:35:21 -07:00
Thomas (Tom) C. Gorordo
55f64d1a32
Update README.md 2026-06-04 12:34:54 -07:00
Thomas (Tom) C. Gorordo
3dc8a3cb46
Update README.md 2026-06-04 12:31:35 -07:00
Thomas (Tom) C. Gorordo
845ac38533
Update README.md 2026-06-04 12:30:02 -07:00
Thomas (Tom) C. Gorordo
b24920c15a
Update README.md 2026-06-04 12:19:39 -07:00
Thomas (Tom) C. Gorordo
24243aad2f
Update README.md 2026-05-27 23:04:51 -07:00
Thomas (Tom) C. Gorordo
f762fd5a23
Update form.html 2026-05-25 23:09:23 -07:00
Thomas (Tom) C. Gorordo
57caad7f27
Update README 2026-05-25 22:31:39 -07:00
Thomas (Tom) C. Gorordo
1a6de605ad
Update README 2026-05-25 22:28:32 -07:00
Thomas (Tom) C. Gorordo
d1db77c466
Revise README
Updated README to improve clarity and add usage details.
2026-05-25 22:25:52 -07:00
18405644d3
sync 2026-05-25 22:23:57 -07:00
9e1e5d9642
version bump to 0.2.0 2026-05-25 19:09:38 -07:00
be82bc4ba5
add envrc 2026-05-25 18:40:32 -07:00
368770d792
switch default to numpy pmg building 2026-05-25 18:01:26 -07:00
Thomas (Tom) C. Gorordo
1a668a7cee
Update README.md 2026-05-25 15:10:50 -07:00
Thomas (Tom) C. Gorordo
808df7b4d4
Update README.md 2026-05-25 14:44:40 -07:00
Thomas (Tom) C. Gorordo
16ea974654
Update README.md 2026-05-25 14:39:59 -07:00
Thomas (Tom) C. Gorordo
70cda86acb
Update README.md 2026-05-25 14:38:05 -07:00
Thomas (Tom) C. Gorordo
144d9cc9a9
Update README 2026-05-25 13:38:16 -07:00
Thomas (Tom) C. Gorordo
c5c93f5aa2
Update README.md 2026-05-25 08:01:54 -07:00
c12be4d9db
two paths for pmg_from_rcv 2026-05-25 07:57:18 -07:00
0c334f699e
comment compression in polars graph building 2026-05-25 07:41:20 -07:00
Thomas (Tom) C. Gorordo
19fadd84d8
Update README 2026-05-25 07:40:29 -07:00
2f07283f26
update README 2026-05-25 07:39:51 -07:00
22765fe052
switch graph-building to numpy, add ballot cacheing 2026-05-25 07:31:26 -07:00
86e880623f
upgrade deps 2026-05-25 07:18:40 -07:00
458015d214
tweak 2026-05-25 07:16:12 -07:00
674fdc1fe9
update to SCC algorithm, tidy internal names 2026-05-25 07:08:08 -07:00
fcf2505820
adding deps for algo upgrade 2026-05-25 04:48:44 -07:00
62923fefaf
rm test file 2026-05-25 03:22:34 -07:00
3f9f4e6c98
rework cli printing options 2026-05-25 03:16:36 -07:00
915ba47ccc
update justfile 2026-05-25 02:37:05 -07:00
Thomas (Tom) C. Gorordo
43e060ee03
Update README 2026-05-25 02:35:39 -07:00
Thomas (Tom) C. Gorordo
a66b1efeae
Update README 2026-05-25 02:33:02 -07:00
Thomas (Tom) C. Gorordo
ee89a7bf11
Update README 2026-05-25 02:31:09 -07:00
Thomas (Tom) C. Gorordo
a7f265c103
Update README.md 2026-05-25 02:30:43 -07:00
Thomas (Tom) C. Gorordo
f71ec59d01
Update README.md 2026-05-25 02:28:39 -07:00
92b464fccf
refactor core a bit, prep for better algo 2026-05-25 02:26:48 -07:00
Thomas (Tom) C. Gorordo
a66116712b
Update README 2026-05-25 00:36:08 -07:00
Thomas (Tom) C. Gorordo
ea9652be87
Update README 2026-05-25 00:32:02 -07:00
Thomas (Tom) C. Gorordo
dd0b306c1e
Update README 2026-05-25 00:30:32 -07:00
5d0f1da322
fix cli compilation 2026-05-25 00:29:37 -07:00
8b39991bcd
move cli script to reduce core package dependencies 2026-05-25 00:15:49 -07:00
2138a0ea6b
clarify some cgi form text 2026-05-22 04:22:18 -07:00
3e56cbfa18
allow source inspection of cgi script 2026-05-22 04:11:48 -07:00
7a30e0a85d
allow source inspection of cgi script 2026-05-22 04:10:21 -07:00
84d6380081
allow source inspection of cgi script 2026-05-22 04:09:27 -07:00
47de09c59c
fix audit link in cgi form.html 2026-05-22 04:02:08 -07:00
4cc6f98273
fix form response backlinks 2026-05-22 03:51:59 -07:00
4e437fa570
permissions 2026-05-22 03:47:08 -07:00
a945d2e8a4
final debug cgi 2026-05-22 03:45:42 -07:00
497fecd997
Merge branch 'main' of https://github.com/tgorordo/smithy 2026-05-22 03:16:31 -07:00
b8b7bb5e9c
rename smithy cgi html form 2026-05-22 03:16:05 -07:00
Thomas (Tom) C. Gorordo
5b4a819113
Update README.md 2026-05-22 03:14:41 -07:00
8ab9667978
debug cgi interface 2026-05-22 02:43:43 -07:00
c57068b34c
implement cgi interface 2026-05-22 01:07:50 -07:00
174df7e572
add marimo to justfile 2026-05-20 12:21:50 -07:00
20 changed files with 4094 additions and 255 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
__pycache__/
.venv
.venv/
venv/
smithycmd.spec
smithygui.spec
build/
dist/
.ipynb_checkpoints
.ruff_cache/
.pytest_cache/
.benchmarks/

181
README.md
View file

@ -1,5 +1,180 @@
# Smithy
*A simple smith set solver for ranked-choice ballots.*
*A simple Smith set solver for ranked-choice ballots.*
The [Smith set](https://en.wikipedia.org/wiki/Smith_set) is the minimal set of election candidates which can beat all others pairwise
(by simple majority ranking preference) - if there is a single winner in the set they are
guaranteed the standard [Condorcet i.e. Majority winner](https://en.wikipedia.org/wiki/Condorcet_winner) (they beat all others pairwise).
`smithy` identifies the Smith set via graph Strongly Connected Component (SCC) analysis of
the pairwise majority graph using [`rustworkx`](https://www.rustworkx.org/).
Pairwise majority comparisons scale quadratically in the number of candidates and linearly
in the number of ballots, while the SCC and condensation graph analysis is
approximately quadratic in the number of candidates for the dense tournament graphs typical
of Condorcet elections. Internally, repeated ballots are compressed/cache-counted before
pairwise evaluation to improve performance over duplicate rankings.
This is all overkill for small elections, but is fun.
Optionally, `smithy` can try to further resolve a nontrivial Smith set (a majoritarian tie or cycle) by running all-paths IRV within the set - at least reducing to an IRV winner set (the set of candidates that win at least one IRV elimination path) within the Smith set that are not only pairwise competitive but can also build competitive plurality
coalitions within the set; in practice this often resolves cycles and is likely to result in a unique delegate (if they win all IRV elimination paths) which can claim a plurality coalition amongst the majority winners. This should identify a winning candidate as the best possible focal point for voluntary coordination; if your elections have other priorities you may want to resolve nontrivial Smith sets differently.
## Usage
`smithy` can be used a few different ways: as a CLI command, via a web form upload,
or imported as a python package (TODO: a small [GUI](https://doc.qt.io/qtforpython-6/)).
### CLI Command
`smithy` provides a [`click`](https://click.palletsprojects.com/en/stable/)
CLI command defined in `src/smithycmd.py` which can be invoked a couple of ways:
- The [GitHub Releases Page](https://github.com/tgorordo/smithy/releases) provides standalone
CLI executables bundled using [PyInstaller](https://pyinstaller.org/en/stable/index.html)
for linux and windows on `x86_64` and linux on `aarch64`. Downloading the appropriate
executable should give you access to the `smithycmd` CLI command.
- Alternatively, if you are using `uv` (see [Development](#Development)), you can invoke
this in your shell as `uv run src/smithycmd.py [...]` from within the repo after cloning it locally.
In either case, the command expects the same argument structure:
```bash
$ uv run src/smithycmd.py --help
Usage: smithycmd.py [OPTIONS] BALLOTS
Compute the Smith set from a box of ranked-choice ballots -- .csv or
.xls(x).
The Smith set is the minimal set of candidates which can beat all others
pairwise (simple ranking majority) - if there is a single winner in the set,
they are guaranteed the Condorcet i.e. Majority winner.
Options:
-b, --show-ballots Show relevant ballots (after selections).
-p, --pretty Pretty-print output.
--help Show this message and exit.
```
where the ballots file should be a `.csv` or `.xls`(`x`) whose columns are candidates and whose
rows are numerical RCV ballot rankings (lower number is best, 1st, 2nd, 3rd, etc.).
(see the [web deployment](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/form.html)
or `test/` for some examples).
If you want a 'None' option in your election, it should be included as a candidate.
While plain output is the default, so that the command can easily be used in a unix-pipe
or `stdio` workflows, it can also pretty-print its output for your reading pleasure
using [`rich`](https://rich.readthedocs.io/en/stable/introduction.html) if you pass it the
`--pretty` (or `-p`) flag. Whether to show the input ballots during output is also
controlled by a corresponding flag.
### UOregon CGI Application
[**This** `pages.uoregon.edu` page](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/form.html)
hosts a [CGI form](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069)
which accepts a `.csv` or `.xls`(`x`) upload of a ballot-box and responds with the resulting
Smith set. The application can typically handle ~100s of candidates and ~10ks of votes
before running into hosting resource limitations and timing out.
No ballot data is retained on the server after execution.
### Python Package
The `smithy` python package primarily provides the function `smith_set(ballot_box_df)`
(defined in `src/smithy`), which returns the smith set given a
[polars](https://docs.pola.rs/api/python/stable/reference/index.html)
dataframe `ballot_box_df` whose columns are candidates and whose rows are RCV ballots.
e.g. `smithy` can be imported into a [marimo](https://docs.marimo.io/#highlights) notebook
as seen in `test/test_nb.py` - if you're using `uv` you can launch marimo via
`uv run marimo --edit` and navigate to that example notebook
(`just marimo` is an available shorthand if you are using the `just` taskrunner --
see [Development](#Development)). Cloning `smithy` then working in scratch notebooks within the repo
(e.g. makeing your own `nbs/` directory)
is probably the easiest way to use it if you need to do some of cleanup of the tabular
data before invocation - just do your cleanup in a notebook using
[polars](https://docs.pola.rs/py-polars/html/reference/) then pass a tidy dataframe to `smith_set(df)`.
Of course, you can also import into any python script or package for a more involved project
as well:
- If you're using `uv` adding `smithy` should be as simple as
```bash
uv add git+https://github.com/tgorordo/smithy.git
```
- Alternatively, if you're using the more default `pip` you can make it available by
```bash
git clone https://github.com/tgorordo/smithy.git # or submodule add somewhere appropriate in your project
cd smithy
pip install .
```
(if you're new to pip-venvs,
[this brief guide might be helpful](https://pages.uoregon.edu/tgorordo/courses/uoph410-510a_Image-Analysis/setup.html)).
(The core algorithm is also pretty dead simple, and you could just copy it over into your project too).
## Development
Current development tooling is based on the [`uv`](https://docs.astral.sh/uv/) Python package and
project manager. A [`justfile`](https://github.com/casey/just) lists some common useful
task commands. A simple [`shell.nix`](https://nix.dev/tutorials/first-steps/declarative-shell.html)
can be used to load these tools into a local environment, and you might find
[`direnv`](https://github.com/direnv/direnv) a convenient pairing to `nix` - but just having a
system-provided `uv` available is more than enough to use and develop `smithy`.
### Formatting, Checking, Testing and Locking
Source can be formatted using [`ruff`](https://docs.astral.sh/ruff/) by running
```bash
uv run ruff format src test
```
(or `just format`)
which should be done before any `git` commit.
This is not really taken full advantage of at the moment, but type hints can be checked
by running [pyright](https://github.com/microsoft/pyright)
```bash
uv run pyright src
```
(or `just check`).
A [pytest](https://docs.pytest.org/en/stable/) test suite can be run as
```bash
uv run pytest -vvv --tb=short --log-cli-level=INFO
```
(or `just test`).
Dependencies can be locked by running
```bash
uv lock
uv pip compile pyproject.toml -o requirements.txt --group dev
```
(or `just lock`).
### Bundling, Releases, Deployment and Cleanup
Bundling the CLI command with [PyInstaller](https://pyinstaller.org/en/stable/index.html) can be done by
```bash
uv run --with-requirements src/smithycmd.py pyinstaller --clean -F src/smithycmd.py
```
(or `just compile`)
which will output an executable `smithycmd` for the CLI in `dist/`, which can be uploaded
to the latest [GitHub Releases Page](https://github.com/tgorordo/smithy/releases).
Deploying the main CGI page at
[`pages.uoregon.edu/tgorordo/files/smithy/src/cgi`](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi)
requires shell or SFTP access to [my `pages.uoregon.edu`](https://pages.uoregon.edu/tgorordo),
which you won't get unless you're [me](https://github.com/tgorordo).
If you want to deploy it to your own `pages.uoregon.edu` page, then it should be enough to
ensure you have [CGI set up](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069)
then SFTP into `sftp.uoregon.edu` and `put -r smithy` (or git clone when `ssh`ed into
`shell.uoregon.edu`) wherever you'd like in your `public_html`, ensure `smithy.cgi` has
`chmod 755` permissions,
[standalone install `uv` for your user](https://docs.astral.sh/uv/getting-started/installation/)
then run `uv sync` from somewhere inside the `smithy` directory. Navigating to the `smithy.cgi`
file in `pages.uoregon.edu/YOUR_USER/.../cgi/smithy.cgi` should give you a working form.
If you're using `just` you can cleanup working files by running `just clean` and can fully
wipe your `uv` generated `.venv` with `just wipe` (if you want manual versions of those,
check the `justfile` definition). It's probably best to do this before any commit as well,
though probably not required if you're careful or have `.gitignore` updated appropriately
for any changes you made.
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the standard Condorcet i.e. Majority winner (they beat all others pairwise).

View file

@ -1,8 +1,20 @@
list:
just --list
just --list --unsorted
run spreadsheet:
uv run smithy {{spreadsheet}}
run *args:
uv run src/smithycmd.py {{args}}
python *arguments:
uv run python -c {{arguments}}
marimo:
uv run marimo --edit
sync *args:
uv sync {{args}}
format:
uv run ruff format src test
check:
uv run pyright src
@ -10,20 +22,13 @@ check:
test:
uv run pytest -vvv --tb=short --log-cli-level=INFO
format:
uv run ruff format src test
example:
uv run python src/main.py test/test_ballot.csv
compile:
uv run pyinstaller --clean -F src/main.py --name smithy
uv run --with-requirements src/smithycmd.py pyinstaller --clean -F src/smithycmd.py
clean:
uv run pyclean src test
uv run ruff clean
rm -rf main.spec cli.spec build dist .pytest_cache .hypothesis .benchmarks __marimo__
rm -rf smithycmd.spec build dist .pytest_cache .hypothesis .benchmarks __marimo__
wipe:
just clean

View file

@ -1,6 +1,6 @@
[project]
name = "smithy"
version = "0.1.0"
version = "0.2.0"
description = "Add your description here"
readme = "README.md"
authors = [
@ -8,13 +8,19 @@ authors = [
]
requires-python = ">=3.13"
dependencies = [
"click>=8.4.0",
"numpy>=2.4.6",
"polars>=1.40.1",
"rustworkx>=0.17.1",
]
[project.optional-dependencies]
cli = [
"click>=8.4.0",
"rich>=15.0.0",
]
[project.scripts]
smithy = "smithy:cli"
#[project.scripts]
#smithy = "smithycmd:cli"
[build-system]
requires = ["uv_build>=0.11.7,<0.12.0"]
@ -33,3 +39,11 @@ dev = [
"pytest-benchmark>=5.2.3",
"ruff>=0.15.13",
]
[pytest]
testpaths = "test"
log_cli = true
[tool.pyright]
include = ["src"]
exclude = ["test"]

View file

@ -24,7 +24,6 @@ charset-normalizer==3.4.7
# via requests
click==8.4.0
# via
# smithy (pyproject.toml)
# marimo
# uvicorn
distro==1.9.0
@ -84,12 +83,8 @@ markdown==3.10.2
# via
# marimo
# pymdown-extensions
markdown-it-py==4.2.0
# via rich
markupsafe==3.0.3
# via jinja2
mdurl==0.1.2
# via markdown-it-py
msgspec==0.21.1
# via marimo
narwhals==2.21.2
@ -100,6 +95,10 @@ nbformat==5.10.4
# via marimo
nodeenv==1.10.0
# via pyright
numpy==2.4.6
# via
# smithy (pyproject.toml)
# rustworkx
openai==2.37.0
# via pydantic-ai-slim
opentelemetry-api==1.42.0
@ -151,7 +150,6 @@ pygments==2.20.0
# via
# marimo
# pytest
# rich
pyinstaller==6.20.0
# via smithy (pyproject.toml:dev)
pyinstaller-hooks-contrib==2026.5
@ -184,8 +182,6 @@ regex==2026.5.9
# via tiktoken
requests==2.34.2
# via tiktoken
rich==15.0.0
# via smithy (pyproject.toml)
rpds-py==0.30.0
# via
# jsonschema
@ -194,6 +190,8 @@ ruff==0.15.13
# via
# smithy (pyproject.toml:dev)
# marimo
rustworkx==0.17.1
# via smithy (pyproject.toml)
setuptools==82.0.1
# via
# pyinstaller

View file

@ -1,38 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['src/main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='smithy',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

103
src/cgi/form.html Normal file
View file

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Smithy - RCV Ballot Counter </title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
}
input[type="file"] {
margin-bottom: 15px;
}
input[type="submit"] {
padding: 8px 16px;
}
</style>
</head>
<body>
<h1>Smithy: RCV Ballot Counter - CGI Application Upload</h1>
<div class="form-container">
<form action="./smithy.cgi" method="POST" enctype="multipart/form-data">
<label for="spreadsheet">Upload a ballot-box spreadsheet (.xlsx, .xls, .csv):</label>
<input type="file" id="spreadsheet" name="spreadsheet" accept=".xlsx,.xls,.csv" required>
<br>
<input type="submit" value="Upload and Solve!">
</form>
</div>
<div class="explanation-container">
<h2>About</h2>
<p>This is an upload form for the <a href="https://github.com/tgorordo/smithy">smithy</a> RCV Ballot counter for identifying the Smith set (majority winners) in small elections,
mainly to help with <a href="https://blogs.uoregon.edu/physicsgsg/">UO Physics Grad student elections</a> of various flavors. This form uses the <a href="https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069">UO pages.uoregon.edu CGI Capability</a>,
so the implementation of smithy being invoked can be <a href="https://pages.uoregon.edu/tgorordo/files/smithy/"> inspected here</a>
and you may also inspect the source of this page to verify that <a href="https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/smithy.cgi?source">this script<a> is called to invoke it - though
you have to trust it to <a href="https://en.wikipedia.org/wiki/Quine_(computing)">quine</a> itself faithfully.
</p>
<p> The <a href="https://en.wikipedia.org/wiki/Smith_set">Smith set</a> is the minimal set of candidates which can beat all others pairwise (by majority ranking)
- if there is a single winner in the set they are guaranteed the standard <a href=https://en.wikipedia.org/wiki/Condorcet_winner>Condorcet i.e. Majority winner</a>
(they beat all others pairwise).</p>
<h3>Input: Expected Spreadsheet Format</h3>
<p>A ballot-box spreadsheet should be organized by columns as candidates and rows as numerical rankings of the candidates -
i.e. each row is the ranking provided by the ballot of a voter,
e.g.:</p>
<p><a href="../../test/test_ballot.csv">test_ballot.csv<a>
<table>
<tr>
<th>Alice</th>
<th>Bob</th>
<th>Charlie</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>3</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>1</td>
<td>2</td>
</tr>
</table>
where this reads that the first voter had preferences "Alice" over "Bob" over "Charlie", the second voter "Bob" over "Alice" over "Charlie", etc.
(This example is constructed to demonstrate a situation without a singular majority winner - its Smith set a tie between "Alice" and "Bob"). Rankings may contain ties by e.g. repeating a numerical rank.</p>
<h3>Output: Smith Set Format</h3>
<p>The form will return a list of the Smith-set winners sorted lexicographically (they are all equally good majority winners).
It might be helpful for clarity to provide the input sheet's columns already in lexicographic order, but this is not required. </p>
</div>
</body>
<footer>
<hr>
<p>Author: <a href="https://pages.uoregon.edu/tgorordo">Thomas (Tom) C. Gorordo</a>
Source: <a href="https://github.com/tgorordo/pages.uoregon.edu">pages.uoregon.edu/tgorordo</a>,
<a href="https://github.com/tgorordo/smithy">smithy</a></p>
</footer>
</html>

144
src/cgi/script.py Normal file
View file

@ -0,0 +1,144 @@
import traceback
import sys, os
import html
import re
import polars as pl
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../smithy/src")
),
)
from smithy import smith_set_from_rcv
print("Content-Type: text/html\n")
message = ""
content_type = os.environ.get("CONTENT_TYPE", "")
content_length = int(os.environ.get("CONTENT_LENGTH", 0) or 0)
boundary = content_type.split("boundary=")[-1].encode()
body = sys.stdin.buffer.read(content_length)
parts = body.split(b"--" + boundary)
spreadsheet = None
for part in parts:
if b"Content-Disposition" in part and b'name="spreadsheet"' in part:
header, _, data = part.partition(b"\r\n\r\n")
filename_match = re.search(rb'filename="([^"]+)"', header)
if filename_match:
filename = filename_match.group(1).decode()
filedata = data.rstrip(b"\r\n--")
spreadsheet = (filename, filedata)
break
if spreadsheet is not None:
filename, filedata = spreadsheet
if filename and filedata:
filepath = os.path.join("/tmp", filename)
with open(filepath, "wb") as f:
f.write(filedata)
try:
if filename.endswith(".csv"):
df = pl.read_csv(filepath)
elif filename.endswith((".xlsx", ".xls")):
df = pl.read_excel(filepath)
else:
message = """
<h1>Error</h1>
<p>File extension is not valid. Use CSV (.csv) or Excel (.xlsx, .xls).</p>
<p><a href="form.html">Go Back</a></p>
"""
if df is not None:
# Normalize
df = df.with_columns(
[
pl.col(c)
.cast(pl.Utf8)
.str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(0)
for c in df.columns
]
)
smiths = smith_set_from_rcv(df) # Solve!
message = f"""
<h1>The Smith set winners are:</h1>
{smiths}
<p><a href="form.html">Go Back</a></p>
"""
else:
message = """
<h1>Error</h1>
<p>DataFrame was empty.</p>
<p><a href="form.html">Go Back</a></p>
"""
except Exception as e:
message = f"""
<h1>Error</h1>
<p>Internal Error Encountered: {e}
<p><a href="form.html">Go Back</a></p>
"""
traceback.print_exc()
else:
message = """
<h1>Error</h1>
<p>Filename or File Data not found/valid in form submission.</p>
<p><a href="form.html">Go Back</a></p>
"""
else:
message = """
<h1>Error</h1>
<p>No file field found in the form.</p>
<p><a href="form.html">Go Back</a></p>
"""
print("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Smithy - RCV Ballot Counter </title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
</style>
</head>
<body>
""")
print(message)
print("""
</body>
<footer>
<hr>
<p>Author: <a href="https://pages.uoregon.edu/tgorordo">Thomas (Tom) C. Gorordo</a>
Source: <a href="https://github.com/tgorordo/pages.uoregon.edu">pages.uoregon.edu/tgorordo</a>,
<a href="https://github.com/tgorordo/smithy">smithy</a></p>
</footer>
</html>
""")

12
src/cgi/smithy.cgi Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ "$QUERY_STRING" == "source" ]]; then
echo "Content-Type: text/plain"
echo
sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' "$0"
exit 0
fi
exec "$SCRIPT_DIR/../../.venv/bin/python" "$SCRIPT_DIR/script.py"

View file

@ -1,4 +0,0 @@
from smithy import cli
if __name__ == "__main__":
cli()

View file

@ -1,77 +1,79 @@
import click
import polars as pl
import rustworkx as rwx
from itertools import combinations
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from .rcv import smith_set
from .rcv import pmg_from_rcv
from .irv import irv_from_rcv
@click.command()
@click.argument("spreadsheet", type=click.Path(exists=True, dir_okay=False))
def cli(spreadsheet: str) -> None:
def ss_from_pmg(pmg: rwx.PyDiGraph) -> list[str]:
"""
Compute the Smith set from a ranked-choice ballot spreadsheet.
Find the Smith set from a pairwise majority graph.
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the Condorcet i.e. Majority winner.
parameters
---
pmg: rwx.PyDiGraph
A graph whose nodes correspond to candidates and (directed) edges show
which candidates they beat pairwise.
returns
---
smith_set: list
A list of the Smith set candidates - all are equally good winners;
ordering is determined lexicographically. If there is a Condorcet winner
(single Majority winner), the Smith set will contain that single candidate.
"""
console = Console()
sccs = rwx.strongly_connected_components(pmg)
try:
# Load spreadsheet
if spreadsheet.endswith(".csv"):
df = pl.read_csv(spreadsheet)
cg = rwx.condensation(pmg, sccs)
elif spreadsheet.endswith((".xlsx", ".xls")):
df = pl.read_excel(spreadsheet)
src_sccs = [nd for nd in cg.node_indices() if cg.in_degree(nd) == 0]
else:
console.print(
"[bold red]Unsupported file type.[/bold red]\nUse CSV or Excel."
)
raise SystemExit(1)
smith_set = sorted([c for scc in src_sccs for c in cg[scc]])
# Normalize numerical dataframe entries
df = df.with_columns(
[
pl.col(c)
.cast(pl.Utf8)
.str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(0)
for c in df.columns
]
return smith_set
def smith_set_from_rcv(ballots: pl.DataFrame) -> list:
"""
Compute the Smith set from a Ranked-Choice ballot.
The Smith set is the minimal set of candidates which can beat all others pairwise -
if there is a single winner in the set they are guaranteed the Condorcet i.e. Majority winner.
parameters
---
ballots : pl.DataFrame
A Polars DataFrame representing ballots. Each column is a candidate and each
row is is a voter's ranking of the candidates. Lower numbers indicate higher
preference (1 = top-choice).
returns
---
smith_set : list
A list of the Smith set candidates - all are equally good winners;
ordering is determined lexicographically. If there is a Condorcet winner
(single Majority winner), the Smith set will contain that single candidate.
"""
return ss_from_pmg(pmg_from_rcv(ballots))
def smith_set(df: pl.DataFrame, ballotkind="rcv") -> list:
if ballotkind == "rcv":
return smith_set_from_rcv(df)
else:
raise NotImplementedError(
f"`smith_set` ballotkind={ballotkind} is not implemented."
)
# Compute Smith set
smiths = smith_set(df)
# Preview table
preview = Table(title="Ballot Box")
for col in df.columns:
preview.add_column(col)
for row in df.head(5).iter_rows():
preview.add_row(*map(str, row))
console.print(preview)
# Results
console.print()
console.print(
Panel.fit(
"\n".join(f"{c}" for c in smiths),
title="Resulting Smith Set",
border_style="green",
)
def irv_set(df: pl.DataFrame, ballotkind="rcv") -> list:
if ballotkind == "rcv":
return irv_from_rcv(df)
else:
raise NotImplementedError(
f"`irv_set` ballotkind={ballotkind} is not implemented."
)
except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}")
raise SystemExit(1)

102
src/smithy/irv.py Normal file
View file

@ -0,0 +1,102 @@
import polars as pl
import numpy as np
def irv_from_rcv(ballots: pl.DataFrame, method: str = "bigslow") -> list[str]:
"""
Compute the set of all-paths IRV winners from an RCV ballot.
parameters
---
ballots: pl.DataFrame
An RCV table of ballots.
method: str
Either "bigslow" or "smallfast" for selecting an internal method for counting
first-choices during IRV rounds. Defaults to "bigslow" but you can use "smallfast"
so long as the table of ballots is expected to fit in a reasonable numpy array (after compression).
returns
---
winners: list[srt]
A lexicographically sorted list of IRV winners. If a candidate wins every elimination path
then this set will contain only one entry, otherwise it will contain all candidates that win
at least one IRV elimination path.
"""
compressed = ballots.group_by(ballots.columns).len().rename({"len": "count"})
return sorted(_irv_winners(compressed, method=method))
def _fst_counts_bigslow(compressed: pl.DataFrame) -> pl.DataFrame:
surviving = [c for c in compressed.columns if c != "count"]
fstcexpr = (
pl.concat_list([pl.col(c) for c in surviving])
.list.arg_min().map_elements(lambda i: surviving[i], return_dtype=pl.String).alias("first_choice")
)
tally = (
compressed.with_columns(fstcexpr)
.group_by("first_choice")
.agg(pl.col("count").sum())
.filter(pl.col("first_choice").is_not_null())
)
return tally
def _fst_counts_smallfast(compressed: pl.DataFrame) -> pl.DataFrame:
surviving = [c for c in compressed.columns if c != "count"]
a = compressed.select(surviving).to_numpy()
cs = compressed["count"].to_numpy()
fstc_idxs = np.argmin(a, axis=1)
tally = {c: 0 for c in surviving}
for i, c in zip(fstc_idxs, cs):
tally[surviving[i]] += int(c)
return pl.DataFrame(
{"first_choice": surviving, "count": [tally[c] for c in surviving]}
)
def _irv_round(compressed: pl.DataFrame, method="bigslow"):
if method == "bigslow":
count_fn = _fst_counts_bigslow
elif method == "smallfast":
count_fn = _fst_counts_smallfast
else:
raise NotImplementedError(
f"Error: _fst_counts method={method} not implemented."
)
tally = count_fn(compressed)
eliminate = tally.filter(pl.col("count") == pl.col("count").min())[
"first_choice"
].to_list()
for e in eliminate:
surviving = [c for c in compressed.columns if c not in ("count", e)]
yield (
compressed.select(surviving + ["count"])
.group_by(surviving)
.agg(pl.col("count").sum())
)
def _irv_winners(compressed, method="bigslow"):
surviving = [c for c in compressed.columns if c != "count"]
if len(surviving) == 1:
return set(surviving)
winners = set()
for branch in _irv_round(compressed, method=method):
winners |= _irv_winners(branch, method=method)
return winners

View file

@ -1,64 +1,118 @@
import polars as pl
import rustworkx as rwx
from itertools import combinations
def smith_set(df: pl.DataFrame) -> list:
def pmg_from_rcv_bigslow(ballots: pl.DataFrame) -> rwx.PyDiGraph:
"""
Compute the Smith set from a Ranked-Choice ballot.
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the Condorcet i.e. Majority winner.
Build a pairwise majority winner graph from a box of Ranked-Choice Ballots.
parameters
---
df : pl.DataFrame
rcv_ballots : pl.DataFrame
A Polars DataFrame representing ballots. Each column is a candidate and each
row is is a voter's ranking of the candidates. Lower numbers indicate higher
preference (1 = top-choice).
returns
---
smith_set : list
A list of the Smith set candidates - all are equally good winners; ordering is determined lexicographically.
If there is a Condorcet winner (single Majority winner), the Smith set will contain that single candidate.
nodes: dict[str, int]
A dictionary of candidate names to associated node ids.
pwm_graph: rwx.PyDiGraph
A pairwise majority winner graph whose nodes correspond to candidates and
(directed) edges show which candidates they beat pairwise.
"""
candidates = ballots.columns
candidates = df.columns
pmg = rwx.PyDiGraph()
nodes = {c: pmg.add_node(c) for c in candidates}
# Build pairwise majority graph
graph: dict[str, set[str]] = {c: set() for c in candidates}
compressed = ballots.group_by(ballots.columns).len().rename({"len": "count"})
for a, b in combinations(candidates, 2):
result = df.select(
exprs = []
pairs = list(combinations(candidates, 2))
for a, b in pairs:
exprs.extend(
[
(pl.col(a) < pl.col(b)).sum().alias("a_wins"),
(pl.col(b) < pl.col(a)).sum().alias("b_wins"),
pl.when(pl.col(a) < pl.col(b))
.then(pl.col("count"))
.otherwise(0)
.sum()
.alias(f"{a}>{b}"),
pl.when(pl.col(b) < pl.col(a))
.then(pl.col("count"))
.otherwise(0)
.sum()
.alias(f"{b}>{a}"),
]
).row(0)
)
a_wins, b_wins = result
results = compressed.select(exprs).row(0, named=True)
for a, b in pairs:
a_wins = results[f"{a}>{b}"]
b_wins = results[f"{b}>{a}"]
if a_wins > b_wins:
graph[a].add(b)
pmg.add_edge(nodes[a], nodes[b], a_wins - b_wins)
elif b_wins > a_wins:
graph[b].add(a)
pmg.add_edge(nodes[b], nodes[a], b_wins - a_wins)
# Find Smith set
for size in range(1, len(candidates) + 1):
for sub in combinations(candidates, size):
subset = set(sub)
out = set(candidates) - subset
return pmg
dom = True
for member in subset:
if not out.issubset(graph[member]):
dom = False
break
def pmg_from_rcv_smallfast(ballots: pl.DataFrame) -> rwx.PyDiGraph:
"""
Build a pairwise majority winner graph from a box of Ranked-Choice Ballots.
if dom:
return sorted(subset)
parameters
---
rcv_ballots : pl.DataFrame
A Polars DataFrame representing ballots. Each column is a candidate and each
row is is a voter's ranking of the candidates. Lower numbers indicate higher
preference (1 = top-choice).
return []
returns
---
nodes: dict[str, int]
A dictionary of candidate names to associated node ids.
pwm_graph: rwx.PyDiGraph
A pairwise majority winner graph whose nodes correspond to candidates and
(directed) edges show which candidates they beat pairwise.
"""
candidates = ballots.columns
pmg = rwx.PyDiGraph()
nodes = {c: pmg.add_node(c) for c in candidates}
compressed = ballots.group_by(ballots.columns).len().rename({"len": "count"})
counts = compressed["count"].to_numpy()
arr = compressed.drop("count").to_numpy()
results = ((arr[:, :, None] < arr[:, None, :]) * counts[:, None, None]).sum(axis=0)
for i, a in enumerate(candidates):
for j in range(i + 1, len(candidates)):
b = candidates[j]
a_wins = results[i, j]
b_wins = results[j, i]
if a_wins > b_wins:
pmg.add_edge(nodes[a], nodes[b], int(a_wins - b_wins))
elif b_wins > a_wins:
pmg.add_edge(nodes[b], nodes[a], int(b_wins - a_wins))
return pmg
def pmg_from_rcv(ballots: pl.DataFrame, method="bigslow") -> rwx.PyDiGraph:
if method == "bigslow":
return pmg_from_rcv_bigslow(ballots)
elif method == "smallfast":
return pmg_from_rcv_smallfast(ballots)
else:
raise NotImplementedError(f"`pmg_from_rcv` method={method} not implemented.")

115
src/smithycmd.py Normal file
View file

@ -0,0 +1,115 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "click>=8.4.1",
# "rich>=15.0.0",
# "polars>=1.40.1",
# "rustworkx>=0.17.1"
# ]
# ///
import sys, io
import click
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
import polars as pl
from smithy import smith_set, irv_set
@click.command()
@click.argument("ballots", type=click.Path(exists=True, dir_okay=False))
@click.option(
"--try-resolve-irv",
is_flag=True,
help="Try to reduce or resolve the Smith set by running all-paths IRV on the set.",
)
@click.option(
"--show-ballots",
"-b",
is_flag=True,
help="Show relevant ballots (after selections).",
)
@click.option("--pretty", "-p", is_flag=True, help="Pretty-print output.")
def cli(ballots: str, try_resolve_irv=False, show_ballots=False, pretty=False) -> None:
"""
Compute the Smith set from a box of ranked-choice ballots -- .csv or .xls(x).
The Smith set is the minimal set of candidates which can beat all others pairwise
(simple ranking majority) - if there is a single winner in the set,
they are guaranteed the Condorcet i.e. Majority winner.
"""
console = Console()
try:
# Load ballots
if ballots.endswith(".csv"):
df = pl.read_csv(ballots)
elif ballots.endswith((".xlsx", ".xls")):
df = pl.read_excel(ballots)
else:
console.print(
"[bold red]Unsupported file type.[/bold red]\nUse CSV or Excel."
)
raise SystemExit(1)
# Normalize numerical dataframe entries
df = df.with_columns(
[
pl.col(c)
.cast(pl.String)
.str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(0)
for c in df.columns
]
)
# Compute Smith set
smiths = smith_set(df)
if len(smiths) > 1 and try_resolve_irv:
irv_ballots = df.select(smiths)
smiths = irv_set(irv_ballots)
if show_ballots and pretty:
preview = Table(title="Ballot Box")
for col in df.columns:
preview.add_column(col)
for row in df.head(5).iter_rows():
preview.add_row(*map(str, row))
console.print(preview)
console.print()
elif show_ballots and not pretty:
buf = io.StringIO()
df.write_csv(buf)
console.print(buf.getvalue())
console.print("---")
if pretty:
console.print(
Panel.fit(
"\n".join(f"{c}" for c in smiths),
title="Resulting IRV-resolved Smith Set"
if (try_resolve_irv)
else "Resulting Smith Set",
border_style="green",
)
)
else:
console.print(", ".join(f"{c}" for c in smiths))
except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}")
raise SystemExit(1)
if __name__ == "__main__":
cli()

18
src/smithygui.py Normal file
View file

@ -0,0 +1,18 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "click>=8.4.1",
# "polars>=1.41.0",
# "pyside6>=6.11.1",
# "rich>=15.0.0",
# "rustworkx>=0.17.1",
# ]
# ///
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
window = QWidget()
window.show()
app.exec()

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

301
uv.lock generated
View file

@ -170,14 +170,14 @@ wheels = [
[[package]]
name = "click"
version = "8.4.0"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
]
[[package]]
@ -240,15 +240,15 @@ wheels = [
[[package]]
name = "genai-prices"
version = "0.0.61"
version = "0.0.62"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "httpx2" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/65/71/0c76010eec75f4b3623d521044785c0977c14adabe1cac72b004349567fb/genai_prices-0.0.61.tar.gz", hash = "sha256:4b3bcfd49f174c05831b09f9ee36557d3648569e2f594af6c24b72031b3f0e52", size = 67806, upload-time = "2026-05-19T17:01:36.902Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/8e/ed322d1f22b57fd455749bdbe2f285d310e1c1ebe921cb3d5c0b920de648/genai_prices-0.0.62.tar.gz", hash = "sha256:baf1ffa64be0d15577878216464d6a2d04244db5fbdf78d56bde43809e7aef44", size = 67611, upload-time = "2026-05-25T18:47:16.306Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/ec/b08dc2e834ca00fd8dfedcb17ae2e920667adaad617b45e32b7a3b146f24/genai_prices-0.0.61-py3-none-any.whl", hash = "sha256:d77142f61c13e69909ac19c8e44fd315fd65f3afd714e8d55e914fab0eaf47a2", size = 70853, upload-time = "2026-05-19T17:01:37.858Z" },
{ url = "https://files.pythonhosted.org/packages/81/35/ce64112dcc6f406b3e290dcf57a97acfa2b7d3d0391979219cb9d4a9db6d/genai_prices-0.0.62-py3-none-any.whl", hash = "sha256:5d9ab0d9e5d81e035f88bf591fb6a8dde527922786acf1ee2737358f7bbe0167", size = 70333, upload-time = "2026-05-25T18:47:17.642Z" },
]
[[package]]
@ -282,6 +282,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpcore2"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/7e/8ab39aab1d392845b6512009a9be57d24a5bd4ec7a22d02e513d0645e7a8/httpcore2-2.2.0.tar.gz", hash = "sha256:10e0e142f1ecc1c1cb2a9ebbce82e57f16169f61d163ea336abf36799e89294b", size = 63533, upload-time = "2026-05-17T05:29:55.836Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/22/64de17e7956e8c002f7558ed667d924c2a288344aeff4bd8ff5dc5fdb70b/httpcore2-2.2.0-py3-none-any.whl", hash = "sha256:ce859f268bf8d34fa2d7753e09e4dd5194f557e1b3038439b68a89b2999572fa", size = 79288, upload-time = "2026-05-17T05:29:52.56Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
@ -298,12 +311,27 @@ wheels = [
]
[[package]]
name = "idna"
version = "3.15"
name = "httpx2"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore2" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/aa/c3119de1aa7ad870a01aaddbf3bc3445ed9a681c31d45e3838fd8b7bc155/httpx2-2.2.0.tar.gz", hash = "sha256:f3428d59b1752b8f5629826277262fb4d65e3a683f48af8a5b16c4d012e0b801", size = 80477, upload-time = "2026-05-17T05:29:57.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
{ url = "https://files.pythonhosted.org/packages/be/e0/e0a52596c14194e428c20de4903f4abec38c0dfb5364d20f1d4a2b6266ef/httpx2-2.2.0-py3-none-any.whl", hash = "sha256:12347ebd2daeaefd50b529359778fff767082a09c5826752c963e71269722ff0", size = 74083, upload-time = "2026-05-17T05:29:54.543Z" },
]
[[package]]
name = "idna"
version = "3.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
]
[[package]]
@ -517,7 +545,7 @@ wheels = [
[[package]]
name = "marimo"
version = "0.23.6"
version = "0.23.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@ -532,6 +560,7 @@ dependencies = [
{ name = "psutil" },
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "python-multipart" },
{ name = "pyyaml" },
{ name = "pyzmq", marker = "python_full_version < '3.15'" },
{ name = "starlette" },
@ -539,9 +568,9 @@ dependencies = [
{ name = "uvicorn" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/96/7f03859bfc88b9ee25f1562ad1e7e0284c442f2956eaf0f9c849836d378f/marimo-0.23.6.tar.gz", hash = "sha256:d63aeeee1e9ea7cac79bf2530daba915199153dce4d156fade7546474679d3ca", size = 38426356, upload-time = "2026-05-11T21:39:57.588Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/fe/a1dec6111a660ee3bd26521c24788e7aef519a31c60cf37f767f29313454/marimo-0.23.8.tar.gz", hash = "sha256:8049df4ad263e7126e959d7d910b014e6181dffe49f540a89c3174e61a446a99", size = 38505767, upload-time = "2026-05-22T16:24:19.881Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/a7/c19a6481dfe7762eb07a5bd0a7d7ae560a2ab5505a519bce159d0fd08beb/marimo-0.23.6-py3-none-any.whl", hash = "sha256:e8d19d875b0212600faa80eaaed0fd3f34dfb7b5241e630cf2b1cedd8dd14509", size = 38849553, upload-time = "2026-05-11T21:39:53.996Z" },
{ url = "https://files.pythonhosted.org/packages/2d/76/51b57a2e521b9110f25ec935f2774412e2f3d678b3fdea7841987244fb2c/marimo-0.23.8-py3-none-any.whl", hash = "sha256:99a4035d035fb320c8f2dcefc2213e0d64e9de13e989bc3f2a973b19dc40542a", size = 38938839, upload-time = "2026-05-22T16:24:16.102Z" },
]
[package.optional-dependencies]
@ -704,9 +733,59 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
name = "numpy"
version = "2.4.6"
source = { registry = "https://pypi.org/simple" }
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" }
wheels = [
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
{ 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" },
]
[[package]]
name = "openai"
version = "2.37.0"
version = "2.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -718,21 +797,21 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/50/5901f01ef14e6c27788beb91e54fef5d6204fb5fb9e97402fc8a14de2e32/openai-2.37.0.tar.gz", hash = "sha256:f4bc562cc5f3a43d40d678105572d9d44765f6e0f50c125f63055419b72f4bd9", size = 754706, upload-time = "2026-05-15T22:30:35.428Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/4c/bce61680d0699a78a405fd9a67989b175ba020590428831aab2ab1d2be7c/openai-2.37.0-py3-none-any.whl", hash = "sha256:814633888b8f3b1ffd6615697c6e4ef93632d08b7c2e28c8c5ef3556e5a10107", size = 1303238, upload-time = "2026-05-15T22:30:32.767Z" },
{ url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.42.0"
version = "1.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/ca/25288069c399be6769159d9fb7b1190b603537d82aad2fa2746a0cc2c8c6/opentelemetry_api-1.42.0.tar.gz", hash = "sha256:ea84c893ad177791d138e0349d6ceebd8d3bf006440900400ce220008dafc372", size = 72300, upload-time = "2026-05-19T09:46:29.885Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/0b/be5daf659b82b525338fde371dfcfab09b606a19bb5620c37076964710ec/opentelemetry_api-1.42.0-py3-none-any.whl", hash = "sha256:558d88f88192a973579910ef6f2c13db47a268d5ec2e53e83e50e74a39a02922", size = 61310, upload-time = "2026-05-19T09:46:06.561Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
]
[[package]]
@ -782,14 +861,14 @@ wheels = [
[[package]]
name = "polars"
version = "1.40.1"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "polars-runtime-32" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/8c/bc9bc948058348ed43117cecc3007cd608f395915dae8a00974579a5dab1/polars-1.40.1.tar.gz", hash = "sha256:ab2694134b137596b5a59bfd7b4c54ebbc9b59f9403127f18e32d363777552e8", size = 733574, upload-time = "2026-04-22T19:15:55.507Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/13/fe30b3e2f9ab54a27d82af04fb2edc51c7342cbaa88815e175769a9f5901/polars-1.41.0.tar.gz", hash = "sha256:7cb5465eb66eb868fde779bf5c41c9f2f244481d72c52133e8ed10ba64372e4f", size = 737530, upload-time = "2026-05-22T20:20:56.209Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/91/74fc60d94488685a92ac9d49d7ec55f3e91fe9b77942a6235a5fa7f249c3/polars-1.40.1-py3-none-any.whl", hash = "sha256:c0f861219d1319cdea45c4ce4d30355a47176b8f98dcedf95ea8269f131b8abd", size = 828723, upload-time = "2026-04-22T19:14:25.452Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c8/5807714256c5f3de08593113df17f14f99417a451cb2d91530ad94785003/polars-1.41.0-py3-none-any.whl", hash = "sha256:35dcd24de88a198dc50929924f064ba12a0a0a4a3e77e116689491b4b3ab58ac", size = 832953, upload-time = "2026-05-22T20:19:30.958Z" },
]
[package.optional-dependencies]
@ -799,18 +878,18 @@ pyarrow = [
[[package]]
name = "polars-runtime-32"
version = "1.40.1"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ba/26d40f039be9f552b5fd7365a621bdfc0f8e912ef77094ae4693491b0bae/polars_runtime_32-1.40.1.tar.gz", hash = "sha256:37f3065615d1bf90d03b5326222df4c5c1f8a5d33e50470aa588e3465e6eb814", size = 2935843, upload-time = "2026-04-22T19:15:57.26Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/8f/30dc715ea1135b4b80397edf33fe7b1bb124850e96e38d9918e2b3d6d0b0/polars_runtime_32-1.41.0.tar.gz", hash = "sha256:37ffbe5414f14bf43bcc8e08a0386c97c692e3fd4e87af74529d7f14b1b2d1cb", size = 2985826, upload-time = "2026-05-22T20:20:57.622Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/46/22c8af5eed68ac2eeb556e0fa3ca8a7b798e984ceff4450888f3b5ac61fd/polars_runtime_32-1.40.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b748ef652270cc49e9e69f99a035e0eb4d5f856d42bcd6ac4d9d80a40142aa1e", size = 52098755, upload-time = "2026-04-22T19:14:28.555Z" },
{ url = "https://files.pythonhosted.org/packages/c6/3e/48599a38009ca60ff82a6f38c8a621ce3c0286aa7397c7d79e741bd9060e/polars_runtime_32-1.40.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d249b3743e05986060cec0a7aaa542d020df6c6b876e556023a310efd581f9be", size = 46367542, upload-time = "2026-04-22T19:14:32.433Z" },
{ url = "https://files.pythonhosted.org/packages/43/e9/384bc069367a1a36ee31c13782c178dbd039b2b873b772d4a0fc23a2373d/polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5987b30e7aa1059d069498496e8dda35afd592b0ac3d46ed87e3ff8df1ad652c", size = 50252104, upload-time = "2026-04-22T19:14:35.945Z" },
{ url = "https://files.pythonhosted.org/packages/15/ef/7d57ceb0651af74194e97ed6583e148d352f03d696090221b8059cdfc90b/polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d7f42a8b3f16fc66002cc0f6516f7dd7653396886ae0ed362ab95c0b3408b59", size = 56250788, upload-time = "2026-04-22T19:14:39.743Z" },
{ url = "https://files.pythonhosted.org/packages/10/0f/e4b3ffc748827a14a474ec9c42e45c066050e440fec57e914091d9adda75/polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5f7becc237a7ec9d9a10878dc8e54b73bbf4e2d94a2991c37d7a0b38590d8f9", size = 50432590, upload-time = "2026-04-22T19:14:43.388Z" },
{ url = "https://files.pythonhosted.org/packages/d9/0b/b8d95fbed869fa4caabe9c400e4210374913b376e925e96fdcfa9be6416b/polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:992d14cf191dde043d36fbdbc98a65e43fbc7e9a5024cecd45f838ac4988c1ee", size = 54155564, upload-time = "2026-04-22T19:14:47.239Z" },
{ url = "https://files.pythonhosted.org/packages/06/d9/d091d8fb5cbed5e9536adfed955c4c89987a4cc3b8e73ae4532402b91c74/polars_runtime_32-1.40.1-cp310-abi3-win_amd64.whl", hash = "sha256:f78bb2abd00101cbb23cc0cb068f7e36e081057a15d2ec2dde3dda280709f030", size = 51829755, upload-time = "2026-04-22T19:14:50.85Z" },
{ url = "https://files.pythonhosted.org/packages/65/ad/b33c3022a394f3eb55c3310597cec615412a8a33880055eee191d154a628/polars_runtime_32-1.40.1-cp310-abi3-win_arm64.whl", hash = "sha256:b5cbfaf6b085b420b4bfcbe24e8f665076d1cccfdb80c0484c02a023ce205537", size = 45822104, upload-time = "2026-04-22T19:14:54.192Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e7/9d1630d666eca6a67e2096c0ed2c0e18f1355fe440043fd0830de1b71ab6/polars_runtime_32-1.41.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:766b60c74550382731b604ed62a385a8403b341bf18282d3fd2f746fa3c4cafd", size = 52163350, upload-time = "2026-05-22T20:19:34.431Z" },
{ url = "https://files.pythonhosted.org/packages/11/b3/01538d51cd2790729ae13c23db44bc787bdfe20867faeb1087afc390c53b/polars_runtime_32-1.41.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bee0d294daca79cedd5749e1bf3373c2d4107eb849fe544a60df6c08abc972ce", size = 46474331, upload-time = "2026-05-22T20:19:37.936Z" },
{ url = "https://files.pythonhosted.org/packages/79/29/efa82e1b3e6711f254df3793f3d3fd99f26ef1bcaffa6533266fa6522de4/polars_runtime_32-1.41.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08b3f915468bf00d327b4a1236935a4ec3174dcb163785fcd98185ad1319a503", size = 50358997, upload-time = "2026-05-22T20:19:42.209Z" },
{ url = "https://files.pythonhosted.org/packages/53/18/5a04b06b773047cbf43912ab802eef3c1d50ef7e66d51a41b16726f9bc62/polars_runtime_32-1.41.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72269f768c57229190dba0cb5abb8a1b228e96cc6331273a77a7957576885bd", size = 56332032, upload-time = "2026-05-22T20:19:45.928Z" },
{ url = "https://files.pythonhosted.org/packages/2b/d5/d728ce7a39ea925555db7d2c9f7b5df3ca17568e483db6501783f653e0b9/polars_runtime_32-1.41.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1532c7560a5c0fd06943080ee42aade721f186becdfbb1baafa622e3199c3b62", size = 50529017, upload-time = "2026-05-22T20:19:49.489Z" },
{ url = "https://files.pythonhosted.org/packages/0b/92/6b2092dfed4278f636499217767204fce19ed72695690e2c4eba99e2892e/polars_runtime_32-1.41.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:43368d8a754ee274f1e1099a7c09aae59cd69d22b8a6f83c0f782a00cd3a6662", size = 54244707, upload-time = "2026-05-22T20:19:52.852Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/46b43be0f5becbef65843f438de3950ac6f8d0fa0008d7de0025eed00097/polars_runtime_32-1.41.0-cp310-abi3-win_amd64.whl", hash = "sha256:a9bd6095ecadc6799d166b9e8f7183a7ca8ba0a5aef8a426ec41df8ed8b09df7", size = 51918379, upload-time = "2026-05-22T20:19:55.832Z" },
{ url = "https://files.pythonhosted.org/packages/a3/da/30f15f0c3959b70e7a6583eccd37140a30b5c643ca374792d150b3a357df/polars_runtime_32-1.41.0-cp310-abi3-win_arm64.whl", hash = "sha256:ed922400f0eb393345fd7b6874b150eb943af2b816297a3dde03735cb5f3de08", size = 45921961, upload-time = "2026-05-22T20:19:59.525Z" },
]
[[package]]
@ -903,11 +982,11 @@ wheels = [
[[package]]
name = "pyclean"
version = "3.6.0"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0d/7690e519b66101c41c114e2ddbf144f9b4ff6ea938fb199c09ed958f3970/pyclean-3.6.0.tar.gz", hash = "sha256:a46c179be187999d50d2d3362b2c94856e60f77de32ca7ec2db4eeb16bc39199", size = 27000, upload-time = "2026-03-30T17:34:30.509Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8b/cc/1039d7423a4fe961a36c41c985cf7c1a84270de922d4cc82e16494c7a8b5/pyclean-3.7.0.tar.gz", hash = "sha256:f4605562684fe721d79cfb92842d9d993d1aa7e9a1d13f316e45b29d02d9946b", size = 27580, upload-time = "2026-05-23T01:44:06.561Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/2a/1797004bb000db555bd45e38550b3ca10699f87e22a78e6753882e0ac2cf/pyclean-3.6.0-py3-none-any.whl", hash = "sha256:3e5f9c83f2472df5bdafa78ed533bd38690756e0a7b3b6f11c8b29c9affc728b", size = 27750, upload-time = "2026-03-30T17:34:29.034Z" },
{ url = "https://files.pythonhosted.org/packages/6d/5d/38264b704ed3dd35c765dda435118cf9a249048bd638fbd46d3fd51a9146/pyclean-3.7.0-py3-none-any.whl", hash = "sha256:76c1688c1a976889d526c1a2623107f4bc746ce26538f184f041422d91653858", size = 28374, upload-time = "2026-05-23T01:44:05.483Z" },
]
[[package]]
@ -936,7 +1015,7 @@ wheels = [
[[package]]
name = "pydantic-ai-slim"
version = "1.99.0"
version = "1.102.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "genai-prices" },
@ -947,9 +1026,9 @@ dependencies = [
{ name = "pydantic-graph" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/c6/ed4999450eb2d5106201eb378e5d1763f8af7bec445481bc14f4b1635ef0/pydantic_ai_slim-1.99.0.tar.gz", hash = "sha256:51435f81620d9bc7c2e0a124c19452db730660445810422669b2ba6183bce68b", size = 721667, upload-time = "2026-05-20T01:32:26.66Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e2/3e/14980440e8f0532535e1fbe936fec5f8d8e7bc6cafa81f6f3c51b1884fe5/pydantic_ai_slim-1.102.0.tar.gz", hash = "sha256:0b8f2b70fa2b40efcbd09d341a346934fc4e46622ae281f858c6bfd3d0d3152b", size = 739988, upload-time = "2026-05-23T01:14:32.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/9b/d1860e08f11600d35646ffce888a92d779803ac44f5bbe7229710efb0f95/pydantic_ai_slim-1.99.0-py3-none-any.whl", hash = "sha256:120964b74089b65088dc7ad5377bddaecfef3ce4da302d888fa668f2677d5cd7", size = 895702, upload-time = "2026-05-20T01:32:17.583Z" },
{ url = "https://files.pythonhosted.org/packages/b4/2e/089df86adaf904dd97a1b139d29fe728af0e41430d747f5b6315df3b0c1e/pydantic_ai_slim-1.102.0-py3-none-any.whl", hash = "sha256:f9fa9c3fb58a76f85522f78d1037d201b424de46d532263ed780b3730060449f", size = 919311, upload-time = "2026-05-23T01:14:23.464Z" },
]
[package.optional-dependencies]
@ -1016,7 +1095,7 @@ wheels = [
[[package]]
name = "pydantic-graph"
version = "1.99.0"
version = "1.102.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@ -1024,9 +1103,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/3d/b8481e0326a261a5c1bc1aa49d3218361a06f97bb133ddc891e90f2b6eb5/pydantic_graph-1.99.0.tar.gz", hash = "sha256:b9d7d56bd4fab1fc0bc881ae77d6c9be321cee85b428e7da7fd3ade0bc8010fe", size = 62552, upload-time = "2026-05-20T01:32:29.321Z" }
sdist = { url = "https://files.pythonhosted.org/packages/51/37/4265a1a63eddf35a5aa621c9b2355525bdeae3eb59c3954b165fbfe31404/pydantic_graph-1.102.0.tar.gz", hash = "sha256:e285bd7115e4e92676eaf0a5e7e6faa64cda8c4819f67923a118c50666b909ab", size = 62584, upload-time = "2026-05-23T01:14:36.056Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/2e/49d51eba4698d6fe41c48fabf7f7f6bd3e3a4f3a0d29487e5ed742aba6cf/pydantic_graph-1.99.0-py3-none-any.whl", hash = "sha256:d40baf3effbe1610017234b09f873cfe956979fb4ec92e57d2705345e2943b33", size = 80092, upload-time = "2026-05-20T01:32:21.356Z" },
{ url = "https://files.pythonhosted.org/packages/a4/49/5597c52d50114440047dd4ce4f6505e32ee336f43267639907d1a17648ee/pydantic_graph-1.102.0-py3-none-any.whl", hash = "sha256:b1a28314adc4abca4db02cf095d064782ec5712e0847ce7a6b79a3c84bf1fc01", size = 80100, upload-time = "2026-05-23T01:14:27.583Z" },
]
[[package]]
@ -1178,6 +1257,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.29"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
@ -1447,27 +1535,49 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.13"
version = "0.15.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" },
{ url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" },
{ url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" },
{ url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" },
{ url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" },
{ url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" },
{ url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" },
{ url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" },
{ url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" },
{ url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" },
{ url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" },
{ url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" },
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
]
[[package]]
name = "rustworkx"
version = "0.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/b0/66d96f02120f79eeed86b5c5be04029b6821155f31ed4907a4e9f1460671/rustworkx-0.17.1.tar.gz", hash = "sha256:59ea01b4e603daffa4e8827316c1641eef18ae9032f0b1b14aa0181687e3108e", size = 399407, upload-time = "2025-09-15T16:29:46.429Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/24/8972ed631fa05fdec05a7bb7f1fc0f8e78ee761ab37e8a93d1ed396ba060/rustworkx-0.17.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c08fb8db041db052da404839b064ebfb47dcce04ba9a3e2eb79d0c65ab011da4", size = 2257491, upload-time = "2025-08-13T01:43:31.466Z" },
{ url = "https://files.pythonhosted.org/packages/23/ae/7b6bbae5e0487ee42072dc6a46edf5db9731a0701ed648db22121fb7490c/rustworkx-0.17.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:4ef8e327dadf6500edd76fedb83f6d888b9266c58bcdbffd5a40c33835c9dd26", size = 2040175, upload-time = "2025-08-13T01:43:33.762Z" },
{ url = "https://files.pythonhosted.org/packages/cd/ea/c17fb9428c8f0dcc605596f9561627a5b9ef629d356204ee5088cfcf52c6/rustworkx-0.17.1-cp39-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b809e0aa2927c68574b196f993233e269980918101b0dd235289c4f3ddb2115", size = 2324771, upload-time = "2025-08-13T01:43:35.553Z" },
{ url = "https://files.pythonhosted.org/packages/d7/40/ec8b3b8b0f8c0b768690c454b8dcc2781b4f2c767f9f1215539c7909e35b/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e82c46a92fb0fd478b7372e15ca524c287485fdecaed37b8bb68f4df2720f2", size = 2068584, upload-time = "2025-08-13T01:43:37.261Z" },
{ url = "https://files.pythonhosted.org/packages/d9/22/713b900d320d06ce8677e71bba0ec5df0037f1d83270bff5db3b271c10d7/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42170075d8a7319e89ff63062c2f1d1116ced37b6f044f3bf36d10b60a107aa4", size = 2380949, upload-time = "2025-08-13T01:52:17.435Z" },
{ url = "https://files.pythonhosted.org/packages/20/4b/54be84b3b41a19caf0718a2b6bb280dde98c8626c809c969f16aad17458f/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65cba97fa95470239e2d65eb4db1613f78e4396af9f790ff771b0e5476bfd887", size = 2562069, upload-time = "2025-08-13T02:09:27.222Z" },
{ url = "https://files.pythonhosted.org/packages/39/5b/281bb21d091ab4e36cf377088366d55d0875fa2347b3189c580ec62b44c7/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246cc252053f89e36209535b9c58755960197e6ae08d48d3973760141c62ac95", size = 2221186, upload-time = "2025-08-13T01:43:38.598Z" },
{ url = "https://files.pythonhosted.org/packages/cc/2d/30a941a21b81e9db50c4c3ef8a64c5ee1c8eea3a90506ca0326ce39d021f/rustworkx-0.17.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c10d25e9f0e87d6a273d1ea390b636b4fb3fede2094bf0cb3fe565d696a91b48", size = 2123510, upload-time = "2025-08-13T01:43:40.288Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ef/c9199e4b6336ee5a9f1979c11b5779c5cf9ab6f8386e0b9a96c8ffba7009/rustworkx-0.17.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:48784a673cf8d04f3cd246fa6b53fd1ccc4d83304503463bd561c153517bccc1", size = 2302783, upload-time = "2025-08-13T01:43:42.073Z" },
{ url = "https://files.pythonhosted.org/packages/30/3d/a49ab633e99fca4ccbb9c9f4bd41904186c175ebc25c530435529f71c480/rustworkx-0.17.1-cp39-abi3-win32.whl", hash = "sha256:5dbc567833ff0a8ad4580a4fe4bde92c186d36b4c45fca755fb1792e4fafe9b5", size = 1931541, upload-time = "2025-08-13T01:43:43.415Z" },
{ url = "https://files.pythonhosted.org/packages/a9/ec/cee878c1879b91ab8dc7d564535d011307839a2fea79d2a650413edf53be/rustworkx-0.17.1-cp39-abi3-win_amd64.whl", hash = "sha256:d0a48fb62adabd549f9f02927c3a159b51bf654c7388a12fc16d45452d5703ea", size = 2055049, upload-time = "2025-08-13T01:43:44.926Z" },
]
[[package]]
@ -1481,11 +1591,17 @@ wheels = [
[[package]]
name = "smithy"
version = "0.1.0"
version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "numpy" },
{ name = "polars" },
{ name = "rustworkx" },
]
[package.optional-dependencies]
cli = [
{ name = "click" },
{ name = "rich" },
]
@ -1505,10 +1621,13 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.4.0" },
{ name = "click", marker = "extra == 'cli'", specifier = ">=8.4.0" },
{ name = "numpy", specifier = ">=2.4.6" },
{ name = "polars", specifier = ">=1.40.1" },
{ name = "rich", specifier = ">=15.0.0" },
{ name = "rich", marker = "extra == 'cli'", specifier = ">=15.0.0" },
{ name = "rustworkx", specifier = ">=0.17.1" },
]
provides-extras = ["cli"]
[package.metadata.requires-dev]
dev = [
@ -1565,14 +1684,14 @@ wheels = [
[[package]]
name = "starlette"
version = "1.0.0"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
{ url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" },
]
[[package]]
@ -1677,41 +1796,41 @@ wheels = [
[[package]]
name = "uv"
version = "0.11.15"
version = "0.11.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/34/609d5d01ba21dc8f0974610ca7802fbb2c946a0c38665cfe5c5aeddbefb5/uv-0.11.15.tar.gz", hash = "sha256:755f959ec6a2fd8ccb6ee76ad90ab759d2eb1f4797444078645dd1ee4bca92d6", size = 4159545, upload-time = "2026-05-18T19:57:48.133Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/99/025154611a4bd97a23851574c15d73bb71ada09d35f092d6972f9ac87f70/uv-0.11.16.tar.gz", hash = "sha256:4b435fcb0af8f34833dcc1903a8a223856437efd0d515c2160a2871def221238", size = 4177038, upload-time = "2026-05-21T22:10:01.009Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/7c/dcc230c5911884d8848145dabcac8fb95a5ed6f9fe1c57fae8242618f28a/uv-0.11.15-py3-none-linux_armv6l.whl", hash = "sha256:83b04ab49514a0a761ffedb36a748ee81f87746671e72088e5f32c9585e5f1a9", size = 23110183, upload-time = "2026-05-18T19:57:23.051Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f3/efd4e044b60eb9c3c12ee386be098d56c335538ccec7caa49349cfba9344/uv-0.11.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cae61f737be075b90be9e3f07d961072aed7019f4c9b8ed5c5d41c4d6cade3", size = 22637941, upload-time = "2026-05-18T19:57:26.752Z" },
{ url = "https://files.pythonhosted.org/packages/a6/b8/48627f895a1569e576822e0a8416aa4797eb4a4551de21a4ad97b9b5819d/uv-0.11.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9accae33619a9166e5c48531deb455d672cfb89f9357a00975e669c76b0bd49f", size = 21258803, upload-time = "2026-05-18T19:57:05.473Z" },
{ url = "https://files.pythonhosted.org/packages/af/50/4bc8a148274feabee2d9c9f1fa15009e10c0228dfe57981ee3ea2ef1d481/uv-0.11.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c0cf52cd6d50bb9e05e2d968f45f80761107e4cbc8d4a26d9758f9d8274aaec1", size = 23066178, upload-time = "2026-05-18T19:57:33.058Z" },
{ url = "https://files.pythonhosted.org/packages/a9/56/139fc3bec9a8b0a25bfe2196123adb9f16124da437bf4fbcf0d21cfcafb2/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:49dc6ed70bff00937384f96cdc4b1a4742d18e5504ec2c4a1214dba2dee5687a", size = 22705332, upload-time = "2026-05-18T19:57:36.714Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b0/b18b3dd204f8c213236a1ebd148e009861637129a8cce34df0e9aa22ed40/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:adb9a89352539fdd8f7cd5f9966cf9f94fc5b98e0ccdf5003a04123dc6423bec", size = 22707534, upload-time = "2026-05-18T19:58:04.117Z" },
{ url = "https://files.pythonhosted.org/packages/76/36/3ca09f95572df99d361b49c96b1297149e96e120d8d1ecf074095a4b6da4/uv-0.11.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40ff67e3f8e8a7533781a2e892a534975a93acb83ea35460e64e7b2bf2111774", size = 24096607, upload-time = "2026-05-18T19:58:11.625Z" },
{ url = "https://files.pythonhosted.org/packages/64/be/3bdee21a296bbf5336a526e3613d0e7d4538dacc39c62d7fcba55d15f6b0/uv-0.11.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6463a299ed7e6b5a800ed6f108af8e1588352629424133ddef7572b0e1e1118", size = 25082562, upload-time = "2026-05-18T19:57:40.69Z" },
{ url = "https://files.pythonhosted.org/packages/cd/73/f371f3689ffe741066468d001d85f739fc4b5574de83b639ef19b5e8a7f4/uv-0.11.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68c1e62d4b78578b90b833553286b65d6a7e327537716441068583ba652ec4f5", size = 24253391, upload-time = "2026-05-18T19:57:18.47Z" },
{ url = "https://files.pythonhosted.org/packages/d3/16/fe392d618af6b00c064b3e718d585dcf791546a77c5123a5bec07ce53a0a/uv-0.11.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98edf1bdaf82447014852051d93e3ee95012509c567bf057fd117e6bdbd9a807", size = 24415871, upload-time = "2026-05-18T19:58:19.651Z" },
{ url = "https://files.pythonhosted.org/packages/6e/24/2e92a052fb6334fcd746d1c7cb57847c204b118c84f5da53c0f9e129f7b7/uv-0.11.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:be8f76d25bcf4c92bb384240ac1bf9aa7f51063d0bdeca4c9cf0ec3ed8b145e0", size = 23159007, upload-time = "2026-05-18T19:57:10.653Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2e/6923d0658d164bb2c435ed1868aa2d49b3074594679917a001ff92dc95bb/uv-0.11.15-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f9f4fbbf4fe485522054f3c7496c6e8e932d6436e4200ff3daf718db0b7c7bd5", size = 23769385, upload-time = "2026-05-18T19:58:15.856Z" },
{ url = "https://files.pythonhosted.org/packages/a4/99/7e34cd949e57360814e8064cc9fb7104df445d0f6a663504e5f7473480aa/uv-0.11.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0ed920e896b2fd13a35031707e307e42fbb2681458b967440a17272d86d49137", size = 23860973, upload-time = "2026-05-18T19:57:55.575Z" },
{ url = "https://files.pythonhosted.org/packages/28/98/8fe1f5f9d816e94569a0298dd8e0936801097625fa1952162951f0d628b6/uv-0.11.15-py3-none-musllinux_1_1_i686.whl", hash = "sha256:41d907611f3e6a13262807fd7f0a17849f76285ca80f536f6b3943732bdc6656", size = 23431392, upload-time = "2026-05-18T19:57:59.814Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6b/76a1ce2fa860026913a5941700cdc7d715fce9c3277a3fa3489cf2523ca0/uv-0.11.15-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e3b68f8bf1a4568710f77e5bda9182ce7682811d89a8e7468c22460e032b234d", size = 24519478, upload-time = "2026-05-18T19:57:51.165Z" },
{ url = "https://files.pythonhosted.org/packages/43/60/1d58e8a05718cb50494763115710b73846cacb651fd735d285233fd72c59/uv-0.11.15-py3-none-win32.whl", hash = "sha256:8e2da3076761086a5b76869c3f38ef0509c836046ef41ddd19485dfd7271dca9", size = 22020178, upload-time = "2026-05-18T19:58:07.64Z" },
{ url = "https://files.pythonhosted.org/packages/55/53/40fcefcb348af660488597ed3c01363df7344e60611f8883750dc596f5c6/uv-0.11.15-py3-none-win_amd64.whl", hash = "sha256:cc3915ab291a1ecaf31de05f5d8bd70d09c66fe9911a53f70d9efa62ff0dbd8a", size = 24668779, upload-time = "2026-05-18T19:57:44.894Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7d/fa3a9960c95af9bbe2a629048760d0b9b4fead8ccd4f2235af747ec7cdf0/uv-0.11.15-py3-none-win_arm64.whl", hash = "sha256:4f39426a13dee24897aed60c4b98058c66f18bd983885ac5f4a54a04b24fbddf", size = 23198178, upload-time = "2026-05-18T19:57:14.68Z" },
{ url = "https://files.pythonhosted.org/packages/55/e3/8b8cfc802bc476c67e31a39725538193265cf3a19585b4a60c232659f919/uv-0.11.16-py3-none-linux_armv6l.whl", hash = "sha256:c9e9d9cb73ee8cd2ad696dbf1bc3232abaac363270557684b6b85a2bdb8eb276", size = 23508087, upload-time = "2026-05-21T22:10:06.227Z" },
{ url = "https://files.pythonhosted.org/packages/45/78/d5ca91c636ac88e902b6b3ff31ad32d2d02663232d844aff871467a323d2/uv-0.11.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:01172238a75e42a5a55d12555cd9ec98bee24249f3645b98a4b32eb5f1ff5e43", size = 23028989, upload-time = "2026-05-21T22:09:50.127Z" },
{ url = "https://files.pythonhosted.org/packages/c7/26/c84580dfec5a87c36fb1218eac17c5194fa3e58e2a9232cf085d69eb6bed/uv-0.11.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75f9b5bac49b97131973910c220feac60fe47b10a333941b237ff0ae4b36721", size = 21572023, upload-time = "2026-05-21T22:09:58.703Z" },
{ url = "https://files.pythonhosted.org/packages/84/68/ba2bdc64fea96ef8c9796a991f244541b65bb9d31c661b322cc724857a4e/uv-0.11.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a801484f4507b6c2133e557350f3143b61b8f8b61dddb01ff7b84a74cdfab1fb", size = 23289936, upload-time = "2026-05-21T22:10:15.423Z" },
{ url = "https://files.pythonhosted.org/packages/c9/81/74922f693d5804a77d009338ca8dc709eff871fb60d9f2c263dede8d77d1/uv-0.11.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:eb538069e768b042cf870be700a210518ce628e36d99d9a83b85acaf484d7f6a", size = 23020906, upload-time = "2026-05-21T22:10:24.242Z" },
{ url = "https://files.pythonhosted.org/packages/60/81/cda8886f5df4dd28854a9b97bcc3ee6a7d1b5b5b23aaaccfbf1ed3e5e2bf/uv-0.11.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7cdb23457a4d1bc76bf1016638ea1d1ada0e8e032f656168e933d4d17c47e72", size = 23004220, upload-time = "2026-05-21T22:10:32.847Z" },
{ url = "https://files.pythonhosted.org/packages/98/7c/65837e07de23f0a40ab860bc6601f7c022d4bcf4b97ca79b6c35a2e72e65/uv-0.11.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:451327388d59ac3041cbda474296f3ceeafac5b1f645476198e7b95f504fcfd5", size = 24319651, upload-time = "2026-05-21T22:10:21.492Z" },
{ url = "https://files.pythonhosted.org/packages/85/70/9d364542bf118433b60ed71422e47d2c8c470aca7d3aef0df9449a5f726a/uv-0.11.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7992b8276149b3ffaf35ce9434702d3e16bae6ec393e99df209b870a7e19eb0", size = 25359517, upload-time = "2026-05-21T22:09:46.519Z" },
{ url = "https://files.pythonhosted.org/packages/99/b4/650896e8cff5a3289cee860c41fd9876da83ca628c5871f9a61d5fc75c72/uv-0.11.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a8db9b3314d900e7a240105afce43f806c9e04c59ea10a40bdbdca84c6d0c5", size = 24563421, upload-time = "2026-05-21T22:10:35.82Z" },
{ url = "https://files.pythonhosted.org/packages/b1/7d/184711a8c02466e1486d57efdc9394ce09cbf43ee2c5794da70bd25db3fb/uv-0.11.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b10086165189c39c53142a0e2f34e0b8889ef681886f589ed17be45a1a774c7", size = 24676607, upload-time = "2026-05-21T22:10:39.784Z" },
{ url = "https://files.pythonhosted.org/packages/ee/3f/5b338df6505f77f73c20eae38cb29f57d14dba56dac835386e3dc6e2a5d6/uv-0.11.16-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cfe1f06fb8f135a735a961065d5ee90f99cccf41749fb1f964edb5b3c3dae19b", size = 23401615, upload-time = "2026-05-21T22:10:30.124Z" },
{ url = "https://files.pythonhosted.org/packages/b6/f9/54bbcbc77443dc76468f09a49cc9f4f92ca49b4159a011c6010d223de4ea/uv-0.11.16-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:2454f80d8b548fb2e246151578809b14ad4395b3f357d738bae1af11918e91af", size = 24104468, upload-time = "2026-05-21T22:09:53.323Z" },
{ url = "https://files.pythonhosted.org/packages/3e/0a/b5f105514fddea5110fe3947cd18a9f199ff93dbad78e5e5a08e1b5d0ea2/uv-0.11.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:4249d57a563165d368050680deeb722f9c0053a0dbf3244b11cca3e6d85a3c7d", size = 24164861, upload-time = "2026-05-21T22:10:09.458Z" },
{ url = "https://files.pythonhosted.org/packages/f6/01/15d4ca2be7257862b077a9077ac31ce81c419f35ef7994e76356a317716b/uv-0.11.16-py3-none-musllinux_1_1_i686.whl", hash = "sha256:374c30126483ce95675c5de49e54c2454ddedb01c17b8321417fe4eb9da83406", size = 23644919, upload-time = "2026-05-21T22:10:03.129Z" },
{ url = "https://files.pythonhosted.org/packages/49/bf/9de3e262e6ff93aec2e0a4c238857293fd2c616dd79f25bb440f126bf32c/uv-0.11.16-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:746edfc9d1d8cd03dd58739989f634d3580648048d09f81a9c68da74c4eb9d62", size = 24973746, upload-time = "2026-05-21T22:10:18.413Z" },
{ url = "https://files.pythonhosted.org/packages/f6/7d/f4126dce104f1b5d0b451ce3ca41c4db69b963c2e78c3465fcda6440de31/uv-0.11.16-py3-none-win32.whl", hash = "sha256:50299b20aab2d28c05ff27d781ce2af3f5af2102bc304dc07a4ad54b05e2af8a", size = 22400991, upload-time = "2026-05-21T22:10:27.119Z" },
{ url = "https://files.pythonhosted.org/packages/8f/38/99627cb995a03389b227ce4b12b08e770565d0aa7850cd0420973194a638/uv-0.11.16-py3-none-win_amd64.whl", hash = "sha256:e901aafa5007beffafe57bfa44e5e248d99fb5d97036a3718fd65cf9723c5cd3", size = 25067163, upload-time = "2026-05-21T22:10:12.317Z" },
{ url = "https://files.pythonhosted.org/packages/b6/68/3ed1c0bdfb4bec501e5cde73419b4f39c8a125ef905a85fc0f239f19eb9b/uv-0.11.16-py3-none-win_arm64.whl", hash = "sha256:d777cb29661cdfa7f90dae77406c85fb5b729bf8bc13941dc237958a1ea1ba00", size = 23502015, upload-time = "2026-05-21T22:09:56.014Z" },
]
[[package]]
name = "uvicorn"
version = "0.47.0"
version = "0.48.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" },
{ url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
]
[[package]]