init cgi script and html form

This commit is contained in:
Thomas (Tom) C. Gorordo 2025-04-25 17:14:46 -07:00
parent 740ae98027
commit 5da4a21a3f
Signed by: tgorordo
GPG key ID: 0CBED22BB0D94490
9 changed files with 909 additions and 110 deletions

View file

@ -14,36 +14,30 @@ bibliography: REFERENCES.bib
### Gale-Shapley Deferred Acceptance
The most basic versions of the stable matching problem was outlined and solved by [@gale&shapley1962].
The most basic version(s) of the stable matching problem was outlined and solved in [@gale&shapley1962] and won Shapley the
[2012 Nobel Prize in Economics](https://www.nobelprize.org/prizes/economic-sciences/2012/popular-information/).
The basic problem being solved is as follows:
#### Stable Marriage Problem
The "Stable Marriage problem" TODO
TODO
## Usage
Using `carousel` is pretty simple once it's set up: given some input rankings, and some post-selection criteria
the program should generate a landscape of valid matching solutions for you to choose from (and can generate more on request).
### Installation, Setup, Dependencies & Tooling
There are a number of ways to guarantee you have the required dependencies to run `carousel`.
The most complete method is using `uv` (with `nix` and `direnv`), but a plain/more barebones setup using `venv` is also possible.
#### Setup and run with `uv`
`carousel` was developed using the the [`uv`](https://github.com/astral-sh/uv) package and project manager.
#### College Admissions Problem
Solving the stable marriage problem also provides a solution to the "College Admissions Problem",
with just a little more work.
TODO
#### Raw setup with `venv`
It's possible to only use only default Python tooling, if so desired, via the
[`venv` module](https://docs.python.org/3/library/venv.html).
## Data - Input/Output
All [input table formats supported by `polars`](https://docs.pola.rs/user-guide/io/) are supported by `carousel` (`csv`, `excel`, `json` to name a few),
which accepts a few inter-related tabular schemes for the input/output data.
TODO
#### Convenience [`direnv`](https://github.com/direnv/direnv) and [`nix`](https://github.com/NixOS/nix) environment management and [`just`](https://github.com/casey/just) taskrunner.
TODO
### Matching: Input & Output
All [input table formats supported by `polars`](https://docs.pola.rs/user-guide/io/) are supported by `carousel`.
Input data should be in one of three forms:
### Input
Input describes the preferences/rankings of the "applicants" of "reviewers" to which they will be matched (possibly many-to-one, as in the "College Admission Problem"),
as well as the preferences/rankings for the reviwers of applicants.
Input should be in one of three forms:
#### Preferences
Preferences enumerate by-name some preferences in descending order,
@ -78,6 +72,8 @@ a pair of corresponding rankings, *or* a matrix encoding both rankings at once:
| CaseMed | (3, 2) | (1, 1) | (2, 3) |
| Emory | (2, 1) | (3, 3) | (1, 2) |
### Output
#### Matching
A matching is a table whose rows list the applicants matched to each reviewer
e.g. a matching from the med-school ranking matrix in the previous section might look like
@ -99,10 +95,52 @@ TODO check/make stable.
TODO matching more people per school e.g.
### Matching: Post-Selection
## Usage
There are 4 main ways to use Carousel:
### UO Pages Server - CGI Form
An HTML form submission interface is hosted at
> [`https://pages.uoregon.edu/tgorordo/forms/carousel.html`](https://pages.uoregon.edu/tgorordo/forms/carousel.html)
using the [pages.uoregon.edu CGI feature](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069).
Submit your applicant and reviewer preferences in tabular form,
or as excel uploads and the server will return a table of matches for you to choose from.
### Command Line Binary
TODO
### GUI Binary
TODO
### Python Library/Development
If you prefer to invoke `carousel` directly (or incorporate it as a library into another script)
in a python environment instead of using any of the bundled/released versions of the program described above (or wish to
reproduce those bundles), you can do so using the [`uv` environment/package/project manager](https://github.com/astral-sh/uv)
or a raw python virtual environment using the [`venv` module](https://docs.python.org/3/library/venv.html)
(if you need an intro to python `venv`s see [this page](https://pages.uoregon.edu/tgorordo/uoph410-510a_Image-Analysis/venvs.html)).
Some extra command-line development conveniences are available if you use the tools:
- [`just`](https://github.com/casey/just) is a taskrunner that can execute the provided `justfile` of some common useful commands.
- [`direnv`](https://github.com/direnv/direnv) with [`nix` (shell)](https://github.com/NixOS/nix) can guarantee minimal development tooling without polluting your broader environment. i.e. they can auto-install and run all of carousel's tooling in an environment specific to your development directory.
but everything provided by these tools can also be done using more standard/default shell tooling.
[`uv`](https://github.com/astral-sh/uv) as your package/environment manager is highly recommended, however.
TODO
## Post-Selection
It's often desirable to enforce additional criteria on solutions
that are not well-posed within the core optimization problem.
Since the solver itself is stochastic, these are often most easily implemented
Since the solver itself is stochastic to some extent, these are often most easily implemented
by a post-selection.

View file

@ -1,11 +1,14 @@
run:
uv run carousel
python *arguments:
uv run python -c "import code; from rich import pretty; pretty.install(); code.interact()" {{arguments}}
check:
uv run pyright src
test:
uv run pytest -vv --tb=short
uv run pytest -vvv --tb=short --log-cli-level=INFO
format:
uv run ruff format src test

View file

@ -10,7 +10,6 @@ dependencies = [
"numpy>=2.2.4",
"polars>=1.26.0",
"pyside6>=6.9.0",
"pytest-benchmark>=5.1.0",
"rich>=14.0.0",
]
@ -28,11 +27,17 @@ dev = [
"pyinstaller>=6.12.0",
"pyright>=1.1.398",
"pytest>=8.3.5",
"pytest-benchmark>=5.1.0",
"ruff>=0.11.2",
]
srv = [
"legacy-cgi>=2.6.3",
"python-dotenv>=1.1.0",
]
[pytest]
testpaths = "test"
log_cli = true
[tool.pyright]
include = ["src"]

View file

@ -1,18 +1,28 @@
import logging, rich
from rich.logging import RichHandler
rich.traceback.install()
import itertools as it
import numpy as np
import polars as pl
import polars.selectors as pls
logging.basicConfig(
level="NOTSET",
level=logging.INFO,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[pl, pls])],
handlers=[
RichHandler(
show_time=True,
markup=True,
rich_tracebacks=True,
tracebacks_suppress=[pl, pls, np],
)
],
)
log = logging.getLogger("rich")
log = logging.getLogger(__name__)
def rank_to_pref(ranking):
@ -64,6 +74,7 @@ def ranking_matrix(A, B):
def check_valid_pref(preferences):
"""A valid set of preferences has all unique entries in each column, TODO"""
repeats = preferences.select(
(~pl.all_horizontal((pl.all().is_unique() | pl.all().is_null()).all())).alias(
"repeats"
@ -73,6 +84,7 @@ def check_valid_pref(preferences):
def check_valid_rank(ranking):
"""A valid ranking has no ties, TODO"""
ties = ranking.select(
(~pl.all_horizontal((pl.all().is_unique() | pl.all().is_null()).all())).alias(
"ties"
@ -91,6 +103,16 @@ def check_valid_assgn(assgn, applicants, reviewers):
pass
def assgn_to_match(assgn):
# TODO
pass
def match_to_assgn(match):
# TODO
pass
def get_rank(ranking, ranker, ranked):
idx = ranking.select(pl.arg_where(pl.col("") == ranked)).item()
return ranking[ranker][idx]
@ -122,22 +144,56 @@ def check_stable(*args, **kwargs):
return not check_unstable(*args, **kwargs)
def deferred_acceptance(applicant_ranking, reviewer_ranking):
def deferred_acceptance(applicant_rankings, reviewer_rankings):
"""Find the Gale-Shapley deferred-acceptance stable matching for preferences A, R."""
applicants = applicant_ranking.columns[1:]
reviewers = reviewer_ranking.columns[1:]
# TODO - the core algorithm!
pass
reviewer_rankings = reviewer_rankings.rename(
{reviewer_rankings.columns[0]: "applicant"}
)
app_prefs = rank_to_pref(applicant_rankings)
offers = app_prefs.transpose(
include_header=True,
header_name="applicant",
column_names=["pref" + str(i + 1) for i in range(app_prefs.width)],
).with_columns(pl.coalesce(pl.all().exclude("applicant")).alias("offer"))
def assgn_to_match(assgn):
# TODO
pass
# offers = pl.concat(pl.align_frames(offers, reviewer_rankings, on="applicant"), how="horizontal")
offers = pl.concat([offers, reviewer_rankings], how="align_left")
match = pl.DataFrame(
{
r: offers.select(pl.col("applicant", "offer").sort_by(r))
.select(
pl.when(pl.col("offer").eq(r)).then(pl.col("applicant")).otherwise(None)
)
.select(pl.all().fill_null(strategy="backward").first())
.to_series()
for r in reviewer_rankings.columns[1:]
}
) # .select(pl.all().fill_null(strategy="backward").first())
def match_to_assgn(match):
# TODO
pass
# while check_unstable(match, applicant_rankings, reviewer_rankings):
while match.select(pl.any_horizontal(pl.all().has_nulls())).item():
# TODO null applicant preferences that rejected
rejected_applicants = offers.select(
pl.col("applicant").is_in(match.row(0)).alias("matched")
)
return match
offers = offers.with_columns(pl.col("pref"))
offers = offers.with_columns(
pl.coalesce(
# TODO: select prefn columns using a regex
).alias("offer")
)
# TODO update match
# else if stable
return match
def main() -> None:

24
src/cgi/carousel.py Normal file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python
import os
from dotenv import load_dotenv
load_dotenv()
logdir = os.getenv("CAROUSEL_LOGDIR")
import cgi, cgitb
cgitb.enable(display=0, logdir=logdir)
form = cgi.FieldStorage()
from ..carousel import *
print("Content-Type: text/html")
print()
print("<title>Carousel Matches!</title>")
print("<h1>Carousel Stable Matches Found:</h1>")
print("TODO - INPUTH ECHO NOT IMPLEMENTED")
print("TODO - OUTPUT NOT IMPLEMENTED")

1
src/cgi/default.env Normal file
View file

@ -0,0 +1 @@
CAROUSEL_LOGDIR="$HOME/logs/carousel/"

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

@ -0,0 +1,652 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Data Entry Form</title>
<style>
body {
font-family: Georgia, serif;
line-height: 1.6;
color: #1a1a1a;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f8f8f8;
}
h1 {
color: #333;
text-align: center;
border-bottom: 1px solid #ddd;
padding-bottom: 15px;
font-weight: normal;
}
h2 {
color: #333;
font-weight: normal;
}
.container {
background: #fff;
border-radius: 3px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.form-section {
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.controls-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.controls {
display: flex;
gap: 10px;
}
.fill-default-control {
display: flex;
align-items: center;
margin-top: 5px;
}
.fill-default-btn {
background: #777;
margin-right: 10px;
}
.fill-default-btn:hover {
background: #555;
}
.disabled-table {
opacity: 0.5;
pointer-events: none;
}
button {
background: #555;
color: white;
border: none;
padding: 8px 15px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
font-family: Georgia, serif;
}
button:hover {
background: #333;
}
button.secondary {
background: #999;
}
button.secondary:hover {
background: #777;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
table {
border-collapse: collapse;
width: 100%;
min-width: 600px;
border: 1px solid #ddd;
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
th {
background-color: #f5f5f5;
position: relative;
}
th input {
width: 100%;
box-sizing: border-box;
padding: 5px;
border: 1px solid #ccc;
border-radius: 2px;
font-family: Georgia, serif;
}
td input {
width: 100%;
box-sizing: border-box;
padding: 5px;
border: 1px solid #ccc;
border-radius: 2px;
font-family: Georgia, serif;
}
.file-upload {
margin-bottom: 20px;
}
.file-upload label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.file-info {
margin-top: 5px;
font-size: 14px;
color: #666;
}
.submit-section {
text-align: center;
border-bottom: none;
}
.submit-button {
background: #444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
font-family: Georgia, serif;
}
.submit-button:hover {
background: #222;
}
.progress-bar {
height: 5px;
background-color: #f1f1f1;
margin-top: 10px;
border-radius: 2px;
overflow: hidden;
display: none;
}
.progress {
height: 100%;
background-color: #666;
width: 0%;
transition: width 0.3s;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 3px;
display: none;
}
.success {
background-color: #f1f5f1;
border: 1px solid #ddd;
color: #333;
}
.error {
background-color: #f5f1f1;
border: 1px solid #ddd;
color: #333;
}
.tabs {
display: flex;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
background: #f0f0f0;
border: 1px solid #ddd;
border-bottom: none;
border-radius: 3px 3px 0 0;
margin-right: 5px;
}
.tab.active {
background: #fff;
border-bottom: 1px solid #fff;
position: relative;
top: 1px;
}
.tab-content {
display: none;
border-top: 1px solid #ddd;
padding-top: 20px;
}
.tab-content.active {
display: block;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 20px;
height: 20px;
border-radius: 50%;
border-left-color: #555;
animation: spin 1s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
(<a href="https://pages.uoregon.edu/tgorordo">Back to ~/tgorordo</a>)
<div class="container">
<h1>Carousel Stable Matcher</h1>
<div class="tabs">
<div class="tab active" id="table-tab">Table Input</div>
<div class="tab" id="file-tab">File Upload</div>
</div>
<form id="data-form" action="/cgi-bin/carousel.py" method="post" enctype="multipart/form-data">
<div class="tab-content active" id="table-content">
<!-- First Table Section -->
<div class="form-section">
<div class="form-header">
<h2>Applicant Preferences</h2>
<div class="controls">
<button type="button" id="add-column-1">Add Column</button>
<button type="button" id="add-row-1">Add Row</button>
<button type="button" class="secondary" id="remove-column-1">Remove Column</button>
<button type="button" class="secondary" id="remove-row-1">Remove Row</button>
</div>
</div>
<div class="table-container">
<table id="data-table-1">
<thead>
<tr id="header-row-1">
<th><input type="text" placeholder="Applicant 1" name="table1_header_0" required></th>
<th><input type="text" placeholder="Applicant 2" name="table1_header_1" required></th>
<th><input type="text" placeholder="Applicant 3" name="table1_header_2" required></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" placeholder="Preference 1..." name="table1_row_0_col_0"></td>
<td><input type="text" placeholder="Preference 1..." name="table1_row_0_col_1"></td>
<td><input type="text" placeholder="Preference 1..." name="table1_row_0_col_2"></td>
</tr>
<tr>
<td><input type="text" name="table1_row_1_col_0"></td>
<td><input type="text" name="table1_row_1_col_1"></td>
<td><input type="text" name="table1_row_1_col_2"></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Second Table Section -->
<div class="form-section">
<div class="form-header">
<h2>Reviewer Preferences</h2>
<div class="controls-container">
<div class="controls">
<button type="button" id="add-column-2">Add Column</button>
<button type="button" id="add-row-2">Add Row</button>
<button type="button" class="secondary" id="remove-column-2">Remove Column</button>
<button type="button" class="secondary" id="remove-row-2">Remove Row</button>
</div>
<div class="fill-default-control">
<label for="fill-default">
<button type="button" id="fill-default-btn" class="fill-default-btn">Fill Default</button>
<input type="checkbox" id="fill-default" name="fill_default"> (Use default uniform preferences.)
</label>
</div>
</div>
</div>
<div class="table-container" id="table2-container">
<table id="data-table-2">
<thead>
<tr id="header-row-2">
<th><input type="text" placeholder="Reviewer 1" name="table2_header_0" required></th>
<th><input type="text" placeholder="Reviewer 2" name="table2_header_1" required></th>
<th><input type="text" placeholder="Reviewer 3" name="table2_header_2" required></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text" placeholder="Preference 1..." name="table2_row_0_col_0"></td>
<td><input type="text" placeholder="Preference 1..." name="table2_row_0_col_1"></td>
<td><input type="text" placeholder="Preference 1..." name="table2_row_0_col_2"></td>
</tr>
<tr>
<td><input type="text" name="table2_row_1_col_0"></td>
<td><input type="text" name="table2_row_1_col_1"></td>
<td><input type="text" name="table2_row_1_col_2"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-content" id="file-content">
<div class="form-section">
<div class="file-upload">
<label for="file-input">Upload a CSV or Excel file:</label>
<input type="file" id="file-input" name="file" accept=".csv,.xlsx,.xls">
<div class="file-info" id="file-info"></div>
</div>
</div>
</div>
<div class="form-section submit-section">
<button type="submit" class="submit-button">
<span id="spinner" class="spinner"></span>
<span id="submit-text">Submit Preferences</span>
</button>
<div class="progress-bar" id="progress-bar">
<div class="progress" id="progress"></div>
</div>
<div class="message" id="message"></div>
</div>
<div class="container">
<details>
<summary><b>Input/Output Format Documentation</b></summary>
<p>
The matcher assumes a specific tabular scheme for the input data - to read more about how this works see the
<a href="https://github.com/tgorordo/carousel">README</a>.
You can enter preference information directly in the tables above, or upload an Excel spreadsheet with
two sheets of preferences, or a CSV containing a ranking matrix.
</p> More on each format scheme:
<h3>Tabular Input</h3>
You can enter tables of preferences directly in the first tab of the form above.
<h4>Example:</h4>
<h3>File Upload</h3>
For more complicated input, a file upload might be preferable to table form submission
(e.g. if there's a validation issue you won't have to manually re-enter the table, just tweak and re-upload the file).
Two file formats are supported, Excel and CSV:
<h4>Excel</h4>
<h4>CSV</h4>
</details>
</div>
</form>
</div>
<script>
// Tab switching functionality
document.getElementById('table-tab').addEventListener('click', () => {
switchTab('table');
});
document.getElementById('file-tab').addEventListener('click', () => {
switchTab('file');
});
function switchTab(tabName) {
// Hide all tabs and contents
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Show the selected tab and content
document.getElementById(`${tabName}-tab`).classList.add('active');
document.getElementById(`${tabName}-content`).classList.add('active');
}
// Table manipulation functions
function setupTableControls(tableNumber) {
const addColumnBtn = document.getElementById(`add-column-${tableNumber}`);
const addRowBtn = document.getElementById(`add-row-${tableNumber}`);
const removeColumnBtn = document.getElementById(`remove-column-${tableNumber}`);
const removeRowBtn = document.getElementById(`remove-row-${tableNumber}`);
const dataTable = document.getElementById(`data-table-${tableNumber}`);
const headerRow = document.getElementById(`header-row-${tableNumber}`);
addColumnBtn.addEventListener('click', () => {
const colCount = headerRow.cells.length;
// Add header cell
const newHeader = document.createElement('th');
newHeader.innerHTML = `<input type="text" placeholder="Column ${colCount + 1}" name="table${tableNumber}_header_${colCount}" required>`;
headerRow.appendChild(newHeader);
// Add cell to each row
const rows = dataTable.querySelectorAll('tbody tr');
rows.forEach((row, rowIndex) => {
const newCell = document.createElement('td');
newCell.innerHTML = `<input type="text" name="table${tableNumber}_row_${rowIndex}_col_${colCount}">`;
row.appendChild(newCell);
});
});
addRowBtn.addEventListener('click', () => {
const tbody = dataTable.querySelector('tbody');
const rows = tbody.querySelectorAll('tr');
const rowCount = rows.length;
const colCount = headerRow.cells.length;
// Create new row
const newRow = document.createElement('tr');
// Add cells to the new row
for (let i = 0; i < colCount; i++) {
const newCell = document.createElement('td');
newCell.innerHTML = `<input type="text" name="table${tableNumber}_row_${rowCount}_col_${i}">`;
newRow.appendChild(newCell);
}
tbody.appendChild(newRow);
});
removeColumnBtn.addEventListener('click', () => {
const colCount = headerRow.cells.length;
if (colCount <= 1) {
alert('Table must have at least one column.');
return;
}
// Remove the last header cell
headerRow.deleteCell(colCount - 1);
// Remove the last cell from each row
const rows = dataTable.querySelectorAll('tbody tr');
rows.forEach(row => {
row.deleteCell(colCount - 1);
});
});
removeRowBtn.addEventListener('click', () => {
const tbody = dataTable.querySelector('tbody');
const rows = tbody.querySelectorAll('tr');
if (rows.length <= 1) {
alert('Table must have at least one row.');
return;
}
// Remove the last row
tbody.removeChild(rows[rows.length - 1]);
});
}
// Setup controls for both tables
setupTableControls(1);
setupTableControls(2);
// Fill Default functionality for Table 2
const fillDefaultCheckbox = document.getElementById('fill-default');
const fillDefaultBtn = document.getElementById('fill-default-btn');
const table2Container = document.getElementById('table2-container');
fillDefaultBtn.addEventListener('click', () => {
fillDefaultCheckbox.checked = !fillDefaultCheckbox.checked;
toggleTable2State();
});
fillDefaultCheckbox.addEventListener('change', toggleTable2State);
function toggleTable2State() {
if (fillDefaultCheckbox.checked) {
table2Container.classList.add('disabled-table');
} else {
table2Container.classList.remove('disabled-table');
}
}
// File upload handling
const fileInput = document.getElementById('file-input');
const fileInfo = document.getElementById('file-info');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
fileInfo.textContent = `Selected: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
} else {
fileInfo.textContent = '';
}
});
// Form submission
const dataForm = document.getElementById('data-form');
const submitButton = document.querySelector('.submit-button');
const spinner = document.getElementById('spinner');
const submitText = document.getElementById('submit-text');
const progressBar = document.getElementById('progress-bar');
const progress = document.getElementById('progress');
const message = document.getElementById('message');
dataForm.addEventListener('submit', (e) => {
e.preventDefault();
// Show spinner and disable button
spinner.style.display = 'inline-block';
submitButton.disabled = true;
submitText.textContent = 'Submitting...';
progressBar.style.display = 'block';
message.style.display = 'none';
message.className = 'message';
// Collect form data
const formData = new FormData(dataForm);
// Add a flag to indicate which tab is active
formData.append('input_type', document.getElementById('table-content').classList.contains('active') ? 'table' : 'file');
// If table input is active, add table structure info
if (document.getElementById('table-content').classList.contains('active')) {
// Table 1 info
const colCount1 = document.getElementById('header-row-1').cells.length;
const rowCount1 = document.getElementById('data-table-1').querySelectorAll('tbody tr').length;
formData.append('table1_col_count', colCount1);
formData.append('table1_row_count', rowCount1);
// Table 2 info
const colCount2 = document.getElementById('header-row-2').cells.length;
const rowCount2 = document.getElementById('data-table-2').querySelectorAll('tbody tr').length;
formData.append('table2_col_count', colCount2);
formData.append('table2_row_count', rowCount2);
formData.append('table2_use_default', document.getElementById('fill-default').checked);
}
// Simulate upload progress (in a real application, you'd track actual upload progress)
let width = 0;
const progressInterval = setInterval(() => {
if (width >= 90) {
clearInterval(progressInterval);
} else {
width += 5;
progress.style.width = width + '%';
}
}, 100);
// Send form data to server
fetch(dataForm.action, {
method: 'POST',
body: formData
})
.then(response => {
clearInterval(progressInterval);
progress.style.width = '100%';
if (!response.ok) {
throw new Error('Server error: ' + response.status);
}
return response.json();
})
.then(data => {
// Show success message
message.textContent = data.message || 'Data submitted successfully!';
message.classList.add('success');
message.style.display = 'block';
})
.catch(error => {
// Show error message
console.error('Error:', error);
message.textContent = 'An error occurred while submitting the data. Please try again.';
message.classList.add('error');
message.style.display = 'block';
})
.finally(() => {
// Hide spinner and re-enable button
setTimeout(() => {
spinner.style.display = 'none';
submitButton.disabled = false;
submitText.textContent = 'Submit Data';
progressBar.style.display = 'none';
}, 500);
});
});
</script>
</body>
</html>

View file

@ -1,27 +1,14 @@
import logging, rich
from rich.logging import RichHandler
import polars as pl
import polars.selectors as pls
import numpy as np
from polars.testing import assert_frame_equal
import pytest
import pytest, rich
from hypothesis import given, strategies as st
import carousel as crsl
logging.basicConfig(
level="NOTSET",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[np, pl, pls])],
)
log = logging.getLogger("rich")
rng = np.random.default_rng()
@ -57,8 +44,6 @@ def test_invalid_pref():
def test_pref_to_rank():
rr = crsl.pref_to_rank(p)
rich.print(p, rr, r)
assert_frame_equal(crsl.pref_to_rank(p), r, check_dtypes=False)
@ -139,3 +124,12 @@ def test_eg2_isstable():
match = pl.DataFrame({"A": ["c"], "B": ["d"], "C": ["a"], "D": ["b"]})
assert crsl.check_stable(match, ar, rr)
@given(
rankings(names=["a", "b", "c", "d"], choices=["A", "B", "C", "D"]),
rankings(names=["A", "B", "C", "D"], choices=["a", "b", "c", "d"]),
)
def test_defacc_isstable(applicant_rankings, reviewer_rankings):
match = crsl.deferred_acceptance(applicant_rankings, reviewer_rankings)
assert crsl.check_stable(match, applicant_rankings, reviewer_rankings)

132
uv.lock generated
View file

@ -42,6 +42,10 @@ dev = [
{ name = "pytest-benchmark" },
{ name = "ruff" },
]
srv = [
{ name = "legacy-cgi" },
{ name = "python-dotenv" },
]
[package.metadata]
requires-dist = [
@ -62,6 +66,10 @@ dev = [
{ name = "pytest-benchmark", specifier = ">=5.1.0" },
{ name = "ruff", specifier = ">=0.11.2" },
]
srv = [
{ name = "legacy-cgi", specifier = ">=2.6.3" },
{ name = "python-dotenv", specifier = ">=1.1.0" },
]
[[package]]
name = "click"
@ -86,15 +94,15 @@ wheels = [
[[package]]
name = "hypothesis"
version = "6.131.5"
version = "6.131.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3c/49/8d61e63b60b8e9584618a8fdbc1401b27480f9c25fdde2d41fe3372e7def/hypothesis-6.131.5.tar.gz", hash = "sha256:e76b192dc4fd033d7c33f94d8775bcbfd522a143b67adca30513e7727ebe7af6", size = 433127 }
sdist = { url = "https://files.pythonhosted.org/packages/10/ff/217417d065aa8a4e6815ddc39acee1222f1b67bd0e4803b85de86a837873/hypothesis-6.131.9.tar.gz", hash = "sha256:ee9b0e1403e1121c91921dbdc79d7f509fdb96d457a0389222d2a68d6c8a8f8e", size = 435415 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/83/f1427ae8f94b9e2327d2487aacd7645f1c01b12e556c92d3d8cca57aa28b/hypothesis-6.131.5-py3-none-any.whl", hash = "sha256:a97bdc0f23276fb4c52b99aeb7888adfbe052722c4cf9e398a228b48cf2c4504", size = 497966 },
{ url = "https://files.pythonhosted.org/packages/bd/e5/41a6733bfe11997795669dec3b3d785c28918e06568a2540dcc29f0d3fa7/hypothesis-6.131.9-py3-none-any.whl", hash = "sha256:7c2d9d6382e98e5337b27bd34e5b223bac23956787a827e1d087e00d893561d6", size = 499853 },
]
[[package]]
@ -106,6 +114,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
]
[[package]]
name = "legacy-cgi"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/ed/300cabc9693209d5a03e2ebc5eb5c4171b51607c08ed84a2b71c9015e0f3/legacy_cgi-2.6.3.tar.gz", hash = "sha256:4c119d6cb8e9d8b6ad7cc0ddad880552c62df4029622835d06dfd18f438a8154", size = 24401 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/33/68c6c38193684537757e0d50a7ccb4f4656e5c2f7cd2be737a9d4a1bff71/legacy_cgi-2.6.3-py3-none-any.whl", hash = "sha256:6df2ea5ae14c71ef6f097f8b6372b44f6685283dc018535a75c924564183cdab", size = 19851 },
]
[[package]]
name = "macholib"
version = "1.16.3"
@ -150,39 +167,39 @@ wheels = [
[[package]]
name = "numpy"
version = "2.2.4"
version = "2.2.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
sdist = { url = "https://files.pythonhosted.org/packages/dc/b2/ce4b867d8cd9c0ee84938ae1e6a6f7926ebf928c9090d036fc3c6a04f946/numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", size = 20273920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 },
{ url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 },
{ url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 },
{ url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 },
{ url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
{ url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
{ url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
{ url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
{ url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
{ url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
{ url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 },
{ url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 },
{ url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 },
{ url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 },
{ url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
{ url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
{ url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
{ url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
{ url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
{ url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
{ url = "https://files.pythonhosted.org/packages/e2/a0/0aa7f0f4509a2e07bd7a509042967c2fab635690d4f48c6c7b3afd4f448c/numpy-2.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4", size = 20935102 },
{ url = "https://files.pythonhosted.org/packages/7e/e4/a6a9f4537542912ec513185396fce52cdd45bdcf3e9d921ab02a93ca5aa9/numpy-2.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f", size = 14191709 },
{ url = "https://files.pythonhosted.org/packages/be/65/72f3186b6050bbfe9c43cb81f9df59ae63603491d36179cf7a7c8d216758/numpy-2.2.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9", size = 5149173 },
{ url = "https://files.pythonhosted.org/packages/e5/e9/83e7a9432378dde5802651307ae5e9ea07bb72b416728202218cd4da2801/numpy-2.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191", size = 6684502 },
{ url = "https://files.pythonhosted.org/packages/ea/27/b80da6c762394c8ee516b74c1f686fcd16c8f23b14de57ba0cad7349d1d2/numpy-2.2.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372", size = 14084417 },
{ url = "https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d", size = 16133807 },
{ url = "https://files.pythonhosted.org/packages/bf/9b/4cc171a0acbe4666f7775cfd21d4eb6bb1d36d3a0431f48a73e9212d2278/numpy-2.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7", size = 15575611 },
{ url = "https://files.pythonhosted.org/packages/a3/45/40f4135341850df48f8edcf949cf47b523c404b712774f8855a64c96ef29/numpy-2.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73", size = 17895747 },
{ url = "https://files.pythonhosted.org/packages/f8/4c/b32a17a46f0ffbde8cc82df6d3daeaf4f552e346df143e1b188a701a8f09/numpy-2.2.5-cp313-cp313-win32.whl", hash = "sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b", size = 6309594 },
{ url = "https://files.pythonhosted.org/packages/13/ae/72e6276feb9ef06787365b05915bfdb057d01fceb4a43cb80978e518d79b/numpy-2.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471", size = 12638356 },
{ url = "https://files.pythonhosted.org/packages/79/56/be8b85a9f2adb688e7ded6324e20149a03541d2b3297c3ffc1a73f46dedb/numpy-2.2.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6", size = 20963778 },
{ url = "https://files.pythonhosted.org/packages/ff/77/19c5e62d55bff507a18c3cdff82e94fe174957bad25860a991cac719d3ab/numpy-2.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba", size = 14207279 },
{ url = "https://files.pythonhosted.org/packages/75/22/aa11f22dc11ff4ffe4e849d9b63bbe8d4ac6d5fae85ddaa67dfe43be3e76/numpy-2.2.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133", size = 5199247 },
{ url = "https://files.pythonhosted.org/packages/4f/6c/12d5e760fc62c08eded0394f62039f5a9857f758312bf01632a81d841459/numpy-2.2.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376", size = 6711087 },
{ url = "https://files.pythonhosted.org/packages/ef/94/ece8280cf4218b2bee5cec9567629e61e51b4be501e5c6840ceb593db945/numpy-2.2.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19", size = 14059964 },
{ url = "https://files.pythonhosted.org/packages/39/41/c5377dac0514aaeec69115830a39d905b1882819c8e65d97fc60e177e19e/numpy-2.2.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0", size = 16121214 },
{ url = "https://files.pythonhosted.org/packages/db/54/3b9f89a943257bc8e187145c6bc0eb8e3d615655f7b14e9b490b053e8149/numpy-2.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a", size = 15575788 },
{ url = "https://files.pythonhosted.org/packages/b1/c4/2e407e85df35b29f79945751b8f8e671057a13a376497d7fb2151ba0d290/numpy-2.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066", size = 17893672 },
{ url = "https://files.pythonhosted.org/packages/29/7e/d0b44e129d038dba453f00d0e29ebd6eaf2f06055d72b95b9947998aca14/numpy-2.2.5-cp313-cp313t-win32.whl", hash = "sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e", size = 6377102 },
{ url = "https://files.pythonhosted.org/packages/63/be/b85e4aa4bf42c6502851b971f1c326d583fcc68227385f92089cf50a7b45/numpy-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8", size = 12750096 },
]
[[package]]
name = "packaging"
version = "24.2"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
@ -287,15 +304,15 @@ wheels = [
[[package]]
name = "pyright"
version = "1.1.399"
version = "1.1.400"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954 }
sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 },
{ url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460 },
]
[[package]]
@ -374,6 +391,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/d6/b41653199ea09d5969d4e385df9bbfd9a100f28ca7e824ce7c0a016e3053/pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89", size = 44259 },
]
[[package]]
name = "python-dotenv"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
@ -398,36 +424,36 @@ wheels = [
[[package]]
name = "ruff"
version = "0.11.6"
version = "0.11.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053 }
sdist = { url = "https://files.pythonhosted.org/packages/5b/89/6f9c9674818ac2e9cc2f2b35b704b7768656e6b7c139064fc7ba8fbc99f1/ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4", size = 4054861 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105 },
{ url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494 },
{ url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151 },
{ url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951 },
{ url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195 },
{ url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918 },
{ url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426 },
{ url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012 },
{ url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947 },
{ url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753 },
{ url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121 },
{ url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829 },
{ url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108 },
{ url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366 },
{ url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900 },
{ url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592 },
{ url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766 },
{ url = "https://files.pythonhosted.org/packages/b4/ec/21927cb906c5614b786d1621dba405e3d44f6e473872e6df5d1a6bca0455/ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c", size = 10245403 },
{ url = "https://files.pythonhosted.org/packages/e2/af/fec85b6c2c725bcb062a354dd7cbc1eed53c33ff3aa665165871c9c16ddf/ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee", size = 11007166 },
{ url = "https://files.pythonhosted.org/packages/31/9a/2d0d260a58e81f388800343a45898fd8df73c608b8261c370058b675319a/ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada", size = 10378076 },
{ url = "https://files.pythonhosted.org/packages/c2/c4/9b09b45051404d2e7dd6d9dbcbabaa5ab0093f9febcae664876a77b9ad53/ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64", size = 10557138 },
{ url = "https://files.pythonhosted.org/packages/5e/5e/f62a1b6669870a591ed7db771c332fabb30f83c967f376b05e7c91bccd14/ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201", size = 10095726 },
{ url = "https://files.pythonhosted.org/packages/45/59/a7aa8e716f4cbe07c3500a391e58c52caf665bb242bf8be42c62adef649c/ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6", size = 11672265 },
{ url = "https://files.pythonhosted.org/packages/dd/e3/101a8b707481f37aca5f0fcc3e42932fa38b51add87bfbd8e41ab14adb24/ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4", size = 12331418 },
{ url = "https://files.pythonhosted.org/packages/dd/71/037f76cbe712f5cbc7b852e4916cd3cf32301a30351818d32ab71580d1c0/ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e", size = 11794506 },
{ url = "https://files.pythonhosted.org/packages/ca/de/e450b6bab1fc60ef263ef8fcda077fb4977601184877dce1c59109356084/ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63", size = 13939084 },
{ url = "https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502", size = 11450441 },
{ url = "https://files.pythonhosted.org/packages/9d/7d/1b048eb460517ff9accd78bca0fa6ae61df2b276010538e586f834f5e402/ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92", size = 10441060 },
{ url = "https://files.pythonhosted.org/packages/3a/57/8dc6ccfd8380e5ca3d13ff7591e8ba46a3b330323515a4996b991b10bd5d/ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94", size = 10058689 },
{ url = "https://files.pythonhosted.org/packages/23/bf/20487561ed72654147817885559ba2aa705272d8b5dee7654d3ef2dbf912/ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6", size = 11073703 },
{ url = "https://files.pythonhosted.org/packages/9d/27/04f2db95f4ef73dccedd0c21daf9991cc3b7f29901a4362057b132075aa4/ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6", size = 11532822 },
{ url = "https://files.pythonhosted.org/packages/e1/72/43b123e4db52144c8add336581de52185097545981ff6e9e58a21861c250/ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26", size = 10362436 },
{ url = "https://files.pythonhosted.org/packages/c5/a0/3e58cd76fdee53d5c8ce7a56d84540833f924ccdf2c7d657cb009e604d82/ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a", size = 11566676 },
{ url = "https://files.pythonhosted.org/packages/68/ca/69d7c7752bce162d1516e5592b1cc6b6668e9328c0d270609ddbeeadd7cf/ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177", size = 10677936 },
]
[[package]]
name = "setuptools"
version = "78.1.0"
version = "79.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 }
sdist = { url = "https://files.pythonhosted.org/packages/bb/71/b6365e6325b3290e14957b2c3a804a529968c77a049b2ed40c095f749707/setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88", size = 1367909 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 },
{ url = "https://files.pythonhosted.org/packages/0d/6d/b4752b044bf94cb802d88a888dc7d288baaf77d7910b7dedda74b5ceea0c/setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51", size = 1256281 },
]
[[package]]