diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..bab54ad
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+name: Bug report
+description: A calculation gives a wrong answer, or something crashes.
+labels: [bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for reporting! For wrong numbers, a reference value (textbook,
+ standard, or hand calculation) helps us fix it fast.
+ - type: textarea
+ id: what
+ attributes:
+ label: What happened?
+ description: Include the full error message if there was one.
+ validations:
+ required: true
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: Code to reproduce
+ description: The smallest snippet that shows the problem.
+ render: python
+ placeholder: |
+ from fpc import Fluids
+ Fluids().colebrook_friction_factor(reynolds=1e5, relative_roughness=1e-4)
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected result and its source
+ description: What should the answer be, and where does that value come from?
+ - type: input
+ id: version
+ attributes:
+ label: Environment
+ description: Operating system, Python version (`uv run python --version`), and commit or version.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..ebbe3c9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/new_domain.yml b/.github/ISSUE_TEMPLATE/new_domain.yml
new file mode 100644
index 0000000..9c957c8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new_domain.yml
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+name: New domain or method request
+description: Propose a new engineering class or new calculations for an existing one.
+labels: [enhancement, new-domain]
+body:
+ - type: input
+ id: domain
+ attributes:
+ label: Domain or class
+ description: A new domain (e.g. "Acoustics") or an existing class (e.g. "Fluids").
+ validations:
+ required: true
+ - type: textarea
+ id: methods
+ attributes:
+ label: Calculations to include
+ description: One line per method, with the formula if you know it.
+ placeholder: |
+ - sound_pressure_level(pressure) -> dB, Lp = 20 log10(p / 20 uPa)
+ - add_decibels(levels) -> combined level
+ validations:
+ required: true
+ - type: textarea
+ id: references
+ attributes:
+ label: References
+ description: Textbooks, standards, or papers the formulas come from, so they can be verified.
+ - type: textarea
+ id: who
+ attributes:
+ label: Who would use this?
+ description: The kind of engineer or student, and the problem it helps them solve.
+ - type: checkboxes
+ id: contribute
+ attributes:
+ label: Contributing
+ options:
+ - label: I'd like to implement this myself (see the checklist in CONTRIBUTOR.md).
diff --git a/.github/ISSUE_TEMPLATE/new_example.yml b/.github/ISSUE_TEMPLATE/new_example.yml
new file mode 100644
index 0000000..8ca3394
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new_example.yml
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+name: New worked example
+description: Suggest a realistic problem for the examples/ folder.
+labels: [example]
+body:
+ - type: textarea
+ id: question
+ attributes:
+ label: The engineering question
+ description: Phrase it the way it comes up at work, with real-looking numbers.
+ placeholder: "We need to move 12 L/s of water up 15 m through 120 m of steel pipe. Which pipe size and pump?"
+ validations:
+ required: true
+ - type: textarea
+ id: approach
+ attributes:
+ label: Approach
+ description: The steps and formulas a good answer would walk through.
+ - type: textarea
+ id: python
+ attributes:
+ label: Python ideas it would teach
+ description: e.g. root finding, pandas groupby, Pint conversions, optimization, plotting.
+ - type: checkboxes
+ id: contribute
+ attributes:
+ label: Contributing
+ options:
+ - label: I'd like to write this example myself.
diff --git a/.github/make_social_preview.py b/.github/make_social_preview.py
index 7552f7c..3a49a34 100644
--- a/.github/make_social_preview.py
+++ b/.github/make_social_preview.py
@@ -18,11 +18,11 @@
import numpy as np
# --- Palette (kept in one place so it is easy to retheme) -----------------
-BG = "#0d1b2a" # deep navy background
-PANEL = "#152538" # slightly lighter panel for the plot
-CYAN = "#4cc9f0" # primary accent
-MAGENTA = "#f72585" # secondary accent
-TEXT = "#e6edf3" # near-white body text
+BG = "#0d1b2a" # deep navy background
+PANEL = "#152538" # slightly lighter panel for the plot
+CYAN = "#4cc9f0" # primary accent
+MAGENTA = "#f72585" # secondary accent
+TEXT = "#e6edf3" # near-white body text
SUBTLE = "#8b97a7" # muted captions
# 16in x 8in at 80 dpi -> 1280 x 640 px, exactly what GitHub wants.
@@ -40,22 +40,37 @@
canvas.add_patch(plt.Rectangle((0.04, 0.905), 0.075, 0.012, color=CYAN, lw=0))
# Title block: project name, one-line value prop, two-line description.
-canvas.text(0.04, 0.74, "First Principles Code",
- fontsize=46, color=TEXT, weight="bold")
-canvas.text(0.04, 0.65, "Open-source Python for engineers",
- fontsize=22, color=CYAN, weight="semibold")
-canvas.text(0.04, 0.51,
- "A teaching template that replaces MATLAB & Maplesoft\n"
- "with NumPy, SciPy, matplotlib, and Pint.",
- fontsize=18, color=SUBTLE, linespacing=1.4)
+canvas.text(0.04, 0.74, "First Principles Code", fontsize=46, color=TEXT, weight="bold")
+canvas.text(
+ 0.04, 0.65, "Open-source Python for engineers", fontsize=22, color=CYAN, weight="semibold"
+)
+canvas.text(
+ 0.04,
+ 0.51,
+ "A teaching template that replaces MATLAB & Maplesoft\n"
+ "with NumPy, SciPy, matplotlib, and Pint.",
+ fontsize=18,
+ color=SUBTLE,
+ linespacing=1.4,
+)
# Feature ribbon and repo URL near the bottom.
-canvas.text(0.04, 0.18,
- "dynamics · statics · vibration · controls"
- " · Pint units · solve_ivp · curve_fit · pytest",
- fontsize=14, color=SUBTLE)
-canvas.text(0.04, 0.10, "github.com/evangress/first-principles-code",
- fontsize=14, color=CYAN, weight="medium")
+canvas.text(
+ 0.04,
+ 0.18,
+ "dynamics · statics · vibration · controls"
+ " · Pint units · solve_ivp · curve_fit · pytest",
+ fontsize=14,
+ color=SUBTLE,
+)
+canvas.text(
+ 0.04,
+ 0.10,
+ "github.com/evangress/first-principles-code",
+ fontsize=14,
+ color=CYAN,
+ weight="medium",
+)
# --- Inset plot: damped free-vibration "simulate -> fit" hero --------------
plot = fig.add_axes((0.60, 0.22, 0.36, 0.58))
@@ -66,12 +81,13 @@
mass, stiffness, damping = 2.0, 800.0, 8.0
omega_n = np.sqrt(stiffness / mass)
zeta = damping / (2 * np.sqrt(stiffness * mass))
-omega_d = omega_n * np.sqrt(1 - zeta ** 2)
+omega_d = omega_n * np.sqrt(1 - zeta**2)
displacement_mm = 10.0 * np.exp(-zeta * omega_n * time) * np.cos(omega_d * time)
plot.plot(time, displacement_mm, color=CYAN, lw=2.5, label="simulate")
-plot.plot(time, displacement_mm, color=MAGENTA, lw=1.4, ls="--", alpha=0.85,
- label="fit recovers params")
+plot.plot(
+ time, displacement_mm, color=MAGENTA, lw=1.4, ls="--", alpha=0.85, label="fit recovers params"
+)
plot.set_title("simulate → fit", color=TEXT, loc="left", fontsize=14, pad=10)
plot.set_xlabel("time [s]", color=SUBTLE, fontsize=12)
@@ -80,8 +96,7 @@
for spine in plot.spines.values():
spine.set_color(SUBTLE)
plot.grid(True, color="#2a3a52", alpha=0.5)
-plot.legend(loc="upper right", facecolor=PANEL, edgecolor=SUBTLE,
- labelcolor=TEXT, fontsize=11)
+plot.legend(loc="upper right", facecolor=PANEL, edgecolor=SUBTLE, labelcolor=TEXT, fontsize=11)
# --- Save -------------------------------------------------------------------
output_path = Path(__file__).resolve().parent / "social-preview.png"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..330d7a8
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+#
+# Builds the documentation site (mkdocs + mkdocstrings) on every pull request
+# so broken links or docstrings fail early, and publishes it to GitHub Pages
+# on every push to main.
+#
+# One-time repository setup: Settings -> Pages -> Source: "GitHub Actions".
+
+name: docs
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: build site
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Install Python 3.14
+ run: uv python install 3.14
+
+ - name: Build (strict - warnings are errors)
+ env:
+ NO_MKDOCS_2_WARNING: "1"
+ run: uv run --group docs mkdocs build --strict
+
+ - name: Upload site for Pages
+ if: github.event_name == 'push'
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+
+ deploy:
+ name: deploy to GitHub Pages
+ if: github.event_name == 'push'
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d86f2a7..d1a30de 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,7 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
#
-# Runs the pytest suite on every push to main and on every pull request.
+# Runs lint, format check, type check, and the pytest suite on every push to
+# main and on every pull request.
# Uses uv (the same tool the project develops with) so CI matches local dev.
name: tests
@@ -14,7 +15,7 @@ on:
jobs:
pytest:
- name: pytest (Python 3.14)
+ name: checks + pytest (Python 3.14)
runs-on: ubuntu-latest
steps:
- name: Check out the repository
@@ -32,5 +33,16 @@ jobs:
- name: Sync dependencies (incl. dev group)
run: uv sync --all-groups
+ - name: Lint (ruff)
+ run: uv run ruff check .
+
+ - name: Check formatting (ruff format)
+ run: uv run ruff format --check .
+
+ - name: Type check (mypy)
+ run: uv run mypy
+
+ # --all-groups above installed the notebook tools, so this also
+ # executes every notebook and runs every example script.
- name: Run pytest
run: uv run pytest
diff --git a/.gitignore b/.gitignore
index 6fb2406..d9ccfd8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -182,3 +182,10 @@ displacement.png
*.png
# ...but keep the committed GitHub social-preview card
!.github/social-preview.png
+
+# PyCharm / JetBrains per-user settings
+.idea/
+
+# Figures and tables written by the example scripts and template
+examples/output/
+templates/output/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 30cf57e..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Ignored default folder with query files
-/queries/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/first-principles-code.iml b/.idea/first-principles-code.iml
deleted file mode 100644
index 50bbd7c..0000000
--- a/.idea/first-principles-code.iml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 5dec72b..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 4dbdfe1..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/pyProjectModel.xml b/.idea/pyProjectModel.xml
deleted file mode 100644
index 9963416..0000000
--- a/.idea/pyProjectModel.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..285b2b3
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+#
+# pre-commit runs these checks automatically on every `git commit`.
+# One-time setup: uv run pre-commit install
+# Run on demand: uv run pre-commit run --all-files
+#
+# The hooks are "local": they call the exact ruff/pytest versions pinned in
+# uv.lock, so the hook, your editor, and CI always agree.
+repos:
+ - repo: local
+ hooks:
+ - id: ruff-check
+ name: ruff check (lint, auto-fix)
+ entry: uv run ruff check --fix
+ language: system
+ types_or: [python, jupyter]
+ - id: ruff-format
+ name: ruff format
+ entry: uv run ruff format
+ language: system
+ types: [python]
+ - id: pytest
+ name: pytest
+ entry: uv run pytest -q -x
+ language: system
+ pass_filenames: false
+ always_run: true
diff --git a/CLAUDE.md b/CLAUDE.md
index 2e14592..eff99a1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -9,38 +9,42 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
A teaching template: a Python script + class library meant to functionally teach engineers Python techniques and empower them to solve real-world engineering problems. Because it's a teaching artifact, code clarity, docstrings, and explanatory comments are first-class deliverables — not afterthoughts. See "Authoring conventions" below.
-The full project design — intended `main.py` demo, the per-domain library classes, and the README deliverables — lives in [SPEC.md](SPEC.md). Read it before implementing features.
+The original design lives in [SPEC.md](SPEC.md); the improvement backlog lives in [ROADMAP.md](ROADMAP.md). Read both before starting new work.
-## Current state vs. intended design
+## Current state
-The repo is scaffolded but largely unimplemented. Know the difference before you start:
-
-- `main.py` — currently PyCharm hello-world boilerplate. **Intended (SPEC):** a demo that calculates displacement from a known acceleration and velocity as a function of time (using `math` / `scipy`).
-- `library/` — empty except an `__include__` marker file. **Intended (SPEC):** one class per engineering domain.
-- `pyproject.toml` — `dependencies = []`. Add `scipy` (and likely `numpy`) here when implementing the `main.py` demo and the math-heavy classes.
-- No README, no tests yet — both are intended deliverables.
+- `main.py` — displacement demo (closed form, `scipy` integration, library call) that prints results and saves a plot.
+- `fpc/` — the installable package (import name `fpc`, distribution name `first-principles-code`). One class per engineering domain (Dynamics, Statics, Modal, Thermo, Fluids, Material, Gear, Electrical, Mechatronics, Controls, Utility, Design, Properties, plus the radiology set Radiation, Radiography, CT, MRI, Ultrasound, Dicom), the shared Pint registry in `fpc/units.py`, and the unit-aware `Beam` dataclass.
+- `gui.py` — tkinter MATLAB-style console; the `Session` class is GUI-free and tested.
+- `tests/` — pytest suite, run in CI (`.github/workflows/tests.yml`).
+- `examples/` — one self-contained worked problem per script; `templates/` — copy-and-adapt starting scripts; `notebooks/` — Jupyter notebooks; `fpc/data/` — cited reference tables (CSV); `docs/` — mkdocs site sources and guides.
+- `PACKAGES.md` — curated package list by engineering field.
## Tooling & commands
-Python **3.14+**, managed with **uv** (a `.venv` already exists).
+Python **3.14+**, managed with **uv**.
```bash
-uv sync # install deps from pyproject.toml into .venv
-uv add scipy numpy # add a dependency (edits pyproject.toml + lockfile)
-uv run main.py # run the main script
-uv run python -c "..." # run arbitrary python in the project env
+uv sync --all-groups # install runtime + dev + notebook deps
+uv run main.py # run the main demo
+uv run pytest # run tests (single test: uv run pytest path::test_name)
+uv run ruff check . # lint
+uv run ruff format . # format
+uv run mypy fpc # type-check the package
+uv run mkdocs serve # preview the API docs site
+uv run pre-commit install # enable git hooks (ruff + pytest)
```
-There is no test runner or linter configured yet. If you add tests, prefer `pytest` (`uv add --dev pytest`, run with `uv run pytest`, single test via `uv run pytest path::test_name`).
-
## Library architecture
-`./library` holds one class per engineering domain, each class grouping related calculation methods (see SPEC.md for the full planned class list). When adding domains beyond that list, follow the same one-class-per-file, domain-grouped pattern.
+`fpc/` holds one class per engineering domain, each grouping related calculation methods. When adding a domain, follow the checklist in [CONTRIBUTOR.md](CONTRIBUTOR.md) (module, class, `__init__` export, GUI preload, tests, README row, docs page).
+
+Units: plain floats in SI units for quick formulas; Pint quantities (`fpc.units.Q_`) where units are mixed or error-prone. See `docs/units.md`.
## Authoring conventions
These exist to make the code itself a teaching tool — follow them in all new code:
-- Use `**kwargs` for method/function parameters to demonstrate verbose, explicit variable passing.
-- Every class and method gets a docstring — they double as documentation examples.
+- Use keyword-only arguments (`def method(self, *, ...)`) to demonstrate verbose, explicit variable passing.
+- Every class and method gets a docstring with parameter units — they double as documentation examples.
- Comment significant code sections explaining the *why*, not just the *what*.
diff --git a/CONTRIBUTOR.md b/CONTRIBUTOR.md
index ebe3481..b326fb9 100644
--- a/CONTRIBUTOR.md
+++ b/CONTRIBUTOR.md
@@ -12,7 +12,9 @@ code is the product*, not a side effect.
## Ways to contribute
- Add a new method to an existing engineering class.
-- Add a new domain class (see the planned list in [`SPEC.md`](SPEC.md)).
+- Add a new domain class (follow the checklist below).
+- Add a worked example to `examples/` or a notebook to `notebooks/`.
+- Pick up an item from [`ROADMAP.md`](ROADMAP.md).
- Implement an item from the "Python concepts to teach" roadmap in `SPEC.md`.
- Improve docstrings, comments, examples, or the README.
- Add or strengthen tests.
@@ -26,9 +28,20 @@ install and usage tutorial).
```bash
git clone
cd first-principles-code
-uv sync # create the environment from pyproject.toml + uv.lock
+uv sync --all-groups # create the environment (runtime + dev, notebooks, docs, apps)
uv run main.py # confirm the demo runs
uv run pytest # confirm the test suite is green
+uv run pre-commit install # run ruff + pytest automatically on every commit
+```
+
+Before opening a pull request, the same checks CI runs:
+
+```bash
+uv run ruff check . # lint
+uv run ruff format . # auto-format
+uv run mypy # type-check fpc/
+uv run pytest # tests, examples, notebooks
+uv run --group docs mkdocs build --strict # docs site builds cleanly
```
## Coding conventions
@@ -43,13 +56,42 @@ Because the code teaches, these conventions are requirements, not suggestions:
step, not just what the line does.
- **One domain per file, one class per file**, matching the existing layout.
- **State the units** for every physical quantity. For anything where unit
- mistakes are likely, prefer modelling it with Pint (see `library/units.py`
- and `library/beam.py`).
+ mistakes are likely, prefer modelling it with Pint (see `fpc/units.py`
+ and `fpc/beam.py`). Methods taking plain numbers use SI units unless the
+ parameter name says otherwise (`angle_deg`, `temperature_c`); see
+ [docs/units.md](docs/units.md).
+- **Reference data goes in `fpc/data/`**, not hard-coded constants, with its
+ source cited in `fpc/data/README.md`.
- **Validate inputs** that can realistically be wrong (negative area, wrong
units) and `raise ValueError` with a clear message — see `Beam.__post_init__`.
- **Keep methods stateless** where possible, so each one stands alone as a
worked example.
+## Checklist: adding a new domain class
+
+Copy this into your pull request description and tick each box.
+
+- [ ] **Module:** `fpc/.py` with an SPDX header, a module docstring
+ explaining the domain, and one class (`class :`).
+- [ ] **Methods:** keyword-only (`def method(self, *, ...)`), each with a
+ docstring giving the formula, every parameter's units, and the return
+ value's units. Comment the *why* of non-obvious steps.
+- [ ] **Validation:** `raise ValueError` with a clear message for inputs
+ that can realistically be wrong.
+- [ ] **Export:** import it in `fpc/__init__.py` and add it to `__all__`.
+- [ ] **GUI preload:** add a ready-made instance in `gui.py`'s
+ `Session.namespace`.
+- [ ] **Tests:** `tests/test_.py` checking textbook answers with
+ `pytest.approx`, plus at least one invalid-input test.
+- [ ] **Docs:** `docs/api/.md` containing `::: fpc.` and an
+ entry under "API reference" in `mkdocs.yml`.
+- [ ] **README:** a row or sentence in the relevant section and the
+ project-layout tree.
+- [ ] **Example (optional, encouraged):** a script in `examples/` answering
+ one realistic question with the new class, plus a row in
+ `examples/README.md`.
+- [ ] **Checks pass:** ruff, mypy, pytest, and the strict docs build.
+
## Tests
Every new method or class needs a test. The library methods are pure functions
@@ -80,9 +122,11 @@ uv add # runtime dependency
uv add --dev # development-only dependency (e.g. a tool)
```
-Favor well-maintained, widely-used scientific packages (see the package table
-in the README), and add a one-line note in the README table if it is something
-a learner would benefit from knowing about.
+Favor well-maintained, widely-used scientific packages (see
+[PACKAGES.md](PACKAGES.md)). If it is something a learner would benefit from
+knowing about, add a row to the right section of PACKAGES.md. Optional
+tooling belongs in a dependency group (`uv add --group notebooks ...`), not
+the runtime dependencies.
## Submitting changes
diff --git a/PACKAGES.md b/PACKAGES.md
new file mode 100644
index 0000000..0e2eefa
--- /dev/null
+++ b/PACKAGES.md
@@ -0,0 +1,152 @@
+
+
+
+# Python packages for engineers
+
+A curated list of Python packages, grouped by engineering field. Each row says
+**when** to reach for the package, not just what it is.
+
+- Add a package to this project with `uv add `; it updates
+ `pyproject.toml` and the lockfile.
+- Packages marked **(installed)** are already dependencies of this project.
+- Entries marked **(stdlib)** ship with Python; there is nothing to install.
+- Some packages install under one name and import under another. Those are
+ listed in [PyPI name vs. import name](#pypi-name-vs-import-name).
+
+**Last verified: 2026-11-06** (latest release date on PyPI, and that each link resolves).
+
+## Core stack
+
+| Package | Use it when... |
+| --- | --- |
+| [NumPy](https://numpy.org/) (installed) | You need fast arrays and vectorized math instead of Python loops |
+| [SciPy](https://scipy.org/) (installed) | You need integration, ODEs, interpolation, root finding, linear algebra, or statistics |
+| [pandas](https://pandas.pydata.org/) (installed) | Your data is a table: test logs, CSV/Excel exports, time series |
+| [Matplotlib](https://matplotlib.org/) (installed) | You need a plot, especially a publication-quality or MATLAB-style figure |
+| [SymPy](https://www.sympy.org/) | You want to derive, simplify, differentiate, or solve equations symbolically |
+| [Pint](https://pint.readthedocs.io/) (installed) | You want units carried through a calculation so mismatches raise errors |
+| [Jupyter](https://jupyter.org/) (`jupyterlab`) | You want an interactive notebook to explore and document a calculation |
+| [marimo](https://marimo.io/) | You want a reactive notebook stored as plain `.py` (clean diffs, runs as a script or app) |
+
+## Mechanical and structural
+
+| Package | Use it when... |
+| --- | --- |
+| [sectionproperties](https://sectionproperties.readthedocs.io/) | You need area, inertia, torsion constant, or stresses for an arbitrary cross-section |
+| [anaStruct](https://anastruct.readthedocs.io/) (`anastruct`) | You want quick 2-D frame and truss analysis with plotted diagrams |
+| [Pynite](https://github.com/JWock82/Pynite) (`PyNiteFEA`) | You need 3-D frame, beam, and plate finite element analysis |
+| [OpenSeesPy](https://openseespydoc.readthedocs.io/) (`openseespy`) | You need nonlinear, seismic, or research-grade structural analysis |
+| [scikit-fem](https://scikit-fem.readthedocs.io/) | You want to write your own finite element solver in pure Python |
+| [FEniCSx](https://fenicsproject.org/) (DOLFINx) | You need large-scale or multiphysics PDE solves; install via conda/Docker, not `uv add` (no PyPI package) |
+
+## Thermal and fluids
+
+| Package | Use it when... |
+| --- | --- |
+| [CoolProp](http://www.coolprop.org/) | You need thermophysical properties of water, refrigerants, air, or other fluids |
+| [Cantera](https://github.com/Cantera/cantera) | You are modeling combustion, chemical kinetics, or reacting flows |
+| [fluids](https://fluids.readthedocs.io/) | You are sizing pipes, pumps, valves, or computing pressure drop and friction factors |
+| [ht](https://ht.readthedocs.io/) | You need heat transfer correlations, heat exchanger sizing, or insulation calcs |
+
+## Electrical and signals
+
+| Package | Use it when... |
+| --- | --- |
+| [scipy.signal](https://docs.scipy.org/doc/scipy/reference/signal.html) (installed, part of SciPy) | You need filters, FFT-based spectra, convolution, or transfer functions |
+| [scikit-rf](https://scikit-rf.org/) | You work with RF/microwave networks: S-parameters, Touchstone files, calibration |
+| [lcapy](https://lcapy.readthedocs.io/) | You want symbolic circuit analysis (Laplace domain, node equations, drawn schematics) |
+| [PySpice](https://github.com/PySpice-org/PySpice) (flagged) | You want to drive ngspice simulations from Python. **Last PyPI release 2021**; consider [InSpice](https://github.com/Innovoltive/InSpice), an actively released fork |
+
+## Controls and robotics
+
+| Package | Use it when... |
+| --- | --- |
+| [python-control](https://python-control.org/) (`control`) | You need transfer functions, Bode/Nyquist/root locus, state space, or LQR (MATLAB Control Toolbox style) |
+| [Robotics Toolbox](https://petercorke.github.io/robotics-toolbox-python/) (`roboticstoolbox-python`) | You need forward/inverse kinematics, dynamics, or trajectory planning for robot arms |
+| [harold](https://github.com/ilayn/harold) (flagged) | You want a lightweight alternative controls toolbox. **Last PyPI release 2022**; prefer python-control |
+
+## CAD, geometry, and meshing
+
+| Package | Use it when... |
+| --- | --- |
+| [CadQuery](https://cadquery.readthedocs.io/) | You want parametric 3-D CAD models as code, exported to STEP/STL |
+| [build123d](https://build123d.readthedocs.io/) | Same as CadQuery, with a more Pythonic API (both use the OpenCascade kernel) |
+| [trimesh](https://trimesh.org/) | You need to load, repair, measure, or slice triangle meshes (STL, OBJ, PLY) |
+| [gmsh](https://gmsh.info/) | You need to generate a finite element mesh from geometry |
+| [PyVista](https://docs.pyvista.org/) | You need 3-D visualization of meshes, FEA results, or volume data |
+
+## Optimization and uncertainty
+
+| Package | Use it when... |
+| --- | --- |
+| [scipy.optimize](https://docs.scipy.org/doc/scipy/reference/optimize.html) (installed, part of SciPy) | You need curve fitting, root finding, or small constrained optimization |
+| [Pyomo](https://www.pyomo.org/) | You are building large LP/MILP/NLP models (scheduling, design, process optimization) |
+| [CVXPY](https://www.cvxpy.org/) | Your problem is convex and you want to write it the way it reads on paper |
+| [SALib](https://salib.readthedocs.io/) | You need sensitivity analysis: which inputs actually drive the output (Sobol, Morris) |
+| [pymoo](https://pymoo.org/) | You have multiple competing objectives and want a Pareto front (NSGA-II and friends) |
+
+## Data and ML
+
+| Package | Use it when... |
+| --- | --- |
+| [scikit-learn](https://scikit-learn.org/) | You need classic ML: regression, classification, clustering, surrogate models |
+| [statsmodels](https://www.statsmodels.org/) | You need statistical models with p-values, confidence intervals, ANOVA, or time-series fits |
+| [PyTorch](https://pytorch.org/) (`torch`) | You need deep learning or GPU-accelerated tensors |
+
+## Medical imaging
+
+| Package | Use it when... |
+| --- | --- |
+| [pydicom](https://pydicom.github.io/) (installed) | You need to read, edit, or write DICOM files |
+| [SimpleITK](https://simpleitk.org/) | You work with 3-D volumes: resampling, registration, segmentation, whole DICOM series |
+| [nibabel](https://nipy.org/nibabel/) | You work with NIfTI or other neuroimaging formats (research MRI) |
+| [scikit-image](https://scikit-image.org/) | You need filtering, thresholding, edge detection, or Radon transform / CT reconstruction demos |
+| [highdicom](https://highdicom.readthedocs.io/) | You need to create DICOM segmentations, structured reports, or annotations |
+| [pynetdicom](https://pydicom.github.io/pynetdicom/) | You need to talk to a PACS (C-FIND / C-MOVE / C-STORE) |
+| [MONAI](https://monai.io/) | You are doing deep learning on medical images (built on PyTorch) |
+| [TotalSegmentator](https://github.com/wasserth/TotalSegmentator) | You want pretrained automatic segmentation of 100+ structures on CT/MR |
+| [pyradiomics](https://pyradiomics.readthedocs.io/) (flagged) | You need quantitative radiomic features from an image + mask. **Last PyPI release 2023, wheels only up to Python 3.9**; expect a source build on Python 3.14 |
+
+## Tool building
+
+| Package | Use it when... |
+| --- | --- |
+| [argparse](https://docs.python.org/3/library/argparse.html) (stdlib) | You want a command-line interface with no extra dependency |
+| [Typer](https://typer.tiangolo.com/) | You want a CLI built from type hints, with help text and validation for free |
+| [Streamlit](https://streamlit.io/) | You want to turn a calculation into a shareable web app with sliders and plots |
+| [tkinter](https://docs.python.org/3/library/tkinter.html) (stdlib) | You want a simple desktop GUI with no extra dependency |
+| [PyInstaller](https://pyinstaller.org/) | You want to ship a script as a standalone executable for people without Python |
+
+## Performance
+
+| Package | Use it when... |
+| --- | --- |
+| [Numba](https://numba.pydata.org/) | A numeric loop is too slow and you want to JIT-compile it with one decorator |
+| [Cython](https://cython.org/) | You need C speed or want to wrap existing C/C++ code |
+| [JAX](https://docs.jax.dev/) | You need automatic differentiation, GPU/TPU, or vectorizing a NumPy-style function |
+
+## PyPI name vs. import name
+
+`uv add` takes the PyPI name; `import` takes the module name.
+
+| `uv add ...` | `import ...` |
+| --- | --- |
+| `scikit-learn` | `sklearn` |
+| `scikit-image` | `skimage` |
+| `scikit-rf` | `skrf` |
+| `scikit-fem` | `skfem` |
+| `PyNiteFEA` | `Pynite` |
+| `openseespy` | `openseespy.opensees` |
+| `control` | `control` (project is called python-control) |
+| `roboticstoolbox-python` | `roboticstoolbox` |
+| `TotalSegmentator` | `totalsegmentator` |
+| `torch` | `torch` (project is called PyTorch) |
+| `jupyterlab` | run with `uv run jupyter lab` |
+| `pyinstaller` | run with `uv run pyinstaller` |
+
+## More curated lists
+
+- [awesome-scientific-python](https://github.com/rossant/awesome-scientific-python)
+- [awesome-scientific-computing](https://github.com/nschloe/awesome-scientific-computing)
+- [awesome-python: Science](https://github.com/vinta/awesome-python#science)
+- [awesome-python-chemistry](https://github.com/lmmentel/awesome-python-chemistry)
diff --git a/README.md b/README.md
index 47ee6d4..6b44ae3 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,21 @@ You get:
acceleration and velocity three different ways (closed-form, numerical
integration of constant acceleration, and numerical integration of a
time-varying acceleration).
-- **`library/`** — one well-documented class per engineering domain
- (dynamics, statics, thermo, fluids, materials, gears, electrical,
- mechatronics, modal/vibration, controls), a set of radiology /
- medical-imaging classes, and a `Utility` class for files and dialogs.
+- **`fpc/`** — an installable package with one well-documented class per
+ engineering domain (dynamics, statics, thermo, fluids, materials, gears,
+ electrical, mechatronics, modal/vibration, controls), a set of radiology /
+ medical-imaging classes, `Design` (root finding and optimization),
+ `Properties` (cited reference tables with interpolation), and a `Utility`
+ class for files, tables, and dialogs.
+- **[`examples/`](examples/)** — one script per realistic problem (pump
+ sizing, beam selection, PID tuning, ...), each going from question to answer.
+- **[`templates/engineering_script.py`](templates/engineering_script.py)** —
+ a commented skeleton to copy for your own calculation.
+- **[`notebooks/`](notebooks/)** — Jupyter notebooks that walk through the library.
+- **Guides:** [from calculation to tool](docs/building-tools.md) (script ->
+ CLI -> GUI -> web app -> installed command) and
+ [units: floats or Pint?](docs/units.md).
+- **[`PACKAGES.md`](PACKAGES.md)** — which Python packages to use, by engineering field.
Every method uses keyword-only arguments and carries a docstring, so the code
itself doubles as a reference you can read and learn from.
@@ -106,8 +117,8 @@ Import a class, create an instance, and call its methods with **named
arguments** (this is the project's house style — calls read like sentences):
```python
-from library.dynamics import Dynamics
-from library.thermo import Thermo
+from fpc.dynamics import Dynamics
+from fpc.thermo import Thermo
dyn = Dynamics()
distance = dyn.displacement_constant_acceleration(
@@ -125,7 +136,7 @@ efficiency = thermo.carnot_efficiency(hot_temperature=800, cold_temperature=300)
You can also import everything at once:
```python
-from library import Dynamics, Statics, Fluids, Controls
+from fpc import Dynamics, Statics, Fluids, Controls
```
### Creating and calling objects from lists and dictionaries
@@ -134,7 +145,7 @@ A common pattern: build several objects, store them in a container, then loop
over them. This is how you scale from one calculation to a whole study.
```python
-from library.dynamics import Dynamics
+from fpc.dynamics import Dynamics
dyn = Dynamics()
@@ -152,18 +163,18 @@ for case in launches:
# --- A dictionary that maps a name to a configured object/result ---
materials = {
- "steel": 200e9, # Young's modulus [Pa]
- "aluminum": 69e9,
+ "steel": 200e9, # Young's modulus [Pa]
+ "aluminum": 69e9,
"titanium": 114e9,
}
for name, modulus in materials.items():
- print(f"{name:>9}: E = {modulus/1e9:.0f} GPa")
+ print(f"{name:>9}: E = {modulus / 1e9:.0f} GPa")
```
### Conditional flow control and loops
```python
-from library.fluids import Fluids
+from fpc.fluids import Fluids
fluids = Fluids()
re = fluids.reynolds_number(density=1000, velocity=2, length=0.05, viscosity=1e-3)
@@ -179,8 +190,7 @@ print(f"Re = {re:.0f} -> {regime}")
# while loop: increase velocity until the flow turns turbulent
velocity = 0.01
-while fluids.reynolds_number(density=1000, velocity=velocity,
- length=0.05, viscosity=1e-3) < 4000:
+while fluids.reynolds_number(density=1000, velocity=velocity, length=0.05, viscosity=1e-3) < 4000:
velocity += 0.01
print(f"Flow becomes turbulent at about {velocity:.2f} m/s")
```
@@ -192,8 +202,8 @@ and for telling other functions *how* to behave (sorting, mapping, filtering).
```python
# 1. A throwaway formula you can call like any function
-kinetic_energy = lambda mass, velocity: 0.5 * mass * velocity ** 2
-print(kinetic_energy(2.0, 3.0)) # -> 9.0
+kinetic_energy = lambda mass, velocity: 0.5 * mass * velocity**2
+print(kinetic_energy(2.0, 3.0)) # -> 9.0
# 2. As a sort key: order parts by stress, highest first
parts = [("bracket", 120e6), ("pin", 240e6), ("plate", 80e6)]
@@ -201,34 +211,34 @@ parts.sort(key=lambda part: part[1], reverse=True)
# 3. With map/filter: convert a list of Celsius temps to Kelvin
celsius = [0, 25, 100]
-kelvin = list(map(lambda c: c + 273.15, celsius)) # [273.15, 298.15, 373.15]
+kelvin = list(map(lambda c: c + 273.15, celsius)) # [273.15, 298.15, 373.15]
```
---
## Modelling things with units: the `Beam` dataclass
-`library/beam.py` shows how to model a real object as a `@dataclass` whose
+`fpc/beam.py` shows how to model a real object as a `@dataclass` whose
fields carry **physical units** (via [Pint](https://pint.readthedocs.io/)).
A `Beam` knows its own geometry and computes its structural behaviour — and the
units make the math self-checking.
```python
-from library.units import Q_ # Q_(value, "unit") builds a quantity
-from library.beam import Beam
+from fpc.units import Q_ # Q_(value, "unit") builds a quantity
+from fpc.beam import Beam
beam = Beam(
length=Q_(2.0, "m"),
width=Q_(50, "mm"),
height=Q_(100, "mm"),
- modulus=Q_(200, "GPa"), # defaults to structural steel if omitted
+ modulus=Q_(200, "GPa"), # defaults to structural steel if omitted
)
-beam.moment_of_inertia.to("mm**4") # 4.17e6 mm^4
-beam.mass.to("kg") # 78.5 kg
-beam.max_deflection_point_load(load=Q_(5, "kN")).to("mm") # 1.0 mm
+beam.moment_of_inertia.to("mm**4") # 4.17e6 mm^4
+beam.mass.to("kg") # 78.5 kg
+beam.max_deflection_point_load(load=Q_(5, "kN")).to("mm") # 1.0 mm
beam.max_bending_stress_point_load(load=Q_(5, "kN")).to("MPa") # 30 MPa
-beam.euler_buckling_load().to("kN") # 514 kN
+beam.euler_buckling_load().to("kN") # 514 kN
```
Why units are worth it:
@@ -239,7 +249,7 @@ Beam(length=Q_(2, "s"), ...) # raises ValueError - a time is not a length
```
All quantities must come from the **one shared registry** in
-`library/units.py` (Pint cannot combine quantities from different registries).
+`fpc/units.py` (Pint cannot combine quantities from different registries).
## Simulation & data fitting (SciPy)
@@ -254,13 +264,17 @@ Two SciPy workhorses show up where formulas run out:
measured vibration and returns the natural frequency and damping ratio.
```python
-from library.modal import Modal
+from fpc.modal import Modal
+
modal = Modal()
# Simulate a known oscillator...
t, x = modal.simulate_free_vibration(
- mass=2.0, stiffness=800.0, damping=8.0,
- initial_displacement=0.01, duration=5.0,
+ mass=2.0,
+ stiffness=800.0,
+ damping=8.0,
+ initial_displacement=0.01,
+ duration=5.0,
)
# ...then fit the response and recover the inputs (~3.18 Hz, zeta ~0.10).
modal.fit_damped_response(time=t, displacement=x, guess_frequency_hz=3.0)
@@ -269,37 +283,85 @@ modal.fit_damped_response(time=t, displacement=x, guess_frequency_hz=3.0)
That simulate-then-fit round-trip is checked in the test suite: the fit must
return the parameters the simulation started with.
+## Design, reference data & tables
+
+Analysis asks "how much does this beam deflect?"; design asks "what beam
+deflects no more than 5 mm?". These pieces answer design questions:
+
+```python
+from fpc import Design, Fluids, Properties, Utility
+
+# Root finding (scipy.optimize.brentq): solve equations with no algebraic answer.
+Fluids().colebrook_friction_factor(reynolds=1e5, relative_roughness=1e-4) # 0.0185
+Design().rectangular_beam_height_for_deflection(
+ load=5000,
+ length=2.0,
+ modulus=200e9,
+ width=0.05,
+ max_deflection=0.001,
+) # 0.100 m
+
+# Constrained optimization (scipy.optimize.minimize, SLSQP): lightest beam
+# that satisfies stress, deflection, and aspect-ratio limits.
+Design().lightest_rectangular_beam(
+ load=5000,
+ length=2.0,
+ modulus=200e9,
+ density=7850,
+ allowable_stress=150e6,
+ max_deflection=0.005,
+) # {'width': 0.0199, 'height': 0.0795, 'mass': 24.8, 'active': ['deflection', 'aspect_ratio'], ...}
+
+# Reference tables (pandas) with interpolation; data and sources in fpc/data/.
+props = Properties()
+props.water_liquid(temperature_c=37, property="dynamic_viscosity_pa_s") # linear
+props.water_saturation(temperature_c=93, property="pressure_kpa", method="cubic")
+props.lightest_w_shape(required_sx_in3=30)["designation"] # 'W16x26'
+
+# Your own CSV/Excel files
+frame = Utility().read_table(file_path="test_log.xlsx")
+```
+
+[`examples/sensor_log_analysis.py`](examples/sensor_log_analysis.py) shows the
+full pandas workflow (clean, group, resample, filter, export to Excel), and
+[`examples/vectorization.py`](examples/vectorization.py) compares Python loops
+with NumPy arrays.
+
+Units: methods that take plain numbers expect **SI units** (the docstring
+says so for every parameter); use Pint at the boundaries where units are
+mixed. See [docs/units.md](docs/units.md).
+
## Radiology & medical imaging
Six classes cover the physics and image handling a radiology student
(radiologic technologist, resident, or medical physics student) works with
-every day. Run the guided tour with `uv run radiology_demo.py`. It prints one
-worked example per modality and saves `radiology.png`, which shows a real CT
+every day. Run the guided tour with `uv run examples/radiology_demo.py`. It prints one
+worked example per modality and saves `examples/output/radiology.png`, which shows a real CT
slice in abdomen, lung, and bone windows.
| Class | Module | What it covers |
| --- | --- | --- |
-| `Radiation` | `library/radiation.py` | Inverse square law, attenuation (mu, HVL, TVL, shielding), photon energy, radioactive and effective half-life, equivalent/effective dose with ICRP 103 weighting factors |
-| `Radiography` | `library/radiography.py` | mAs, 15% rule, exposure-maintenance (SID) formula, grid conversion, heat units, magnification, geometric unsharpness, detector Nyquist limit, deviation index |
-| `CT` | `library/ct.py` | Hounsfield units, DICOM rescale, window width/level with presets, rough HU tissue lookup, DLP and effective dose estimate |
-| `MRI` | `library/mri.py` | Larmor frequency, T1 recovery, T2 decay, spin-echo signal, T1/T2/PD weighting from TR/TE |
-| `Ultrasound` | `library/ultrasound.py` | Wavelength, echo depth, acoustic impedance and reflection, attenuation, axial resolution, PRF depth limit, Doppler shift and velocity |
-| `Dicom` | `library/dicom.py` | Read DICOM with pydicom, summarize technical tags, pixels in HU, pixel spacing, basic de-identification, PNG export |
+| `Radiation` | `fpc/radiation.py` | Inverse square law, attenuation (mu, HVL, TVL, shielding), photon energy, radioactive and effective half-life, equivalent/effective dose with ICRP 103 weighting factors |
+| `Radiography` | `fpc/radiography.py` | mAs, 15% rule, exposure-maintenance (SID) formula, grid conversion, heat units, magnification, geometric unsharpness, detector Nyquist limit, deviation index |
+| `CT` | `fpc/ct.py` | Hounsfield units, DICOM rescale, window width/level with presets, rough HU tissue lookup, DLP and effective dose estimate |
+| `MRI` | `fpc/mri.py` | Larmor frequency, T1 recovery, T2 decay, spin-echo signal, T1/T2/PD weighting from TR/TE |
+| `Ultrasound` | `fpc/ultrasound.py` | Wavelength, echo depth, acoustic impedance and reflection, attenuation, axial resolution, PRF depth limit, Doppler shift and velocity |
+| `Dicom` | `fpc/dicom.py` | Read DICOM with pydicom, summarize technical tags, pixels in HU, pixel spacing, basic de-identification, PNG export |
```python
-from pydicom.data import get_testdata_file # sample images, no patient data
-from library import CT, Dicom, Radiography, Radiation
+from pydicom.data import get_testdata_file # sample images, no patient data
+from fpc import CT, Dicom, Radiography, Radiation
radiation = Radiation()
-radiation.effective_dose(organ_equivalent_doses={"thyroid": 10.0}) # -> 0.4 mSv
+radiation.effective_dose(organ_equivalent_doses={"thyroid": 10.0}) # -> 0.4 mSv
Radiography().exposure_maintenance_mas(original_mas=10, original_sid=100, new_sid=180) # 32.4
dicom, ct = Dicom(), CT()
ds = dicom.read(path=get_testdata_file("CT_small.dcm"))
-hu = dicom.pixel_array_in_units(dataset=ds) # stored values -> Hounsfield units
-lung = ct.apply_window(hounsfield=hu, preset="lung") # whole slice windowed in one call
-safe = dicom.anonymize(dataset=ds) # blank PHI before sharing
+hu = dicom.pixel_array_in_units(dataset=ds) # stored values -> Hounsfield units
+lung = ct.apply_window(hounsfield=hu, preset="lung") # whole slice windowed in one call
+safe = dicom.anonymize(dataset=ds) # blank PHI before sharing
```
> **Teaching use only.** These are textbook calculations for learning. They
@@ -308,21 +370,8 @@ safe = dicom.anonymize(dataset=ds) # blank PHI before sharing
> for those. `Dicom.anonymize` removes common direct identifiers only, which
> is *not* full de-identification under DICOM PS3.15 or HIPAA.
-### Python packages for medical imaging
-
-`pydicom` is already installed. Add the others as you need them with `uv add `.
-
-| Package | Use it for |
-| --- | --- |
-| [pydicom](https://pydicom.github.io/) | Read, edit, and write DICOM files (installed) |
-| [SimpleITK](https://simpleitk.org/) | 3-D volumes, resampling, registration, segmentation; reads whole DICOM series |
-| [nibabel](https://nipy.org/nibabel/) | NIfTI and other neuroimaging formats (research MRI) |
-| [scikit-image](https://scikit-image.org/) | Filtering, thresholding, edge detection, Radon transform / CT reconstruction demos |
-| [highdicom](https://highdicom.readthedocs.io/) | Create DICOM segmentations, structured reports, and annotations |
-| [pynetdicom](https://pydicom.github.io/pynetdicom/) | Talk to a PACS (C-FIND / C-MOVE / C-STORE) |
-| [MONAI](https://monai.io/) | Deep learning for medical imaging (built on PyTorch) |
-| [TotalSegmentator](https://github.com/wasserth/TotalSegmentator) | Pretrained automatic segmentation of 100+ structures on CT/MR |
-| [pyradiomics](https://pyradiomics.readthedocs.io/) | Extract quantitative radiomic features from images + masks |
+Python packages for medical imaging (SimpleITK, nibabel, MONAI, ...) are
+listed in [PACKAGES.md](PACKAGES.md#medical-imaging).
**Learning resources:** [Radiopaedia](https://radiopaedia.org/) (cases and
physics articles), [pydicom user guide](https://pydicom.github.io/pydicom/stable/tutorials/index.html),
@@ -339,7 +388,7 @@ demonstrates `pytest.approx` (comparing floats), `@pytest.mark.parametrize`
(table-driven tests), and the `tmp_path` fixture (temporary files).
```bash
-uv run pytest # run everything (40+ tests)
+uv run pytest # run everything
uv run pytest tests/test_dynamics.py # run one file
uv run pytest -k projectile # run tests whose name matches "projectile"
```
@@ -353,21 +402,21 @@ MATLAB's figures. See `plot_motion()` in `main.py` for a worked example.
## Python packages for science, engineering & data work
+The ten to know first. **[PACKAGES.md](PACKAGES.md)** has the full list,
+organized by engineering field (structural, thermal/fluids, electrical,
+controls, CAD, optimization, medical imaging, tool building, performance).
+
| Package | Use it for |
| --- | --- |
| [NumPy](https://numpy.org/) | Fast N-dimensional arrays and vectorised math — the foundation of everything below |
-| [SciPy](https://scipy.org/) | Integration, optimization, linear algebra, signal processing, statistics |
+| [SciPy](https://scipy.org/) | Integration, optimization, root finding, interpolation, signal processing, statistics |
| [Matplotlib](https://matplotlib.org/) | Plotting and figures (the MATLAB-style `pyplot` interface) |
| [pandas](https://pandas.pydata.org/) | Tabular data, time series, CSV/Excel I/O |
| [SymPy](https://www.sympy.org/) | Symbolic math / computer algebra (a free Maple/Mathematica) |
-| [Plotly](https://plotly.com/python/) | Interactive, web-ready charts and dashboards |
-| [Seaborn](https://seaborn.pydata.org/) | Statistical plotting built on Matplotlib |
-| [scikit-learn](https://scikit-learn.org/) | Classic machine learning (regression, clustering, classification) |
-| [Jupyter](https://jupyter.org/) | Notebooks that mix code, math, plots, and prose |
| [Pint](https://pint.readthedocs.io/) | Physical units and automatic unit conversion |
+| [Jupyter](https://jupyter.org/) | Notebooks that mix code, math, plots, and prose |
| [CoolProp](http://www.coolprop.org/) | Thermophysical fluid properties for thermo/HVAC work |
-| [control](https://python-control.readthedocs.io/) | Control-systems design (a free Control System Toolbox) |
-| [CadQuery](https://cadquery.readthedocs.io/) | Parametric 3D CAD modelling in Python |
+| [python-control](https://python-control.readthedocs.io/) | Control-systems design (a free Control System Toolbox) |
| [pytest](https://docs.pytest.org/) | Testing framework to verify your calculations stay correct |
Add any of them with `uv add `.
@@ -398,37 +447,69 @@ Add any of them with `uv add `.
---
+## Use this library in your own project
+
+`fpc` is an installable package, so your own scripts do not have to live in
+this repository.
+
+```bash
+# In your own uv project
+uv add git+https://github.com/kovirlabs/first-principles-code
+
+# Or with pip, from a local clone (editable: your edits take effect immediately)
+pip install -e path/to/first-principles-code
+```
+
+```python
+from fpc import Fluids, Properties
+```
+
+Installing also gives you the `fpc` command
+(`fpc pipe-flow --flow-l-s 12 --diameter-mm 102.3 --length-m 120`; see
+[docs/building-tools.md](docs/building-tools.md)). To pin a version, add
+`@` to the git URL.
+
+## Developer tooling
+
+| Command | What it does |
+| --- | --- |
+| `uv sync --all-groups` | Install everything: runtime, dev tools, notebooks, docs, apps |
+| `uv run pytest` | Tests, including running every example and notebook |
+| `uv run ruff check .` / `uv run ruff format .` | Lint / auto-format |
+| `uv run mypy` | Type-check the `fpc` package |
+| `uv run pre-commit install` | Run ruff and pytest automatically before each commit |
+| `uv run --group docs mkdocs serve` | Preview the API docs site, generated from the docstrings |
+
+CI runs all of these on every pull request; the docs site is published to
+GitHub Pages from `main`.
+
## Project layout
```
first-principles-code/
├── main.py # displacement demo (start here)
├── gui.py # MATLAB-style console GUI (uv run gui.py)
-├── radiology_demo.py # medical-imaging tour (uv run radiology_demo.py)
-├── library/ # one class per engineering domain
+├── fpc/ # the library: one class per engineering domain
│ ├── __init__.py # re-exports every class
│ ├── units.py # shared Pint unit registry (ureg, Q_)
│ ├── beam.py # unit-aware Beam dataclass
-│ ├── utility.py # JSON/CSV/file-picker helpers
-│ ├── dynamics.py
-│ ├── statics.py
-│ ├── modal.py
-│ ├── thermo.py
-│ ├── fluids.py
-│ ├── material.py
-│ ├── gear.py
-│ ├── electrical.py
-│ ├── mechatronics.py
-│ ├── controls.py
-│ ├── radiation.py # radiology: physics, decay, dose
-│ ├── radiography.py # radiology: x-ray technique & geometry
-│ ├── ct.py # radiology: Hounsfield units & windowing
-│ ├── mri.py # radiology: relaxation & contrast
-│ ├── ultrasound.py # radiology: pulse-echo & Doppler
-│ └── dicom.py # radiology: DICOM I/O with pydicom
+│ ├── design.py # root finding & constrained optimization
+│ ├── properties.py # reference-table lookups with interpolation
+│ ├── utility.py # JSON/CSV/Excel/file-picker helpers
+│ ├── cli.py # the `fpc` terminal command
+│ ├── data/ # cited reference tables (CSV) + sources
+│ ├── dynamics.py, statics.py, modal.py, thermo.py, fluids.py,
+│ ├── material.py, gear.py, electrical.py, mechatronics.py, controls.py
+│ └── radiation.py, radiography.py, ct.py, mri.py, ultrasound.py, dicom.py
+├── examples/ # worked problems, one script each (+ tools/ ladder)
+├── templates/ # copy-and-adapt starting script
+├── notebooks/ # Jupyter notebooks
+├── docs/ # guides + API docs site sources (mkdocs.yml)
├── tests/ # pytest suite (uv run pytest)
-├── pyproject.toml # project metadata & dependencies
-├── SPEC.md # full project design spec
+├── pyproject.toml # metadata, dependencies, tool config
+├── PACKAGES.md # Python packages by engineering field
+├── SPEC.md # original project design spec
+├── ROADMAP.md # improvement backlog
├── CONTRIBUTOR.md # how to contribute
├── LICENSE # Apache 2.0
└── README.md # you are here
@@ -447,13 +528,13 @@ uv run gui.py
The result prints to the **Session log** and is recorded in the **Workspace**
table — just like MATLAB's `ans`/workspace.
- The interpreter comes preloaded with `math`, `numpy as np`, and a ready
- instance of every library class (`dyn`, `statics`, `thermo`, `fluids`, …), so
+ instance of every library class (`dyn`, `statics`, `thermo`, `fluids`, `design`, `props`, …), so
you can compute immediately:
`dyn.projectile_range(speed=20, angle_deg=45)`.
- **Open Script** loads a `.py` file into the command box; **Run Script** runs
the whole box at once.
- **Export CSV** / **Export JSON** save every recorded result to disk (via
- `library.Utility`).
+ `fpc.Utility`).
> The GUI needs a graphical display. The evaluation engine (the `Session`
> class) is separated from the Tk widgets, so it can be imported and scripted
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..0e4b1b4
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,156 @@
+
+
+
+# Roadmap
+
+Planned improvements to make this project a launching point for engineers to
+write their own scripts and tools. It complements the "Python concepts to
+teach" list in [SPEC.md](SPEC.md). Work items here are about the project as a
+whole: structure, tooling, docs, and discoverability.
+
+**How to use this file:** check a box when an item is merged, and add the PR
+number next to it. Items marked **(decision needed)** wait on the maintainer's
+call before work starts. Keep items in priority order within each section.
+
+_Last reviewed: 2026-11-06. Sections 0-8 were built in #6 (branch
+`feat/roadmap-buildout`); see "Follow-ups" for what remains._
+
+---
+
+## 0. Housekeeping (quick, low risk)
+
+- [x] **Update `CLAUDE.md`.** It still says `fpc/` is empty, `main.py` is
+ hello-world, and there are no tests or README. Rewrite "Current state" to
+ match reality (14 domain classes + 6 radiology classes, 109 tests, CI, GUI).
+- [x] **Stop tracking `.idea/`.** `git rm -r --cached .idea` and add `.idea/`
+ to `.gitignore` (PyCharm settings are per-user).
+- [x] **Fix stale README numbers.** "40+ tests" -> current count (109), or
+ drop the number so it can't go stale.
+
+## 1. Package discovery: `PACKAGES.md`
+
+- [x] Create `PACKAGES.md`, a curated list of Python packages **organized by
+ engineering field**, one line each saying *when* to use it:
+ - Core stack (NumPy, SciPy, pandas, Matplotlib, SymPy, Pint, Jupyter)
+ - Mechanical and structural (e.g. sectionproperties, anastruct, PyNite,
+ OpenSeesPy, FEniCSx / scikit-fem)
+ - Thermal and fluids (CoolProp, Cantera, fluids, ht)
+ - Electrical and signals (scipy.signal, PySpice, scikit-rf, lcapy)
+ - Controls and robotics (python-control, harold, roboticstoolbox-python)
+ - CAD, geometry, and meshing (CadQuery, build123d, trimesh, gmsh, PyVista)
+ - Optimization and uncertainty (scipy.optimize, Pyomo, CVXPY, SALib, pymoo)
+ - Data and ML (scikit-learn, statsmodels, PyTorch)
+ - Medical imaging (move the README table here: pydicom, SimpleITK, ...)
+ - Tool building (argparse, Typer, Streamlit, tkinter, PyInstaller)
+ - Performance (Numba, Cython, JAX)
+- [x] End with links to existing curated lists:
+ [awesome-scientific-python](https://github.com/rossant/awesome-scientific-python),
+ [awesome-scientific-computing](https://github.com/nschloe/awesome-scientific-computing),
+ [awesome-python: Science](https://github.com/vinta/awesome-python#science),
+ [awesome-python-chemistry](https://github.com/lmmentel/awesome-python-chemistry).
+- [x] Shrink the README package table to the top ~10 and link to `PACKAGES.md`.
+- [x] Verify every package is maintained and the link resolves before
+ merging (check last release date on PyPI).
+
+## 2. Starting points for users' own work
+
+- [x] **Script template.** Add `templates/engineering_script.py`: a skeleton
+ with an inputs dict, calculation, results printout, plot, and CSV export,
+ heavily commented, meant to be copied and adapted.
+- [x] **`examples/` folder of worked problems.** One short, self-contained
+ script per realistic problem, each going from question to answer. Candidates:
+ pump sizing, beam selection, PID tuning, heat-exchanger estimate, motor
+ sizing, shielding-thickness check. Move `radiology_demo.py` here (update the
+ README and GUI references).
+- [x] **"From calculation to tool" guide.** A doc (e.g. `docs/building-tools.md`)
+ walking one calculation up the ladder: script -> command line (`argparse`)
+ -> the existing tkinter GUI -> web app (Streamlit) -> installable package.
+- [x] **Notebooks.** Add `notebooks/` with a few Jupyter (or marimo) notebooks
+ that call the library. Add `jupyter` / `marimo` as an optional dependency
+ group.
+
+## 3. Make the library reusable outside this repo
+
+- [x] **Rename the `library` package to `fpc`.** (Decided: `fpc`.) The name is generic
+ and collides with other code. Candidate: `fpc` (first-principles-code).
+ This breaks every existing import, so do it in one PR that updates all
+ imports, tests, README, GUI, and demos.
+- [x] Add a `[build-system]` section to `pyproject.toml` so the package can
+ be installed with `uv add git+https://github.com/kovirlabs/first-principles-code`
+ or `pip install -e .`.
+- [x] Document "use this library in your own project" in the README.
+
+## 4. Finish the SPEC roadmap items real tools depend on
+
+Tracked in detail in [SPEC.md](SPEC.md) under "Python concepts to teach".
+
+- [x] **pandas data handling.** Read a sensor log / property table from
+ CSV and Excel, filter, group, export. Add `pandas` (and `openpyxl`).
+- [x] **Root finding.** `scipy.optimize.brentq` / `fsolve`, e.g. solve
+ Colebrook for friction factor or find a beam size that meets a deflection
+ limit.
+- [x] **Interpolation.** `scipy.interpolate` / `np.interp` on a property table
+ (steam tables, material data).
+- [x] **Optimization for design.** `scipy.optimize.minimize` with bounds and
+ constraints, e.g. lightest beam that meets stress and deflection limits.
+- [x] **NumPy vectorization lesson.** A side-by-side loop vs. vectorized
+ example with timing, plus boolean masking.
+
+## 5. Units consistency
+
+- [x] **Decided: document the split** (`docs/units.md`): plain SI floats
+ for the math, Pint at the boundaries. Pint is used in `Beam` and partly in `Thermo`;
+ every other class takes plain floats. Either extend Pint support across the
+ library or document clearly when to use each approach (plain floats for
+ quick formulas, Pint for anything with mixed or error-prone units).
+
+## 6. Tooling and professional habits
+
+- [x] **Ruff** for linting and formatting: add to the dev group, configure in
+ `pyproject.toml`, run in CI.
+- [x] **Type checking** (mypy or pyright) on `fpc/`, starting non-strict.
+- [x] **API docs site** generated from the docstrings (mkdocs-material +
+ mkdocstrings), published with GitHub Pages.
+- [x] **pre-commit** hooks so ruff and tests run before each commit.
+
+## 7. Reference data
+
+- [x] (Placed in `fpc/data/` so the tables ship with the installed package.)
+ Add a `data/` folder with CSV/JSON tables (material properties, standard
+ steel sections, fluid properties) and load them through `Utility` or pandas
+ instead of hard-coding constants. Cite the source of every table.
+
+## 8. Contributor experience
+
+- [x] "Add a new domain class" checklist in `CONTRIBUTOR.md` (module, class,
+ `__init__` export, GUI preload, tests, README row).
+- [x] GitHub issue templates: bug report, new domain request, new example.
+
+---
+
+## Follow-ups
+
+Items found while building sections 0-8.
+
+- [ ] **Enable GitHub Pages:** Settings -> Pages -> Source: "GitHub Actions",
+ so `.github/workflows/docs.yml` can publish the docs site.
+- [ ] **Verify `fpc/data/materials.csv` against MatWeb/ASM data sheets.** The
+ values are typical published figures but were entered from reference
+ knowledge, not copied from a fetched source.
+- [ ] **Verify `fpc/data/w_shapes.csv` against the AISC Shapes Database v16
+ spreadsheet.** A test confirms every row is physically self-consistent
+ (Sx = Ix/(d/2), weight = 3.4 x A), which catches typos but not a
+ consistently wrong row. Extend the table while doing this.
+- [ ] **Plan the move off Material for MkDocs**, which reaches end of life on
+ 2027-05-05. Its successor, Zensical, reads the existing `mkdocs.yml`.
+- [ ] **Tighten mypy** module by module as type hints are added
+ (`disallow_untyped_defs` per module).
+- [ ] Consider an InSpice row in place of PySpice in `PACKAGES.md` once
+ InSpice has a longer release history.
+
+## Done
+
+_Move items here with their PR number when merged._
+
+- [x] Radiology / medical-imaging classes: `Radiation`, `Radiography`, `CT`,
+ `MRI`, `Ultrasound`, `Dicom` (#4)
diff --git a/SPEC.md b/SPEC.md
index 651a918..3fce6f5 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -10,7 +10,7 @@ A Github repo that provides a template Python script and class library that func
### main.py
The primary script with some basic imports for math and scipy. It should demonstrate how to calculate displacement from a known acceleration and velocity as a function of time.
-### ./library
+### ./fpc
A direcotry that will contain all classes.
Initial class list:
@@ -59,29 +59,32 @@ engineer move from "a single formula" to "a real workflow they can compute,
visualize, and trust." Listed roughly in order of payoff. Items marked
**(done)** are already in the project.
-1. **NumPy array thinking / vectorization** — operating on whole arrays instead
+1. **NumPy array thinking / vectorization (done)** — see
+ `examples/vectorization.py`; operating on whole arrays instead
of looping element by element; broadcasting and boolean masking
(`stress[stress > yield_strength]`). The biggest shift coming from MATLAB.
2. **Plotting with matplotlib (done)** — turning result arrays into figures
(`main.py` plots position/velocity/acceleration vs time and saves a PNG).
The figure is usually the real engineering deliverable.
-3. **The wider SciPy toolbox (partly done)** — general ODE simulation with
+3. **The wider SciPy toolbox (done)** — general ODE simulation with
`integrate.solve_ivp` (`Dynamics.simulate_projectile_drag`,
`Modal.simulate_free_vibration`) and curve fitting with `optimize.curve_fit`
- (`Modal.fit_damped_response`) are implemented. Still open: root finding
- (`optimize.brentq`, `fsolve`) and interpolation (`scipy.interpolate`).
+ (`Modal.fit_damped_response`), root finding (`Fluids.colebrook_friction_factor`,
+ `Design.rectangular_beam_height_for_deflection`), constrained optimization
+ (`Design.lightest_rectangular_beam`), and interpolation (`Properties`).
4. **Physical units with Pint (done)** — quantities that carry units,
auto-convert, and raise if you add metres to seconds. The shared registry
- lives in `library/units.py`; `library/beam.py` uses it. Guards against
+ lives in `fpc/units.py`; `fpc/beam.py` uses it. Guards against
unit-mismatch errors that plain floats cannot catch.
5. **Type hints and dataclasses (done)** — annotate units/types on signatures
so the editor catches mistakes; model entities as `@dataclass` objects
- instead of passing loose floats. See `Beam` in `library/beam.py`.
+ instead of passing loose floats. See `Beam` in `fpc/beam.py`.
6. **Error handling and input validation (done)** — `raise ValueError(...)` for
bad inputs (wrong units, negative dimensions); see `Beam.__post_init__`.
7. **Testing with pytest (done)** — pure-function methods checked against known
textbook answers (`tests/`), so the library can be refactored fearlessly.
-8. **Data handling with pandas** — reading sensor logs / material-property
+8. **Data handling with pandas (done)** — `Properties`, `Utility.read_table`,
+ and `examples/sensor_log_analysis.py`; reading sensor logs / material-property
tables from CSV/Excel, filtering, grouping, and exporting results.
Smaller but useful: `@property` / `@staticmethod` for richer class design,
diff --git a/docs/api/beam.md b/docs/api/beam.md
new file mode 100644
index 0000000..ad242df
--- /dev/null
+++ b/docs/api/beam.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.beam
diff --git a/docs/api/cli.md b/docs/api/cli.md
new file mode 100644
index 0000000..693b4f0
--- /dev/null
+++ b/docs/api/cli.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.cli
diff --git a/docs/api/controls.md b/docs/api/controls.md
new file mode 100644
index 0000000..e7770be
--- /dev/null
+++ b/docs/api/controls.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.controls
diff --git a/docs/api/ct.md b/docs/api/ct.md
new file mode 100644
index 0000000..23b1741
--- /dev/null
+++ b/docs/api/ct.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.ct
diff --git a/docs/api/design.md b/docs/api/design.md
new file mode 100644
index 0000000..ad3b638
--- /dev/null
+++ b/docs/api/design.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.design
diff --git a/docs/api/dicom.md b/docs/api/dicom.md
new file mode 100644
index 0000000..b917816
--- /dev/null
+++ b/docs/api/dicom.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.dicom
diff --git a/docs/api/dynamics.md b/docs/api/dynamics.md
new file mode 100644
index 0000000..ae34ebd
--- /dev/null
+++ b/docs/api/dynamics.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.dynamics
diff --git a/docs/api/electrical.md b/docs/api/electrical.md
new file mode 100644
index 0000000..e8007c4
--- /dev/null
+++ b/docs/api/electrical.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.electrical
diff --git a/docs/api/fluids.md b/docs/api/fluids.md
new file mode 100644
index 0000000..14d8332
--- /dev/null
+++ b/docs/api/fluids.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.fluids
diff --git a/docs/api/gear.md b/docs/api/gear.md
new file mode 100644
index 0000000..aa31168
--- /dev/null
+++ b/docs/api/gear.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.gear
diff --git a/docs/api/material.md b/docs/api/material.md
new file mode 100644
index 0000000..04c7134
--- /dev/null
+++ b/docs/api/material.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.material
diff --git a/docs/api/mechatronics.md b/docs/api/mechatronics.md
new file mode 100644
index 0000000..a6906bd
--- /dev/null
+++ b/docs/api/mechatronics.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.mechatronics
diff --git a/docs/api/modal.md b/docs/api/modal.md
new file mode 100644
index 0000000..1276a35
--- /dev/null
+++ b/docs/api/modal.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.modal
diff --git a/docs/api/mri.md b/docs/api/mri.md
new file mode 100644
index 0000000..1c65232
--- /dev/null
+++ b/docs/api/mri.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.mri
diff --git a/docs/api/properties.md b/docs/api/properties.md
new file mode 100644
index 0000000..9b44eb8
--- /dev/null
+++ b/docs/api/properties.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.properties
diff --git a/docs/api/radiation.md b/docs/api/radiation.md
new file mode 100644
index 0000000..fa4f407
--- /dev/null
+++ b/docs/api/radiation.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.radiation
diff --git a/docs/api/radiography.md b/docs/api/radiography.md
new file mode 100644
index 0000000..e3445b3
--- /dev/null
+++ b/docs/api/radiography.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.radiography
diff --git a/docs/api/statics.md b/docs/api/statics.md
new file mode 100644
index 0000000..2e64730
--- /dev/null
+++ b/docs/api/statics.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.statics
diff --git a/docs/api/thermo.md b/docs/api/thermo.md
new file mode 100644
index 0000000..5b74e68
--- /dev/null
+++ b/docs/api/thermo.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.thermo
diff --git a/docs/api/ultrasound.md b/docs/api/ultrasound.md
new file mode 100644
index 0000000..5b4f692
--- /dev/null
+++ b/docs/api/ultrasound.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.ultrasound
diff --git a/docs/api/units.md b/docs/api/units.md
new file mode 100644
index 0000000..0ca0b4e
--- /dev/null
+++ b/docs/api/units.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.units
diff --git a/docs/api/utility.md b/docs/api/utility.md
new file mode 100644
index 0000000..e0ca023
--- /dev/null
+++ b/docs/api/utility.md
@@ -0,0 +1,4 @@
+
+
+
+::: fpc.utility
diff --git a/docs/building-tools.md b/docs/building-tools.md
new file mode 100644
index 0000000..2cede84
--- /dev/null
+++ b/docs/building-tools.md
@@ -0,0 +1,120 @@
+
+
+
+# From calculation to tool
+
+You have a calculation that works. Other people want to use it. This guide
+takes **one calculation**, the pressure drop of water in a pipe, up five rungs
+of a ladder. Each rung is a working file you can run.
+
+| Rung | File | Who can use it | Effort |
+| --- | --- | --- | --- |
+| 1. Script | [`examples/tools/step1_script.py`](https://github.com/kovirlabs/first-principles-code/blob/main/examples/tools/step1_script.py) | You | Minutes |
+| 2. Command line | [`examples/tools/step2_cli.py`](https://github.com/kovirlabs/first-principles-code/blob/main/examples/tools/step2_cli.py) | Anyone with a terminal, other programs | + 20 lines |
+| 3. Desktop GUI | [`examples/tools/step3_gui.py`](https://github.com/kovirlabs/first-principles-code/blob/main/examples/tools/step3_gui.py) | Colleagues who avoid terminals | + 60 lines |
+| 4. Web app | [`examples/tools/step4_web_app.py`](https://github.com/kovirlabs/first-principles-code/blob/main/examples/tools/step4_web_app.py) | Anyone with a link | + 50 lines and a server |
+| 5. Installed command | [`fpc/cli.py`](https://github.com/kovirlabs/first-principles-code/blob/main/fpc/cli.py) + `[project.scripts]` | Anyone who installs the package | + 5 lines of config |
+
+**The rule that makes the ladder cheap:** the engineering lives in the library
+(`Fluids.pipe_flow`), and every rung is a thin front end that collects inputs,
+converts units, calls the library, and displays results. When the math changes,
+you fix it in one place and every front end gets the fix, and the library tests
+in `tests/` protect all five front ends at once.
+
+---
+
+## Rung 1: script
+
+```bash
+uv run examples/tools/step1_script.py
+```
+
+Inputs are constants at the top of the file. This is the right tool for a
+one-off answer and for exploring. You have outgrown it when you catch yourself
+editing the file to run a second case, or copying it to make `_v2`.
+
+For a fuller starting point (inputs dict, report, plot, CSV export), copy
+[`templates/engineering_script.py`](https://github.com/kovirlabs/first-principles-code/blob/main/templates/engineering_script.py).
+
+## Rung 2: command-line tool (`argparse`)
+
+```bash
+uv run examples/tools/step2_cli.py --help
+uv run examples/tools/step2_cli.py --flow-l-s 12 --diameter-mm 102.3 --length-m 120
+```
+
+`argparse` is in the standard library. You declare each input once (name, type,
+default, help text) and you get validation, error messages, and `--help` for
+free. Key habits shown in the file:
+
+- **Units in flag names** (`--diameter-mm`). Convert to SI immediately after parsing.
+- **`main(argv=None)`**: passing a list instead of the real command line makes
+ the tool testable without a subprocess (see `tests/test_cli.py`).
+- A CLI can be **scripted**: a shell loop or a batch file can run 200 cases.
+
+When the CLI grows many subcommands, [Typer](https://typer.tiangolo.com/)
+builds the same thing from type hints with less code.
+
+## Rung 3: desktop GUI (`tkinter`)
+
+```bash
+uv run examples/tools/step3_gui.py
+```
+
+`tkinter` ships with Python, so there is nothing to install. The form is built
+from a `FIELDS` dict: adding an input is one line. The essential GUI habit is
+**handling bad input**. Typed text might be `"abc"`, so the button handler
+catches `ValueError` and shows a message instead of crashing.
+
+The project's [`gui.py`](https://github.com/kovirlabs/first-principles-code/blob/main/gui.py) is the open-ended version: a MATLAB-style
+console where you type any expression. Use a form like this one when users
+need one specific calculation.
+
+## Rung 4: web app (Streamlit)
+
+```bash
+uv sync --group apps # one-time: installs streamlit
+uv run streamlit run examples/tools/step4_web_app.py
+```
+
+[Streamlit](https://streamlit.io/) reruns your script every time a widget
+changes, so the app reads like a plain script where widgets return values. It
+also makes it natural to show a curve (pressure drop across a range of flows)
+instead of one number.
+
+To share it, deploy to [Streamlit Community Cloud](https://streamlit.io/cloud)
+or an internal server. Check your organization's data policy before putting
+engineering data on a public host.
+
+## Rung 5: installed command (`[project.scripts]`)
+
+The final step turns the tool into a command that installs with the package.
+[`fpc/cli.py`](https://github.com/kovirlabs/first-principles-code/blob/main/fpc/cli.py) is rung 2 moved *inside* the package and extended
+with subcommands. One table in `pyproject.toml` registers it:
+
+```toml
+[project.scripts]
+fpc = "fpc.cli:main"
+```
+
+```bash
+uv sync # in this repo: the `fpc` command is now available
+uv run fpc --help
+uv run fpc pipe-flow --flow-l-s 12 --diameter-mm 102.3 --length-m 120
+uv run fpc water --temperature-c 37
+
+# Anywhere else: install it as a standalone tool
+uv tool install git+https://github.com/kovirlabs/first-principles-code
+fpc water --temperature-c 37
+```
+
+### Beyond the ladder
+
+- **A single `.exe` for people without Python:**
+ [PyInstaller](https://pyinstaller.org/) bundles a script and its
+ dependencies (`uvx pyinstaller --onefile examples/tools/step3_gui.py`).
+ Expect large files and test on the target operating system.
+- **Notebooks** ([`notebooks/`](https://github.com/kovirlabs/first-principles-code/tree/main/notebooks)) are a different kind of tool:
+ a calculation and its explanation in one shareable document.
+- **Reuse in another project:** see "Use this library in your own project" in
+ the README.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..46fefae
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,52 @@
+
+
+
+# First Principles Code
+
+A Python library of engineering calculations, written to teach. Every class
+covers one engineering domain, every method takes named arguments, and every
+docstring states its formula and the units of each parameter. The **API
+reference** in the menu is generated directly from those docstrings.
+
+## Install
+
+```bash
+# Work inside this repository
+git clone https://github.com/kovirlabs/first-principles-code
+cd first-principles-code
+uv sync
+
+# Or use the library from your own project
+uv add git+https://github.com/kovirlabs/first-principles-code
+```
+
+## Ten-second tour
+
+```python
+from fpc import Fluids, Properties, Design
+
+props = Properties()
+mu = props.water_liquid(temperature_c=37, property="dynamic_viscosity_pa_s")
+
+Fluids().colebrook_friction_factor(reynolds=1e5, relative_roughness=1e-4) # 0.0185
+
+Design().rectangular_beam_height_for_deflection(
+ load=5000,
+ length=2.0,
+ modulus=200e9,
+ width=0.05,
+ max_deflection=0.001,
+) # 0.100 m
+```
+
+## Where to go next
+
+- **Guides:** [units, floats or Pint?](units.md) and
+ [from calculation to tool](building-tools.md).
+- **Worked examples:** the
+ [`examples/`](https://github.com/kovirlabs/first-principles-code/tree/main/examples)
+ folder has one script per realistic problem: pump sizing, beam selection,
+ PID tuning, and more.
+- **Notebooks:** [`notebooks/`](https://github.com/kovirlabs/first-principles-code/tree/main/notebooks).
+- **Which package for my field?** See
+ [PACKAGES.md](https://github.com/kovirlabs/first-principles-code/blob/main/PACKAGES.md).
diff --git a/docs/units.md b/docs/units.md
new file mode 100644
index 0000000..b5aff73
--- /dev/null
+++ b/docs/units.md
@@ -0,0 +1,78 @@
+
+
+
+# Units: plain floats or Pint?
+
+Unit mistakes are among the most expensive errors in engineering. The
+project uses **two approaches on purpose**, and this page explains when to
+use each.
+
+| | Plain floats (SI) | Pint quantities |
+| --- | --- | --- |
+| Used by | Most classes: `Dynamics`, `Statics`, `Fluids`, `Thermo`, `Design`, `Gear`, ... | `Beam`, and anywhere you build `Q_(...)` yourself |
+| Looks like | `fluids.reynolds_number(density=998, velocity=2, length=0.05, viscosity=1e-3)` | `beam.max_deflection_point_load(load=Q_(5, "kN")).to("mm")` |
+| Speed | Fastest; works directly with NumPy and SciPy | Slower; some SciPy functions need plain numbers |
+| Catches mistakes | No. `length=50` (meant mm) silently gives a wrong answer | Yes. Adding metres to seconds raises an error |
+| Best for | Quick formulas, inner loops, solvers, optimization | Mixed unit systems, data from catalogues and drawings, input/output boundaries |
+
+## The rule
+
+> **Plain floats for the math. Pint at the edges.**
+
+1. **Library methods that take floats always expect SI base units**: metres,
+ kilograms, seconds, newtons, pascals, kelvin (or °C where the parameter
+ name says `_c`). The docstring states the unit of every parameter.
+ Exceptions are named in the parameter itself, such as `angle_deg`,
+ `motor_rpm`, `energy_kev`, `time_ms`, and the radiology classes, which
+ follow clinical conventions (cm, keV, mSv).
+2. **When inputs arrive in mixed or non-SI units** (a drawing in inches, a
+ catalogue in in³, a datasheet in ksi), wrap them in Pint and convert to SI
+ *once*, at the boundary, before calling the library.
+3. **When a result leaves your script** (a report, a plot label), convert to
+ the unit your reader expects with `.to(...)`.
+
+```python
+from fpc import Fluids, Q_
+
+# Inputs as they appear on the drawing / datasheet:
+diameter = Q_(4.026, "inch") # 4" Schedule 40 inner diameter
+flow = Q_(190, "gallon/minute")
+
+# Boundary: convert to SI floats once, then use the fast float API.
+result = Fluids().pipe_flow(
+ flow=flow.to("m**3/s").magnitude,
+ diameter=diameter.to("m").magnitude,
+ length=Q_(400, "ft").to("m").magnitude,
+ roughness=Q_(0.0018, "inch").to("m").magnitude,
+ density=998.2,
+ viscosity=1.0e-3,
+)
+
+# Boundary out: attach units again and report in the reader's units.
+print(Q_(result["pressure_drop"], "Pa").to("psi"))
+```
+
+See [`examples/beam_selection.py`](https://github.com/kovirlabs/first-principles-code/blob/main/examples/beam_selection.py)
+for a full example that mixes SI loads with a US-unit steel catalogue.
+
+## Habits that prevent unit bugs (with or without Pint)
+
+- **Put the unit in the name**: `length_m`, `flow_l_s`, `temperature_c`. The
+ examples, the template, and the CLI all do this.
+- **Convert once, at the boundary.** Never sprinkle `/ 1000` through the math.
+- **Absolute vs gauge pressure, and kelvin vs Celsius.** Gas laws and Carnot
+ efficiency need absolute values. Pint treats `degC` as an *offset* unit:
+ `Q_(20, "degC") * 2` raises an error because "twice 20 °C" is ambiguous.
+ Use `delta_degC` for temperature differences.
+- **Degrees vs radians.** Python's `math.sin` uses radians. Methods that take
+ degrees say so (`angle_deg`) and convert inside.
+- **Use one registry.** All Pint quantities must come from `fpc.units.ureg`
+ (or `fpc.Q_`). Quantities from different registries cannot be combined.
+
+## Why not Pint everywhere?
+
+Pint everywhere would catch more mistakes. It would also make every method
+slower, complicate SciPy calls (solvers and optimizers want plain arrays), and
+add noise to examples whose point is the physics. The split keeps the library
+fast and readable while still giving you a safety net where unit mistakes
+actually happen: at the boundary between your data and your math.
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..297566e
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,29 @@
+
+
+
+# Worked examples
+
+Each script answers one realistic engineering question from start to finish.
+Read the docstring at the top (QUESTION / APPROACH), run it, then change the
+inputs. Figures and tables go to `examples/output/` (git-ignored).
+
+```bash
+uv run examples/pump_sizing.py
+```
+
+| Script | Question | Python ideas it shows |
+| --- | --- | --- |
+| [`pump_sizing.py`](pump_sizing.py) | Which pipe size and pump for 12 L/s up 15 m? | Root finding (Colebrook), property interpolation, dict loops, system-curve plot |
+| [`beam_selection.py`](beam_selection.py) | Lightest W-beam for a 6 m span, 40 kN load? | Pint unit conversion (SI to US catalogue), pandas filtering and sorting |
+| [`pid_tuning.py`](pid_tuning.py) | Which PI gains settle fastest with less than 5% overshoot? | Grid search, NumPy boolean masks, `min(key=lambda ...)` |
+| [`heat_exchanger_estimate.py`](heat_exchanger_estimate.py) | Duty and area of a water-to-water exchanger? | Energy balance, fixed-point iteration, LMTD |
+| [`motor_sizing.py`](motor_sizing.py) | Which servo motor fits a ball-screw axis? | Motion profiles, RMS torque, screening a list of dicts with `all()` |
+| [`shielding_check.py`](shielding_check.py) | Lead vs concrete thickness for 0.1% transmission? | Attenuation, HVL/TVL, inverse square law |
+| [`sensor_log_analysis.py`](sensor_log_analysis.py) | Operating points and alarm times from a pump log? | pandas: read, clean, group, resample, mask, export to Excel |
+| [`vectorization.py`](vectorization.py) | Which of 1,000,000 load cases exceed yield? | Loops vs NumPy, broadcasting, boolean masking, timing |
+| [`radiology_demo.py`](radiology_demo.py) | Tour of the medical-imaging classes | pydicom, CT windowing, matplotlib subplots |
+
+`data/pump_sensor_log.csv` is **synthetic**, generated by
+`data/make_pump_sensor_log.py` with a fixed random seed.
+
+Starting your own problem? Copy [`templates/engineering_script.py`](../templates/engineering_script.py).
diff --git a/examples/beam_selection.py b/examples/beam_selection.py
new file mode 100644
index 0000000..5800bb3
--- /dev/null
+++ b/examples/beam_selection.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""beam_selection.py - pick a steel W-beam, with units checked by Pint.
+
+QUESTION
+ A 6 m simply supported floor beam carries a 40 kN point load at midspan.
+ Which is the lightest AISC W-shape that keeps bending stress under the
+ allowable and deflection under L/360?
+
+APPROACH
+ 1. Work in SI with Pint quantities: M = P*L/4, required Sx = M / Fb.
+ 2. The steel catalogue is in US units (in^3, in^4). Pint converts
+ required Sx to in^3 - no hand-typed conversion factors.
+ 3. Properties.lightest_w_shape() filters and sorts the table with pandas.
+ 4. Check deflection with that section's Ix; if it fails, step up to the
+ next-lightest section that passes both checks.
+
+This mixes unit systems on purpose: it is exactly the situation where Pint
+earns its keep (see docs/units.md).
+
+Simplification: Fb = 0.66*Fy is an allowable-stress shortcut for a laterally
+braced compact section. A real design follows AISC 360 in full.
+
+Run it: uv run examples/beam_selection.py
+"""
+
+from fpc import Q_, Properties
+
+props = Properties()
+
+# --- Inputs (each one carries its units) ----------------------------------
+span = Q_(6.0, "m")
+load = Q_(40.0, "kN")
+yield_strength = Q_(345, "MPa") # ASTM A992 steel, Fy = 50 ksi
+modulus = Q_(200, "GPa")
+deflection_limit = span / 360 # serviceability limit L/360
+
+
+def deflection(*, section):
+ """Midspan deflection P*L^3 / (48*E*I) for a section from the table."""
+ ix = Q_(section["ix_in4"], "in**4") # attach units to the catalogue value
+ return (load * span**3 / (48 * modulus * ix)).to("mm")
+
+
+def main():
+ allowable_stress = 0.66 * yield_strength
+ max_moment = load * span / 4
+ required_sx = (max_moment / allowable_stress).to("in**3")
+ print(f"Max moment M = {max_moment.to('kN*m'):.1f~P}")
+ print(f"Allowable stress Fb = {allowable_stress.to('MPa'):.0f~P}")
+ print(f"Required modulus Sx >= {required_sx:.1f~P}")
+ print(f"Deflection limit = {deflection_limit.to('mm'):.1f~P}\n")
+
+ # Strength check: the lightest section with enough Sx.
+ section = props.lightest_w_shape(required_sx_in3=required_sx.magnitude)
+ print(f"Lightest for strength: {section['designation']} (Sx = {section['sx_in3']} in^3)")
+
+ # Serviceability check: does it also meet the deflection limit?
+ # If not, walk up the catalogue until one does. Sort exactly like
+ # lightest_w_shape: by weight, and for equal weights the stronger first.
+ shapes = props.load_table(name="w_shapes").sort_values(
+ ["weight_lb_ft", "sx_in3"],
+ ascending=[True, False],
+ )
+ strong_enough = shapes[shapes["sx_in3"] >= required_sx.magnitude]
+ for _, candidate in strong_enough.iterrows():
+ delta = deflection(section=candidate)
+ status = "OK" if delta <= deflection_limit else "too flexible"
+ print(f" {candidate['designation']:>7}: deflection {delta:.1f~P} -> {status}")
+ if delta <= deflection_limit:
+ weight = Q_(candidate["weight_lb_ft"], "lbf/ft").to("kgf/m")
+ print(f"\nSelected: {candidate['designation']} ({weight.magnitude:.1f} kg/m)")
+ return
+ print("\nNo section in the table passes - extend data/w_shapes.csv.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/data/make_pump_sensor_log.py b/examples/data/make_pump_sensor_log.py
new file mode 100644
index 0000000..6932625
--- /dev/null
+++ b/examples/data/make_pump_sensor_log.py
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Generate the SYNTHETIC pump sensor log used by sensor_log_analysis.py.
+
+The numbers are made up (seeded, so they are reproducible) to look like a
+real data-logger export: two pumps, one reading every 10 s for two hours,
+with noise, a few dropped readings, and one pump that runs hot.
+
+Run from the repo root: uv run examples/data/make_pump_sensor_log.py
+"""
+
+from pathlib import Path
+
+import numpy as np
+import pandas as pd
+
+rng = np.random.default_rng(seed=42)
+timestamps = pd.date_range("2026-11-06 08:00", periods=720, freq="10s")
+
+frames = []
+for pump_id, base_temp in (("P-101", 55.0), ("P-102", 63.0)):
+ n = len(timestamps)
+ frames.append(
+ pd.DataFrame(
+ {
+ "timestamp": timestamps,
+ "pump_id": pump_id,
+ "flow_l_s": 12 + rng.normal(0, 0.4, n),
+ "discharge_kpa": 180 + rng.normal(0, 5, n),
+ # A slow upward drift + noise on the bearing temperature.
+ "bearing_temp_c": base_temp + np.linspace(0, 8, n) + rng.normal(0, 1.0, n),
+ }
+ )
+ )
+log = pd.concat(frames, ignore_index=True)
+
+# Real loggers drop readings: blank out ~1% of the flow values.
+log.loc[rng.random(len(log)) < 0.01, "flow_l_s"] = np.nan
+
+out = Path(__file__).resolve().parent / "pump_sensor_log.csv"
+log.round({"flow_l_s": 3, "discharge_kpa": 3, "bearing_temp_c": 3}).to_csv(out, index=False)
+print(f"Wrote {len(log)} rows -> {out}")
diff --git a/examples/data/pump_sensor_log.csv b/examples/data/pump_sensor_log.csv
new file mode 100644
index 0000000..768877b
--- /dev/null
+++ b/examples/data/pump_sensor_log.csv
@@ -0,0 +1,1441 @@
+timestamp,pump_id,flow_l_s,discharge_kpa,bearing_temp_c
+2026-11-06 08:00:00,P-101,12.122,179.519,54.335
+2026-11-06 08:00:10,P-101,11.584,185.64,55.372
+2026-11-06 08:00:20,P-101,12.3,168.596,53.632
+2026-11-06 08:00:30,P-101,12.376,172.517,56.472
+2026-11-06 08:00:40,P-101,11.22,175.386,54.9
+2026-11-06 08:00:50,P-101,11.479,187.306,55.346
+2026-11-06 08:01:00,P-101,12.051,181.413,57.393
+2026-11-06 08:01:10,P-101,11.874,183.837,56.594
+2026-11-06 08:01:20,P-101,11.993,174.299,54.782
+2026-11-06 08:01:30,P-101,11.659,174.402,54.507
+2026-11-06 08:01:40,P-101,12.352,182.239,54.786
+2026-11-06 08:01:50,P-101,12.311,180.291,55.632
+2026-11-06 08:02:00,P-101,12.026,182.744,55.028
+2026-11-06 08:02:10,P-101,12.451,179.062,54.749
+2026-11-06 08:02:20,P-101,12.187,181.391,56.623
+2026-11-06 08:02:30,P-101,11.656,180.791,55.053
+2026-11-06 08:02:40,P-101,12.148,183.889,57.478
+2026-11-06 08:02:50,P-101,11.616,184.035,55.28
+2026-11-06 08:03:00,P-101,12.351,171.901,56.986
+2026-11-06 08:03:10,P-101,11.98,168.764,55.569
+2026-11-06 08:03:20,P-101,11.926,185.009,56.185
+2026-11-06 08:03:30,P-101,11.728,185.939,54.509
+2026-11-06 08:03:40,P-101,12.489,174.897,54.391
+2026-11-06 08:03:50,P-101,11.938,170.701,55.716
+2026-11-06 08:04:00,P-101,11.829,180.495,56.256
+2026-11-06 08:04:10,P-101,11.859,184.654,55.318
+2026-11-06 08:04:20,P-101,12.213,188.988,54.003
+2026-11-06 08:04:30,P-101,12.146,182.581,53.94
+2026-11-06 08:04:40,P-101,12.165,178.141,53.884
+2026-11-06 08:04:50,P-101,12.172,175.534,53.555
+2026-11-06 08:05:00,P-101,12.857,180.057,55.443
+2026-11-06 08:05:10,P-101,11.837,178.504,56.47
+2026-11-06 08:05:20,P-101,11.795,174.925,55.343
+2026-11-06 08:05:30,P-101,11.674,190.244,53.107
+2026-11-06 08:05:40,P-101,12.246,188.926,55.188
+2026-11-06 08:05:50,P-101,12.452,185.68,55.227
+2026-11-06 08:06:00,P-101,11.954,175.396,57.497
+2026-11-06 08:06:10,P-101,11.664,184.275,53.955
+2026-11-06 08:06:20,P-101,11.67,183.198,55.032
+2026-11-06 08:06:30,P-101,12.26,182.213,57.14
+2026-11-06 08:06:40,P-101,12.297,186.248,55.895
+2026-11-06 08:06:50,P-101,12.217,183.177,56.02
+2026-11-06 08:07:00,P-101,11.734,183.7,55.582
+2026-11-06 08:07:10,P-101,12.093,183.185,55.808
+2026-11-06 08:07:20,P-101,12.047,181.704,56.267
+2026-11-06 08:07:30,P-101,12.087,171.082,54.691
+2026-11-06 08:07:40,P-101,12.349,180.418,56.39
+2026-11-06 08:07:50,P-101,12.089,177.219,54.15
+2026-11-06 08:08:00,P-101,12.272,173.601,55.862
+2026-11-06 08:08:10,P-101,12.027,188.409,55.724
+2026-11-06 08:08:20,P-101,12.116,188.645,54.94
+2026-11-06 08:08:30,P-101,12.253,186.796,55.545
+2026-11-06 08:08:40,P-101,11.417,181.276,54.956
+2026-11-06 08:08:50,P-101,11.872,186.753,54.518
+2026-11-06 08:09:00,P-101,11.812,180.06,55.247
+2026-11-06 08:09:10,P-101,11.744,181.014,54.508
+2026-11-06 08:09:20,P-101,11.89,174.533,55.946
+2026-11-06 08:09:30,P-101,12.598,181.985,54.484
+2026-11-06 08:09:40,P-101,11.654,180.302,56.357
+2026-11-06 08:09:50,P-101,12.387,173.487,54.475
+2026-11-06 08:10:00,P-101,11.327,179.744,55.101
+2026-11-06 08:10:10,P-101,11.866,179.601,55.054
+2026-11-06 08:10:20,P-101,12.065,188.988,57.015
+2026-11-06 08:10:30,P-101,12.234,184.471,56.031
+2026-11-06 08:10:40,P-101,12.284,180.057,55.5
+2026-11-06 08:10:50,P-101,12.317,181.244,56.222
+2026-11-06 08:11:00,P-101,11.861,180.221,53.627
+2026-11-06 08:11:10,P-101,11.815,178.985,55.702
+2026-11-06 08:11:20,P-101,12.343,174.588,57.754
+2026-11-06 08:11:30,P-101,11.923,179.245,55.9
+2026-11-06 08:11:40,P-101,11.49,176.27,55.847
+2026-11-06 08:11:50,P-101,11.547,173.748,56.338
+2026-11-06 08:12:00,P-101,11.632,182.556,56.697
+2026-11-06 08:12:10,P-101,12.199,181.956,53.815
+2026-11-06 08:12:20,P-101,12.057,171.066,54.141
+2026-11-06 08:12:30,P-101,12.276,179.387,56.045
+2026-11-06 08:12:40,P-101,11.829,184.979,55.131
+2026-11-06 08:12:50,P-101,12.063,185.296,57.169
+2026-11-06 08:13:00,P-101,12.25,185.129,55.553
+2026-11-06 08:13:10,P-101,11.876,180.195,53.944
+2026-11-06 08:13:20,P-101,12.183,175.775,54.622
+2026-11-06 08:13:30,P-101,11.735,174.582,53.152
+2026-11-06 08:13:40,P-101,11.855,181.723,55.678
+2026-11-06 08:13:50,P-101,11.847,181.896,55.735
+2026-11-06 08:14:00,P-101,11.522,186.437,55.51
+2026-11-06 08:14:10,P-101,12.195,185.5,57.025
+2026-11-06 08:14:20,P-101,11.812,179.339,57.155
+2026-11-06 08:14:30,P-101,12.005,173.779,55.798
+2026-11-06 08:14:40,P-101,12.192,178.404,53.844
+2026-11-06 08:14:50,P-101,12.179,181.086,56.608
+2026-11-06 08:15:00,P-101,12.266,178.99,56.557
+2026-11-06 08:15:10,P-101,11.961,177.111,56.322
+2026-11-06 08:15:20,P-101,11.831,181.264,57.005
+2026-11-06 08:15:30,P-101,11.968,177.48,54.87
+2026-11-06 08:15:40,P-101,11.325,176.86,58.055
+2026-11-06 08:15:50,P-101,11.421,181.557,55.098
+2026-11-06 08:16:00,P-101,11.471,177.99,56.817
+2026-11-06 08:16:10,P-101,11.601,181.221,54.392
+2026-11-06 08:16:20,P-101,12.16,181.366,55.16
+2026-11-06 08:16:30,P-101,11.638,174.303,57.618
+2026-11-06 08:16:40,P-101,11.849,177.594,55.852
+2026-11-06 08:16:50,P-101,12.52,187.189,56.964
+2026-11-06 08:17:00,P-101,11.857,174.19,56.372
+2026-11-06 08:17:10,P-101,12.295,169.417,56.041
+2026-11-06 08:17:20,P-101,11.627,170.691,57.521
+2026-11-06 08:17:30,P-101,11.918,180.146,55.932
+2026-11-06 08:17:40,P-101,11.62,180.155,54.399
+2026-11-06 08:17:50,P-101,11.864,179.412,55.387
+2026-11-06 08:18:00,P-101,12.336,186.071,56.753
+2026-11-06 08:18:10,P-101,11.309,166.636,56.608
+2026-11-06 08:18:20,P-101,12.174,181.98,54.657
+2026-11-06 08:18:30,P-101,12.095,187.807,55.511
+2026-11-06 08:18:40,P-101,11.762,174.361,55.664
+2026-11-06 08:18:50,P-101,11.422,178.101,55.892
+2026-11-06 08:19:00,P-101,12.029,176.236,54.893
+2026-11-06 08:19:10,P-101,11.788,175.528,55.651
+2026-11-06 08:19:20,P-101,12.093,178.369,55.562
+2026-11-06 08:19:30,P-101,12.009,187.137,57.073
+2026-11-06 08:19:40,P-101,12.641,189.187,57.122
+2026-11-06 08:19:50,P-101,11.904,178.32,56.178
+2026-11-06 08:20:00,P-101,11.591,189.525,55.943
+2026-11-06 08:20:10,P-101,12.072,180.178,56.486
+2026-11-06 08:20:20,P-101,12.088,188.768,55.541
+2026-11-06 08:20:30,P-101,12.544,179.534,55.222
+2026-11-06 08:20:40,P-101,12.334,180.655,55.439
+2026-11-06 08:20:50,P-101,12.143,181.827,55.756
+2026-11-06 08:21:00,P-101,12.585,195.894,55.238
+2026-11-06 08:21:10,P-101,11.524,184.256,56.575
+2026-11-06 08:21:20,P-101,,176.464,57.154
+2026-11-06 08:21:30,P-101,11.629,184.845,54.848
+2026-11-06 08:21:40,P-101,11.844,178.191,56.472
+2026-11-06 08:21:50,P-101,11.449,177.551,56.238
+2026-11-06 08:22:00,P-101,12.254,184.543,56.81
+2026-11-06 08:22:10,P-101,11.911,180.155,56.467
+2026-11-06 08:22:20,P-101,11.412,181.393,55.037
+2026-11-06 08:22:30,P-101,11.594,180.07,55.24
+2026-11-06 08:22:40,P-101,12.125,181.683,56.543
+2026-11-06 08:22:50,P-101,12.335,182.125,57.553
+2026-11-06 08:23:00,P-101,12.799,170.315,54.321
+2026-11-06 08:23:10,P-101,13.166,183.333,56.892
+2026-11-06 08:23:20,P-101,12.166,175.09,57.747
+2026-11-06 08:23:30,P-101,11.604,172.789,56.416
+2026-11-06 08:23:40,P-101,11.147,179.708,57.13
+2026-11-06 08:23:50,P-101,12.107,180.42,55.888
+2026-11-06 08:24:00,P-101,11.675,176.532,56.483
+2026-11-06 08:24:10,P-101,11.834,184.155,54.992
+2026-11-06 08:24:20,P-101,11.755,173.289,54.28
+2026-11-06 08:24:30,P-101,11.944,177.965,57.578
+2026-11-06 08:24:40,P-101,12.426,177.076,55.662
+2026-11-06 08:24:50,P-101,12.063,179.767,56.202
+2026-11-06 08:25:00,P-101,11.937,181.394,55.283
+2026-11-06 08:25:10,P-101,11.586,174.961,55.27
+2026-11-06 08:25:20,P-101,11.33,183.621,56.517
+2026-11-06 08:25:30,P-101,11.805,180.315,55.973
+2026-11-06 08:25:40,P-101,11.978,170.54,57.091
+2026-11-06 08:25:50,P-101,12.707,170.207,57.177
+2026-11-06 08:26:00,P-101,12.052,179.938,57.057
+2026-11-06 08:26:10,P-101,12.393,178.895,56.985
+2026-11-06 08:26:20,P-101,11.8,179.483,58.336
+2026-11-06 08:26:30,P-101,11.526,179.86,55.702
+2026-11-06 08:26:40,P-101,11.614,181.128,56.725
+2026-11-06 08:26:50,P-101,11.71,184.738,57.924
+2026-11-06 08:27:00,P-101,12.851,174.444,58.067
+2026-11-06 08:27:10,P-101,11.671,174.14,55.392
+2026-11-06 08:27:20,P-101,12.335,174.533,56.512
+2026-11-06 08:27:30,P-101,11.639,181.445,58.194
+2026-11-06 08:27:40,P-101,12.373,186.225,57.355
+2026-11-06 08:27:50,P-101,12.154,177.843,57.053
+2026-11-06 08:28:00,P-101,11.937,167.492,56.652
+2026-11-06 08:28:10,P-101,11.984,171.48,56.497
+2026-11-06 08:28:20,P-101,11.738,175.835,57.18
+2026-11-06 08:28:30,P-101,12.178,177.211,56.65
+2026-11-06 08:28:40,P-101,11.818,177.958,56.871
+2026-11-06 08:28:50,P-101,11.51,180.193,57.003
+2026-11-06 08:29:00,P-101,11.489,178.441,56.258
+2026-11-06 08:29:10,P-101,12.069,185.247,57.479
+2026-11-06 08:29:20,P-101,12.632,176.62,59.873
+2026-11-06 08:29:30,P-101,12.064,175.688,55.192
+2026-11-06 08:29:40,P-101,11.953,182.394,57.305
+2026-11-06 08:29:50,P-101,12.114,172.322,56.231
+2026-11-06 08:30:00,P-101,12.522,181.948,57.58
+2026-11-06 08:30:10,P-101,12.088,180.513,57.06
+2026-11-06 08:30:20,P-101,11.836,179.263,57.787
+2026-11-06 08:30:30,P-101,12.443,187.941,58.212
+2026-11-06 08:30:40,P-101,12.172,176.889,57.24
+2026-11-06 08:30:50,P-101,12.614,190.301,58.19
+2026-11-06 08:31:00,P-101,12.073,178.873,54.725
+2026-11-06 08:31:10,P-101,11.51,173.615,58.671
+2026-11-06 08:31:20,P-101,11.453,180.35,57.467
+2026-11-06 08:31:30,P-101,12.66,174.619,58.181
+2026-11-06 08:31:40,P-101,12.689,176.241,55.894
+2026-11-06 08:31:50,P-101,11.928,181.985,56.294
+2026-11-06 08:32:00,P-101,11.847,182.778,57.282
+2026-11-06 08:32:10,P-101,12.585,176.889,57.13
+2026-11-06 08:32:20,P-101,11.557,184.937,57.442
+2026-11-06 08:32:30,P-101,11.642,185.788,58.254
+2026-11-06 08:32:40,P-101,12.257,187.182,55.416
+2026-11-06 08:32:50,P-101,11.842,182.647,58.166
+2026-11-06 08:33:00,P-101,11.998,186.817,57.211
+2026-11-06 08:33:10,P-101,11.935,170.596,57.01
+2026-11-06 08:33:20,P-101,12.135,178.41,55.754
+2026-11-06 08:33:30,P-101,12.563,175.665,57.739
+2026-11-06 08:33:40,P-101,12.036,180.596,54.833
+2026-11-06 08:33:50,P-101,12.258,177.143,57.907
+2026-11-06 08:34:00,P-101,11.18,179.169,55.778
+2026-11-06 08:34:10,P-101,11.981,189.411,57.334
+2026-11-06 08:34:20,P-101,11.663,179.151,57.173
+2026-11-06 08:34:30,P-101,11.512,182.069,58.473
+2026-11-06 08:34:40,P-101,11.649,178.839,57.136
+2026-11-06 08:34:50,P-101,11.866,180.379,56.631
+2026-11-06 08:35:00,P-101,12.366,180.03,56.83
+2026-11-06 08:35:10,P-101,11.469,182.242,58.621
+2026-11-06 08:35:20,P-101,12.012,185.827,57.895
+2026-11-06 08:35:30,P-101,11.806,188.237,56.984
+2026-11-06 08:35:40,P-101,11.869,181.548,57.145
+2026-11-06 08:35:50,P-101,12.401,182.948,55.825
+2026-11-06 08:36:00,P-101,12.215,174.246,57.792
+2026-11-06 08:36:10,P-101,,179.561,55.655
+2026-11-06 08:36:20,P-101,11.938,184.701,58.177
+2026-11-06 08:36:30,P-101,11.722,184.33,57.565
+2026-11-06 08:36:40,P-101,11.91,181.058,59.835
+2026-11-06 08:36:50,P-101,12.097,184.432,56.359
+2026-11-06 08:37:00,P-101,12.071,182.454,57.367
+2026-11-06 08:37:10,P-101,11.566,186.002,59.28
+2026-11-06 08:37:20,P-101,12.036,181.447,58.684
+2026-11-06 08:37:30,P-101,12.091,178.222,56.807
+2026-11-06 08:37:40,P-101,13.007,181.679,57.739
+2026-11-06 08:37:50,P-101,12.751,165.347,57.528
+2026-11-06 08:38:00,P-101,11.659,181.914,57.819
+2026-11-06 08:38:10,P-101,11.885,161.758,56.849
+2026-11-06 08:38:20,P-101,11.415,171.383,57.47
+2026-11-06 08:38:30,P-101,11.764,182.259,55.972
+2026-11-06 08:38:40,P-101,12.126,182.388,57.716
+2026-11-06 08:38:50,P-101,12.482,174.188,58.043
+2026-11-06 08:39:00,P-101,11.708,176.439,57.376
+2026-11-06 08:39:10,P-101,11.738,186.853,56.0
+2026-11-06 08:39:20,P-101,11.141,177.58,58.614
+2026-11-06 08:39:30,P-101,11.935,191.215,58.002
+2026-11-06 08:39:40,P-101,11.575,179.99,56.922
+2026-11-06 08:39:50,P-101,11.788,182.04,57.379
+2026-11-06 08:40:00,P-101,11.649,188.084,56.17
+2026-11-06 08:40:10,P-101,11.962,180.655,56.656
+2026-11-06 08:40:20,P-101,11.297,174.988,57.155
+2026-11-06 08:40:30,P-101,11.413,179.451,58.932
+2026-11-06 08:40:40,P-101,12.852,179.822,57.075
+2026-11-06 08:40:50,P-101,11.485,173.176,58.366
+2026-11-06 08:41:00,P-101,11.561,178.721,57.199
+2026-11-06 08:41:10,P-101,12.735,176.289,57.996
+2026-11-06 08:41:20,P-101,13.162,184.622,57.618
+2026-11-06 08:41:30,P-101,11.531,180.173,58.644
+2026-11-06 08:41:40,P-101,11.853,178.586,57.247
+2026-11-06 08:41:50,P-101,12.137,179.469,56.22
+2026-11-06 08:42:00,P-101,12.691,181.116,56.422
+2026-11-06 08:42:10,P-101,11.605,183.084,57.283
+2026-11-06 08:42:20,P-101,11.902,175.001,56.577
+2026-11-06 08:42:30,P-101,12.311,174.792,58.247
+2026-11-06 08:42:40,P-101,12.174,185.523,57.383
+2026-11-06 08:42:50,P-101,11.85,177.938,60.234
+2026-11-06 08:43:00,P-101,11.946,172.916,56.133
+2026-11-06 08:43:10,P-101,11.45,182.219,58.085
+2026-11-06 08:43:20,P-101,11.905,182.317,56.765
+2026-11-06 08:43:30,P-101,11.893,172.346,57.411
+2026-11-06 08:43:40,P-101,12.093,181.147,58.057
+2026-11-06 08:43:50,P-101,11.778,183.678,55.601
+2026-11-06 08:44:00,P-101,12.189,181.872,58.174
+2026-11-06 08:44:10,P-101,12.405,183.16,59.851
+2026-11-06 08:44:20,P-101,12.062,172.979,57.506
+2026-11-06 08:44:30,P-101,12.141,181.655,58.231
+2026-11-06 08:44:40,P-101,12.021,178.487,59.007
+2026-11-06 08:44:50,P-101,12.0,177.586,57.829
+2026-11-06 08:45:00,P-101,11.711,184.353,57.423
+2026-11-06 08:45:10,P-101,12.127,187.396,55.74
+2026-11-06 08:45:20,P-101,11.961,188.972,58.667
+2026-11-06 08:45:30,P-101,12.837,186.574,57.924
+2026-11-06 08:45:40,P-101,12.629,179.451,57.036
+2026-11-06 08:45:50,P-101,12.154,181.764,58.186
+2026-11-06 08:46:00,P-101,11.695,183.834,57.487
+2026-11-06 08:46:10,P-101,11.555,180.606,57.554
+2026-11-06 08:46:20,P-101,12.476,180.654,58.699
+2026-11-06 08:46:30,P-101,12.105,184.119,56.766
+2026-11-06 08:46:40,P-101,12.192,179.704,59.163
+2026-11-06 08:46:50,P-101,11.302,176.354,56.729
+2026-11-06 08:47:00,P-101,12.371,177.928,57.475
+2026-11-06 08:47:10,P-101,12.182,183.17,60.072
+2026-11-06 08:47:20,P-101,11.556,180.015,59.287
+2026-11-06 08:47:30,P-101,11.811,181.701,58.269
+2026-11-06 08:47:40,P-101,12.105,183.35,57.242
+2026-11-06 08:47:50,P-101,12.021,178.126,57.664
+2026-11-06 08:48:00,P-101,11.883,183.781,58.278
+2026-11-06 08:48:10,P-101,11.959,181.894,58.253
+2026-11-06 08:48:20,P-101,11.899,173.826,58.136
+2026-11-06 08:48:30,P-101,12.061,187.212,58.207
+2026-11-06 08:48:40,P-101,12.589,177.496,58.035
+2026-11-06 08:48:50,P-101,10.973,171.725,56.818
+2026-11-06 08:49:00,P-101,11.905,174.775,57.755
+2026-11-06 08:49:10,P-101,12.071,174.895,59.197
+2026-11-06 08:49:20,P-101,12.118,180.261,56.925
+2026-11-06 08:49:30,P-101,11.851,178.631,58.115
+2026-11-06 08:49:40,P-101,11.297,178.316,57.496
+2026-11-06 08:49:50,P-101,12.131,183.099,58.438
+2026-11-06 08:50:00,P-101,12.691,181.699,57.949
+2026-11-06 08:50:10,P-101,11.386,181.58,59.826
+2026-11-06 08:50:20,P-101,12.346,182.049,58.867
+2026-11-06 08:50:30,P-101,11.869,183.081,57.06
+2026-11-06 08:50:40,P-101,11.975,169.46,59.054
+2026-11-06 08:50:50,P-101,11.579,178.178,57.619
+2026-11-06 08:51:00,P-101,11.866,169.099,57.828
+2026-11-06 08:51:10,P-101,12.52,180.18,58.73
+2026-11-06 08:51:20,P-101,12.233,179.977,57.935
+2026-11-06 08:51:30,P-101,12.693,185.228,59.331
+2026-11-06 08:51:40,P-101,12.471,185.938,57.675
+2026-11-06 08:51:50,P-101,12.176,181.014,57.841
+2026-11-06 08:52:00,P-101,12.698,177.498,57.815
+2026-11-06 08:52:10,P-101,12.176,182.426,57.917
+2026-11-06 08:52:20,P-101,12.331,177.36,59.593
+2026-11-06 08:52:30,P-101,11.881,179.993,59.134
+2026-11-06 08:52:40,P-101,12.027,184.931,60.226
+2026-11-06 08:52:50,P-101,11.721,177.211,60.795
+2026-11-06 08:53:00,P-101,12.396,184.028,57.508
+2026-11-06 08:53:10,P-101,11.529,183.387,58.62
+2026-11-06 08:53:20,P-101,12.313,175.226,57.384
+2026-11-06 08:53:30,P-101,11.924,184.869,57.794
+2026-11-06 08:53:40,P-101,12.468,183.493,57.953
+2026-11-06 08:53:50,P-101,12.3,180.51,57.537
+2026-11-06 08:54:00,P-101,12.728,176.188,57.553
+2026-11-06 08:54:10,P-101,12.292,175.704,58.472
+2026-11-06 08:54:20,P-101,11.371,177.312,57.411
+2026-11-06 08:54:30,P-101,11.973,182.713,59.481
+2026-11-06 08:54:40,P-101,11.531,175.222,57.732
+2026-11-06 08:54:50,P-101,11.793,182.188,61.47
+2026-11-06 08:55:00,P-101,12.604,173.791,58.22
+2026-11-06 08:55:10,P-101,12.255,178.98,58.755
+2026-11-06 08:55:20,P-101,11.72,180.548,57.399
+2026-11-06 08:55:30,P-101,11.595,192.226,60.411
+2026-11-06 08:55:40,P-101,12.013,173.113,59.836
+2026-11-06 08:55:50,P-101,11.513,187.36,59.024
+2026-11-06 08:56:00,P-101,11.732,180.749,57.392
+2026-11-06 08:56:10,P-101,12.125,182.056,58.96
+2026-11-06 08:56:20,P-101,12.462,180.592,57.639
+2026-11-06 08:56:30,P-101,12.244,182.224,60.234
+2026-11-06 08:56:40,P-101,11.083,179.232,59.592
+2026-11-06 08:56:50,P-101,12.122,187.27,58.89
+2026-11-06 08:57:00,P-101,12.029,177.718,56.369
+2026-11-06 08:57:10,P-101,12.166,185.661,57.833
+2026-11-06 08:57:20,P-101,12.646,176.778,59.515
+2026-11-06 08:57:30,P-101,11.175,179.699,60.052
+2026-11-06 08:57:40,P-101,11.764,174.64,59.959
+2026-11-06 08:57:50,P-101,12.236,182.275,57.269
+2026-11-06 08:58:00,P-101,,187.226,59.174
+2026-11-06 08:58:10,P-101,12.59,179.613,59.899
+2026-11-06 08:58:20,P-101,,179.016,57.236
+2026-11-06 08:58:30,P-101,12.339,174.427,58.898
+2026-11-06 08:58:40,P-101,11.772,178.854,59.19
+2026-11-06 08:58:50,P-101,12.326,172.036,58.627
+2026-11-06 08:59:00,P-101,12.427,175.436,57.087
+2026-11-06 08:59:10,P-101,12.093,181.134,58.624
+2026-11-06 08:59:20,P-101,12.094,186.595,58.634
+2026-11-06 08:59:30,P-101,12.108,194.046,58.824
+2026-11-06 08:59:40,P-101,11.655,177.067,58.42
+2026-11-06 08:59:50,P-101,11.941,187.177,58.551
+2026-11-06 09:00:00,P-101,11.939,181.219,58.966
+2026-11-06 09:00:10,P-101,12.153,179.244,59.459
+2026-11-06 09:00:20,P-101,12.4,182.163,58.723
+2026-11-06 09:00:30,P-101,11.577,180.31,58.629
+2026-11-06 09:00:40,P-101,11.95,180.552,59.142
+2026-11-06 09:00:50,P-101,12.593,177.958,57.14
+2026-11-06 09:01:00,P-101,11.703,173.009,59.549
+2026-11-06 09:01:10,P-101,11.671,172.282,59.083
+2026-11-06 09:01:20,P-101,12.081,183.266,59.134
+2026-11-06 09:01:30,P-101,12.338,178.616,60.066
+2026-11-06 09:01:40,P-101,12.005,177.02,57.164
+2026-11-06 09:01:50,P-101,12.532,180.043,57.154
+2026-11-06 09:02:00,P-101,12.343,183.975,59.413
+2026-11-06 09:02:10,P-101,12.337,180.902,59.143
+2026-11-06 09:02:20,P-101,12.222,176.72,59.332
+2026-11-06 09:02:30,P-101,12.931,186.131,58.71
+2026-11-06 09:02:40,P-101,11.918,187.896,59.848
+2026-11-06 09:02:50,P-101,11.199,182.473,60.127
+2026-11-06 09:03:00,P-101,12.642,184.868,59.69
+2026-11-06 09:03:10,P-101,11.817,186.21,58.956
+2026-11-06 09:03:20,P-101,12.043,185.65,57.12
+2026-11-06 09:03:30,P-101,12.524,183.07,60.008
+2026-11-06 09:03:40,P-101,11.359,182.992,60.472
+2026-11-06 09:03:50,P-101,11.499,182.598,59.906
+2026-11-06 09:04:00,P-101,11.359,174.513,58.393
+2026-11-06 09:04:10,P-101,11.682,183.504,59.958
+2026-11-06 09:04:20,P-101,12.176,173.221,60.155
+2026-11-06 09:04:30,P-101,12.21,176.027,60.865
+2026-11-06 09:04:40,P-101,12.111,186.518,58.441
+2026-11-06 09:04:50,P-101,11.435,184.201,59.102
+2026-11-06 09:05:00,P-101,11.076,187.437,60.087
+2026-11-06 09:05:10,P-101,12.022,178.642,59.408
+2026-11-06 09:05:20,P-101,11.811,174.239,58.244
+2026-11-06 09:05:30,P-101,12.184,178.798,59.787
+2026-11-06 09:05:40,P-101,12.281,180.51,59.963
+2026-11-06 09:05:50,P-101,12.055,180.395,59.483
+2026-11-06 09:06:00,P-101,12.304,185.668,59.626
+2026-11-06 09:06:10,P-101,12.092,178.194,59.159
+2026-11-06 09:06:20,P-101,12.212,181.76,60.907
+2026-11-06 09:06:30,P-101,11.718,175.057,58.539
+2026-11-06 09:06:40,P-101,11.928,182.252,58.863
+2026-11-06 09:06:50,P-101,12.079,180.016,59.473
+2026-11-06 09:07:00,P-101,12.328,176.251,60.352
+2026-11-06 09:07:10,P-101,11.843,178.821,57.987
+2026-11-06 09:07:20,P-101,12.208,179.079,58.185
+2026-11-06 09:07:30,P-101,11.894,178.649,57.469
+2026-11-06 09:07:40,P-101,11.953,188.857,59.55
+2026-11-06 09:07:50,P-101,12.332,179.508,58.41
+2026-11-06 09:08:00,P-101,11.203,178.781,59.155
+2026-11-06 09:08:10,P-101,11.481,169.513,60.212
+2026-11-06 09:08:20,P-101,11.407,175.529,58.92
+2026-11-06 09:08:30,P-101,11.067,178.685,59.804
+2026-11-06 09:08:40,P-101,11.729,176.571,59.285
+2026-11-06 09:08:50,P-101,12.3,186.909,60.267
+2026-11-06 09:09:00,P-101,11.886,179.175,60.082
+2026-11-06 09:09:10,P-101,12.079,186.442,58.926
+2026-11-06 09:09:20,P-101,12.436,180.309,59.46
+2026-11-06 09:09:30,P-101,12.531,180.186,60.684
+2026-11-06 09:09:40,P-101,11.972,179.556,60.044
+2026-11-06 09:09:50,P-101,12.541,180.019,58.265
+2026-11-06 09:10:00,P-101,12.037,188.594,60.329
+2026-11-06 09:10:10,P-101,11.665,168.402,58.6
+2026-11-06 09:10:20,P-101,11.762,169.992,61.108
+2026-11-06 09:10:30,P-101,11.408,177.284,60.731
+2026-11-06 09:10:40,P-101,11.645,180.073,60.008
+2026-11-06 09:10:50,P-101,11.857,183.451,59.886
+2026-11-06 09:11:00,P-101,12.321,182.365,59.531
+2026-11-06 09:11:10,P-101,12.688,178.079,60.43
+2026-11-06 09:11:20,P-101,11.447,185.095,59.391
+2026-11-06 09:11:30,P-101,12.157,185.151,60.009
+2026-11-06 09:11:40,P-101,11.584,180.92,60.656
+2026-11-06 09:11:50,P-101,12.19,184.813,59.234
+2026-11-06 09:12:00,P-101,11.948,181.363,59.19
+2026-11-06 09:12:10,P-101,11.268,177.193,58.623
+2026-11-06 09:12:20,P-101,12.371,183.489,58.764
+2026-11-06 09:12:30,P-101,11.758,180.553,58.012
+2026-11-06 09:12:40,P-101,11.786,180.007,60.581
+2026-11-06 09:12:50,P-101,11.572,187.365,61.671
+2026-11-06 09:13:00,P-101,11.738,167.746,61.299
+2026-11-06 09:13:10,P-101,12.171,172.912,58.258
+2026-11-06 09:13:20,P-101,11.924,174.065,60.621
+2026-11-06 09:13:30,P-101,12.131,178.184,61.329
+2026-11-06 09:13:40,P-101,12.145,178.727,59.461
+2026-11-06 09:13:50,P-101,12.528,172.463,60.031
+2026-11-06 09:14:00,P-101,11.863,175.074,57.932
+2026-11-06 09:14:10,P-101,11.409,175.696,60.218
+2026-11-06 09:14:20,P-101,12.427,192.287,58.899
+2026-11-06 09:14:30,P-101,11.867,189.009,60.173
+2026-11-06 09:14:40,P-101,12.446,177.941,58.423
+2026-11-06 09:14:50,P-101,12.153,178.182,60.539
+2026-11-06 09:15:00,P-101,11.948,174.254,61.031
+2026-11-06 09:15:10,P-101,12.14,170.459,59.235
+2026-11-06 09:15:20,P-101,12.78,179.416,58.892
+2026-11-06 09:15:30,P-101,12.831,175.011,57.884
+2026-11-06 09:15:40,P-101,12.028,179.576,59.785
+2026-11-06 09:15:50,P-101,12.064,171.999,59.481
+2026-11-06 09:16:00,P-101,12.43,176.19,58.523
+2026-11-06 09:16:10,P-101,11.662,180.743,60.538
+2026-11-06 09:16:20,P-101,12.133,181.831,59.646
+2026-11-06 09:16:30,P-101,11.99,182.087,59.031
+2026-11-06 09:16:40,P-101,12.126,173.398,59.564
+2026-11-06 09:16:50,P-101,11.667,184.273,61.201
+2026-11-06 09:17:00,P-101,11.364,175.999,60.743
+2026-11-06 09:17:10,P-101,11.171,183.164,60.022
+2026-11-06 09:17:20,P-101,11.553,179.947,60.782
+2026-11-06 09:17:30,P-101,11.817,173.118,59.013
+2026-11-06 09:17:40,P-101,11.883,178.419,60.217
+2026-11-06 09:17:50,P-101,12.775,181.827,60.343
+2026-11-06 09:18:00,P-101,12.442,183.065,59.737
+2026-11-06 09:18:10,P-101,11.615,179.295,60.654
+2026-11-06 09:18:20,P-101,12.139,187.659,59.449
+2026-11-06 09:18:30,P-101,11.837,185.038,60.75
+2026-11-06 09:18:40,P-101,11.886,178.717,60.804
+2026-11-06 09:18:50,P-101,12.074,183.751,57.346
+2026-11-06 09:19:00,P-101,12.248,189.669,61.464
+2026-11-06 09:19:10,P-101,11.864,189.802,57.237
+2026-11-06 09:19:20,P-101,12.426,173.86,59.68
+2026-11-06 09:19:30,P-101,11.543,175.367,61.333
+2026-11-06 09:19:40,P-101,12.003,187.425,60.001
+2026-11-06 09:19:50,P-101,13.039,174.709,58.863
+2026-11-06 09:20:00,P-101,12.089,173.387,59.247
+2026-11-06 09:20:10,P-101,12.573,177.569,61.379
+2026-11-06 09:20:20,P-101,12.037,182.101,58.441
+2026-11-06 09:20:30,P-101,12.232,179.488,61.503
+2026-11-06 09:20:40,P-101,11.977,176.747,60.33
+2026-11-06 09:20:50,P-101,11.932,176.629,59.204
+2026-11-06 09:21:00,P-101,11.688,176.438,59.824
+2026-11-06 09:21:10,P-101,12.172,175.602,59.401
+2026-11-06 09:21:20,P-101,11.659,191.408,61.296
+2026-11-06 09:21:30,P-101,12.266,181.488,61.173
+2026-11-06 09:21:40,P-101,12.434,184.434,58.733
+2026-11-06 09:21:50,P-101,12.147,177.555,60.06
+2026-11-06 09:22:00,P-101,11.886,179.07,59.611
+2026-11-06 09:22:10,P-101,12.182,176.432,61.779
+2026-11-06 09:22:20,P-101,11.877,166.741,59.146
+2026-11-06 09:22:30,P-101,12.374,173.11,62.112
+2026-11-06 09:22:40,P-101,11.267,170.947,60.485
+2026-11-06 09:22:50,P-101,11.866,168.751,59.368
+2026-11-06 09:23:00,P-101,11.204,174.023,59.864
+2026-11-06 09:23:10,P-101,11.402,186.625,59.751
+2026-11-06 09:23:20,P-101,12.546,179.778,59.289
+2026-11-06 09:23:30,P-101,12.358,186.453,60.669
+2026-11-06 09:23:40,P-101,11.712,182.055,59.358
+2026-11-06 09:23:50,P-101,11.399,183.913,58.416
+2026-11-06 09:24:00,P-101,10.814,175.496,60.228
+2026-11-06 09:24:10,P-101,11.783,182.62,58.816
+2026-11-06 09:24:20,P-101,12.968,183.644,59.878
+2026-11-06 09:24:30,P-101,12.174,177.117,59.732
+2026-11-06 09:24:40,P-101,11.776,182.798,59.781
+2026-11-06 09:24:50,P-101,12.186,182.831,62.664
+2026-11-06 09:25:00,P-101,11.376,177.252,60.731
+2026-11-06 09:25:10,P-101,11.881,174.386,60.931
+2026-11-06 09:25:20,P-101,12.04,174.079,58.92
+2026-11-06 09:25:30,P-101,11.966,180.433,60.054
+2026-11-06 09:25:40,P-101,12.316,181.635,60.934
+2026-11-06 09:25:50,P-101,12.138,176.037,59.706
+2026-11-06 09:26:00,P-101,12.267,180.133,61.649
+2026-11-06 09:26:10,P-101,11.725,182.853,59.55
+2026-11-06 09:26:20,P-101,12.359,183.091,60.477
+2026-11-06 09:26:30,P-101,12.652,187.759,60.189
+2026-11-06 09:26:40,P-101,11.612,186.385,60.95
+2026-11-06 09:26:50,P-101,11.645,174.994,60.156
+2026-11-06 09:27:00,P-101,12.534,188.418,60.537
+2026-11-06 09:27:10,P-101,11.923,177.345,60.583
+2026-11-06 09:27:20,P-101,12.562,185.224,60.096
+2026-11-06 09:27:30,P-101,11.823,180.34,61.327
+2026-11-06 09:27:40,P-101,12.582,177.939,61.091
+2026-11-06 09:27:50,P-101,12.053,170.962,60.775
+2026-11-06 09:28:00,P-101,12.103,179.142,60.444
+2026-11-06 09:28:10,P-101,12.626,172.203,61.682
+2026-11-06 09:28:20,P-101,11.855,184.836,60.661
+2026-11-06 09:28:30,P-101,11.624,187.584,60.602
+2026-11-06 09:28:40,P-101,11.821,176.015,61.024
+2026-11-06 09:28:50,P-101,12.181,181.51,62.627
+2026-11-06 09:29:00,P-101,11.374,176.373,59.601
+2026-11-06 09:29:10,P-101,12.255,176.858,61.723
+2026-11-06 09:29:20,P-101,11.784,183.872,59.071
+2026-11-06 09:29:30,P-101,12.459,179.807,61.192
+2026-11-06 09:29:40,P-101,11.042,188.698,62.561
+2026-11-06 09:29:50,P-101,11.685,177.589,61.486
+2026-11-06 09:30:00,P-101,11.325,184.997,61.179
+2026-11-06 09:30:10,P-101,,180.487,60.459
+2026-11-06 09:30:20,P-101,12.099,183.979,61.062
+2026-11-06 09:30:30,P-101,11.928,177.764,60.79
+2026-11-06 09:30:40,P-101,11.899,179.752,60.267
+2026-11-06 09:30:50,P-101,11.936,179.423,60.774
+2026-11-06 09:31:00,P-101,12.081,175.82,61.273
+2026-11-06 09:31:10,P-101,11.597,183.31,60.319
+2026-11-06 09:31:20,P-101,12.283,183.251,60.291
+2026-11-06 09:31:30,P-101,12.265,182.968,59.748
+2026-11-06 09:31:40,P-101,12.154,187.685,61.51
+2026-11-06 09:31:50,P-101,12.223,187.247,61.676
+2026-11-06 09:32:00,P-101,12.119,178.125,61.374
+2026-11-06 09:32:10,P-101,12.814,181.865,62.118
+2026-11-06 09:32:20,P-101,11.965,176.882,61.252
+2026-11-06 09:32:30,P-101,11.877,180.117,61.567
+2026-11-06 09:32:40,P-101,11.699,177.001,61.49
+2026-11-06 09:32:50,P-101,11.587,188.1,61.106
+2026-11-06 09:33:00,P-101,11.502,181.818,60.678
+2026-11-06 09:33:10,P-101,11.644,178.91,63.434
+2026-11-06 09:33:20,P-101,11.972,185.968,60.779
+2026-11-06 09:33:30,P-101,12.134,169.147,60.576
+2026-11-06 09:33:40,P-101,12.02,169.929,61.687
+2026-11-06 09:33:50,P-101,11.694,183.979,61.516
+2026-11-06 09:34:00,P-101,12.36,180.302,59.871
+2026-11-06 09:34:10,P-101,12.296,181.252,62.409
+2026-11-06 09:34:20,P-101,,173.368,61.203
+2026-11-06 09:34:30,P-101,11.739,179.884,60.198
+2026-11-06 09:34:40,P-101,12.219,190.018,62.509
+2026-11-06 09:34:50,P-101,12.075,184.121,61.957
+2026-11-06 09:35:00,P-101,11.421,180.849,60.078
+2026-11-06 09:35:10,P-101,11.973,177.805,63.13
+2026-11-06 09:35:20,P-101,12.105,178.027,62.023
+2026-11-06 09:35:30,P-101,11.64,169.354,61.365
+2026-11-06 09:35:40,P-101,12.076,181.288,62.977
+2026-11-06 09:35:50,P-101,11.418,184.227,61.557
+2026-11-06 09:36:00,P-101,12.534,172.122,61.381
+2026-11-06 09:36:10,P-101,12.499,176.693,60.76
+2026-11-06 09:36:20,P-101,11.899,174.213,62.013
+2026-11-06 09:36:30,P-101,12.145,175.173,61.919
+2026-11-06 09:36:40,P-101,11.036,180.267,61.468
+2026-11-06 09:36:50,P-101,11.537,169.578,60.675
+2026-11-06 09:37:00,P-101,11.882,183.071,61.606
+2026-11-06 09:37:10,P-101,11.571,183.769,61.417
+2026-11-06 09:37:20,P-101,12.286,178.744,60.164
+2026-11-06 09:37:30,P-101,12.799,167.596,60.037
+2026-11-06 09:37:40,P-101,11.529,175.018,62.085
+2026-11-06 09:37:50,P-101,11.665,186.165,58.929
+2026-11-06 09:38:00,P-101,12.094,166.11,61.748
+2026-11-06 09:38:10,P-101,12.644,178.262,61.075
+2026-11-06 09:38:20,P-101,11.511,174.1,62.873
+2026-11-06 09:38:30,P-101,12.1,184.023,61.082
+2026-11-06 09:38:40,P-101,12.729,176.624,62.246
+2026-11-06 09:38:50,P-101,11.339,182.02,61.201
+2026-11-06 09:39:00,P-101,11.488,182.827,61.723
+2026-11-06 09:39:10,P-101,11.831,189.181,62.357
+2026-11-06 09:39:20,P-101,11.792,178.985,61.169
+2026-11-06 09:39:30,P-101,12.325,181.88,62.639
+2026-11-06 09:39:40,P-101,12.097,172.575,60.66
+2026-11-06 09:39:50,P-101,11.29,185.952,61.959
+2026-11-06 09:40:00,P-101,12.206,176.197,62.599
+2026-11-06 09:40:10,P-101,11.769,177.196,62.347
+2026-11-06 09:40:20,P-101,12.51,179.889,63.615
+2026-11-06 09:40:30,P-101,11.749,172.186,60.946
+2026-11-06 09:40:40,P-101,11.745,181.144,60.341
+2026-11-06 09:40:50,P-101,12.216,184.837,60.737
+2026-11-06 09:41:00,P-101,,181.465,62.403
+2026-11-06 09:41:10,P-101,12.179,172.056,62.252
+2026-11-06 09:41:20,P-101,11.326,179.381,61.867
+2026-11-06 09:41:30,P-101,12.215,183.71,60.653
+2026-11-06 09:41:40,P-101,11.586,170.61,62.554
+2026-11-06 09:41:50,P-101,12.094,174.626,60.574
+2026-11-06 09:42:00,P-101,11.431,184.383,62.968
+2026-11-06 09:42:10,P-101,12.179,181.35,64.678
+2026-11-06 09:42:20,P-101,11.677,182.06,62.198
+2026-11-06 09:42:30,P-101,11.487,189.297,62.426
+2026-11-06 09:42:40,P-101,12.286,182.343,61.361
+2026-11-06 09:42:50,P-101,12.097,183.289,60.055
+2026-11-06 09:43:00,P-101,11.754,193.554,61.273
+2026-11-06 09:43:10,P-101,12.58,178.931,60.794
+2026-11-06 09:43:20,P-101,11.824,176.011,60.451
+2026-11-06 09:43:30,P-101,12.013,182.139,63.632
+2026-11-06 09:43:40,P-101,12.108,182.195,62.194
+2026-11-06 09:43:50,P-101,11.752,177.366,62.312
+2026-11-06 09:44:00,P-101,12.188,174.838,61.634
+2026-11-06 09:44:10,P-101,11.787,191.549,61.781
+2026-11-06 09:44:20,P-101,11.835,181.634,62.93
+2026-11-06 09:44:30,P-101,12.545,176.339,61.944
+2026-11-06 09:44:40,P-101,11.584,184.58,62.518
+2026-11-06 09:44:50,P-101,11.035,179.454,61.803
+2026-11-06 09:45:00,P-101,12.644,178.736,61.668
+2026-11-06 09:45:10,P-101,13.02,182.811,60.614
+2026-11-06 09:45:20,P-101,11.838,173.541,60.054
+2026-11-06 09:45:30,P-101,11.225,181.763,61.782
+2026-11-06 09:45:40,P-101,11.876,189.565,62.111
+2026-11-06 09:45:50,P-101,11.886,182.331,60.779
+2026-11-06 09:46:00,P-101,11.924,178.119,62.042
+2026-11-06 09:46:10,P-101,11.555,177.464,62.141
+2026-11-06 09:46:20,P-101,12.232,177.948,59.943
+2026-11-06 09:46:30,P-101,12.21,178.73,61.195
+2026-11-06 09:46:40,P-101,11.402,178.111,62.528
+2026-11-06 09:46:50,P-101,12.28,184.582,62.519
+2026-11-06 09:47:00,P-101,12.821,171.334,60.99
+2026-11-06 09:47:10,P-101,12.069,184.31,62.313
+2026-11-06 09:47:20,P-101,11.865,178.102,63.188
+2026-11-06 09:47:30,P-101,11.943,181.885,62.954
+2026-11-06 09:47:40,P-101,12.246,174.365,62.092
+2026-11-06 09:47:50,P-101,11.308,186.643,61.731
+2026-11-06 09:48:00,P-101,12.066,186.228,61.581
+2026-11-06 09:48:10,P-101,11.844,184.798,62.689
+2026-11-06 09:48:20,P-101,12.739,172.426,62.427
+2026-11-06 09:48:30,P-101,11.93,184.146,62.873
+2026-11-06 09:48:40,P-101,12.667,182.024,62.386
+2026-11-06 09:48:50,P-101,11.559,171.974,63.084
+2026-11-06 09:49:00,P-101,12.235,179.879,62.371
+2026-11-06 09:49:10,P-101,12.128,181.823,63.073
+2026-11-06 09:49:20,P-101,11.652,182.78,63.17
+2026-11-06 09:49:30,P-101,12.071,180.886,61.506
+2026-11-06 09:49:40,P-101,12.485,181.456,60.513
+2026-11-06 09:49:50,P-101,11.87,187.368,63.876
+2026-11-06 09:50:00,P-101,11.323,186.13,62.6
+2026-11-06 09:50:10,P-101,11.993,165.665,60.64
+2026-11-06 09:50:20,P-101,11.639,178.413,61.652
+2026-11-06 09:50:30,P-101,11.863,179.177,63.149
+2026-11-06 09:50:40,P-101,11.967,171.24,62.108
+2026-11-06 09:50:50,P-101,11.318,180.471,61.887
+2026-11-06 09:51:00,P-101,11.354,186.244,63.811
+2026-11-06 09:51:10,P-101,12.193,174.567,61.374
+2026-11-06 09:51:20,P-101,11.791,181.682,61.634
+2026-11-06 09:51:30,P-101,10.974,175.421,62.655
+2026-11-06 09:51:40,P-101,12.314,176.64,62.845
+2026-11-06 09:51:50,P-101,12.109,187.387,62.962
+2026-11-06 09:52:00,P-101,11.714,177.287,63.388
+2026-11-06 09:52:10,P-101,11.473,182.311,62.687
+2026-11-06 09:52:20,P-101,12.334,170.644,62.989
+2026-11-06 09:52:30,P-101,12.14,189.312,62.251
+2026-11-06 09:52:40,P-101,12.953,183.013,62.584
+2026-11-06 09:52:50,P-101,12.168,179.088,62.64
+2026-11-06 09:53:00,P-101,12.155,182.878,62.759
+2026-11-06 09:53:10,P-101,11.933,172.951,63.327
+2026-11-06 09:53:20,P-101,12.327,185.913,63.121
+2026-11-06 09:53:30,P-101,12.25,178.38,62.621
+2026-11-06 09:53:40,P-101,12.501,178.98,61.779
+2026-11-06 09:53:50,P-101,11.791,177.542,62.667
+2026-11-06 09:54:00,P-101,11.826,177.097,62.583
+2026-11-06 09:54:10,P-101,11.808,183.473,62.815
+2026-11-06 09:54:20,P-101,12.316,181.28,64.839
+2026-11-06 09:54:30,P-101,12.599,183.259,62.12
+2026-11-06 09:54:40,P-101,11.816,179.956,63.077
+2026-11-06 09:54:50,P-101,11.83,182.566,63.735
+2026-11-06 09:55:00,P-101,12.126,177.906,62.775
+2026-11-06 09:55:10,P-101,11.902,191.149,65.026
+2026-11-06 09:55:20,P-101,12.381,172.16,62.322
+2026-11-06 09:55:30,P-101,11.099,186.305,63.035
+2026-11-06 09:55:40,P-101,11.669,186.84,61.923
+2026-11-06 09:55:50,P-101,11.687,175.739,62.381
+2026-11-06 09:56:00,P-101,11.072,191.146,62.993
+2026-11-06 09:56:10,P-101,11.615,184.977,63.602
+2026-11-06 09:56:20,P-101,11.634,184.856,63.085
+2026-11-06 09:56:30,P-101,11.92,180.14,64.037
+2026-11-06 09:56:40,P-101,12.445,178.914,62.694
+2026-11-06 09:56:50,P-101,11.902,178.953,62.468
+2026-11-06 09:57:00,P-101,11.588,181.953,65.515
+2026-11-06 09:57:10,P-101,11.977,187.006,63.216
+2026-11-06 09:57:20,P-101,12.42,180.907,62.114
+2026-11-06 09:57:30,P-101,11.61,175.443,62.15
+2026-11-06 09:57:40,P-101,11.636,184.122,61.913
+2026-11-06 09:57:50,P-101,12.223,184.606,63.803
+2026-11-06 09:58:00,P-101,11.911,183.273,62.513
+2026-11-06 09:58:10,P-101,12.259,183.727,63.016
+2026-11-06 09:58:20,P-101,11.995,178.645,62.836
+2026-11-06 09:58:30,P-101,12.281,175.355,63.642
+2026-11-06 09:58:40,P-101,11.586,175.659,62.075
+2026-11-06 09:58:50,P-101,11.995,171.823,61.858
+2026-11-06 09:59:00,P-101,11.916,181.185,63.169
+2026-11-06 09:59:10,P-101,11.514,179.583,63.556
+2026-11-06 09:59:20,P-101,11.375,177.8,63.299
+2026-11-06 09:59:30,P-101,12.274,165.222,62.168
+2026-11-06 09:59:40,P-101,11.86,173.763,63.666
+2026-11-06 09:59:50,P-101,11.591,185.604,62.256
+2026-11-06 08:00:00,P-102,11.791,181.528,63.968
+2026-11-06 08:00:10,P-102,11.622,184.469,65.429
+2026-11-06 08:00:20,P-102,12.52,183.762,61.633
+2026-11-06 08:00:30,P-102,12.253,185.704,64.492
+2026-11-06 08:00:40,P-102,12.242,179.99,63.115
+2026-11-06 08:00:50,P-102,12.499,181.517,63.862
+2026-11-06 08:01:00,P-102,13.125,182.558,61.419
+2026-11-06 08:01:10,P-102,11.316,180.708,64.546
+2026-11-06 08:01:20,P-102,11.705,178.014,63.876
+2026-11-06 08:01:30,P-102,11.754,178.338,63.061
+2026-11-06 08:01:40,P-102,11.66,175.912,63.756
+2026-11-06 08:01:50,P-102,,182.108,62.357
+2026-11-06 08:02:00,P-102,11.88,179.53,62.554
+2026-11-06 08:02:10,P-102,11.513,176.684,62.681
+2026-11-06 08:02:20,P-102,12.134,176.689,61.303
+2026-11-06 08:02:30,P-102,12.021,173.948,64.029
+2026-11-06 08:02:40,P-102,12.192,184.894,64.37
+2026-11-06 08:02:50,P-102,12.203,180.393,62.488
+2026-11-06 08:03:00,P-102,12.241,175.686,63.548
+2026-11-06 08:03:10,P-102,11.851,184.043,62.946
+2026-11-06 08:03:20,P-102,12.848,177.198,63.551
+2026-11-06 08:03:30,P-102,12.094,172.967,62.606
+2026-11-06 08:03:40,P-102,11.255,185.106,62.361
+2026-11-06 08:03:50,P-102,11.599,177.315,64.308
+2026-11-06 08:04:00,P-102,11.304,177.956,63.203
+2026-11-06 08:04:10,P-102,12.007,180.206,65.3
+2026-11-06 08:04:20,P-102,11.951,185.713,63.336
+2026-11-06 08:04:30,P-102,11.813,180.1,62.374
+2026-11-06 08:04:40,P-102,12.804,174.004,64.538
+2026-11-06 08:04:50,P-102,12.22,182.719,63.953
+2026-11-06 08:05:00,P-102,12.29,177.575,64.061
+2026-11-06 08:05:10,P-102,11.628,189.44,64.536
+2026-11-06 08:05:20,P-102,11.6,179.994,62.703
+2026-11-06 08:05:30,P-102,,168.825,63.24
+2026-11-06 08:05:40,P-102,12.069,181.787,63.207
+2026-11-06 08:05:50,P-102,11.257,189.723,62.099
+2026-11-06 08:06:00,P-102,11.928,187.88,63.137
+2026-11-06 08:06:10,P-102,12.006,189.155,64.748
+2026-11-06 08:06:20,P-102,,174.901,63.66
+2026-11-06 08:06:30,P-102,11.675,184.561,62.088
+2026-11-06 08:06:40,P-102,11.784,176.39,61.889
+2026-11-06 08:06:50,P-102,11.929,181.121,65.121
+2026-11-06 08:07:00,P-102,12.036,173.635,63.377
+2026-11-06 08:07:10,P-102,12.242,176.778,62.831
+2026-11-06 08:07:20,P-102,12.432,176.619,63.216
+2026-11-06 08:07:30,P-102,11.882,184.493,62.023
+2026-11-06 08:07:40,P-102,11.818,184.004,64.935
+2026-11-06 08:07:50,P-102,12.175,181.384,63.13
+2026-11-06 08:08:00,P-102,12.244,173.248,64.901
+2026-11-06 08:08:10,P-102,11.826,178.442,63.597
+2026-11-06 08:08:20,P-102,12.316,176.269,64.986
+2026-11-06 08:08:30,P-102,12.392,184.348,63.824
+2026-11-06 08:08:40,P-102,12.517,185.891,64.432
+2026-11-06 08:08:50,P-102,12.711,185.282,64.309
+2026-11-06 08:09:00,P-102,11.653,176.224,64.224
+2026-11-06 08:09:10,P-102,12.006,166.302,63.96
+2026-11-06 08:09:20,P-102,12.005,171.165,63.39
+2026-11-06 08:09:30,P-102,11.961,187.108,65.55
+2026-11-06 08:09:40,P-102,11.74,177.994,62.738
+2026-11-06 08:09:50,P-102,11.406,176.246,63.661
+2026-11-06 08:10:00,P-102,12.037,188.213,63.682
+2026-11-06 08:10:10,P-102,11.693,186.326,64.081
+2026-11-06 08:10:20,P-102,12.034,180.122,63.26
+2026-11-06 08:10:30,P-102,11.682,178.533,64.016
+2026-11-06 08:10:40,P-102,11.56,179.339,63.543
+2026-11-06 08:10:50,P-102,12.094,181.136,62.141
+2026-11-06 08:11:00,P-102,12.06,188.0,63.744
+2026-11-06 08:11:10,P-102,12.232,182.112,62.645
+2026-11-06 08:11:20,P-102,12.874,183.63,62.022
+2026-11-06 08:11:30,P-102,12.111,185.846,63.905
+2026-11-06 08:11:40,P-102,12.194,174.152,64.267
+2026-11-06 08:11:50,P-102,11.569,183.774,64.317
+2026-11-06 08:12:00,P-102,11.485,178.527,64.368
+2026-11-06 08:12:10,P-102,12.633,167.42,61.88
+2026-11-06 08:12:20,P-102,11.631,169.359,62.462
+2026-11-06 08:12:30,P-102,11.582,174.909,65.808
+2026-11-06 08:12:40,P-102,11.984,182.656,62.798
+2026-11-06 08:12:50,P-102,11.964,180.569,64.468
+2026-11-06 08:13:00,P-102,11.639,180.922,63.181
+2026-11-06 08:13:10,P-102,11.878,189.755,63.959
+2026-11-06 08:13:20,P-102,12.492,173.655,63.257
+2026-11-06 08:13:30,P-102,11.678,179.863,64.463
+2026-11-06 08:13:40,P-102,11.443,181.522,64.049
+2026-11-06 08:13:50,P-102,,176.319,63.475
+2026-11-06 08:14:00,P-102,12.197,180.142,62.884
+2026-11-06 08:14:10,P-102,11.752,172.0,64.214
+2026-11-06 08:14:20,P-102,11.367,187.05,62.744
+2026-11-06 08:14:30,P-102,11.702,176.592,64.395
+2026-11-06 08:14:40,P-102,11.978,190.157,66.606
+2026-11-06 08:14:50,P-102,12.466,179.838,65.34
+2026-11-06 08:15:00,P-102,11.585,185.14,64.72
+2026-11-06 08:15:10,P-102,11.966,179.733,63.927
+2026-11-06 08:15:20,P-102,12.27,179.846,62.085
+2026-11-06 08:15:30,P-102,11.047,177.08,62.712
+2026-11-06 08:15:40,P-102,12.414,180.596,65.329
+2026-11-06 08:15:50,P-102,11.527,181.59,63.267
+2026-11-06 08:16:00,P-102,12.133,176.079,63.369
+2026-11-06 08:16:10,P-102,12.099,179.375,63.914
+2026-11-06 08:16:20,P-102,12.434,176.95,63.028
+2026-11-06 08:16:30,P-102,12.22,191.498,62.875
+2026-11-06 08:16:40,P-102,11.049,179.551,64.062
+2026-11-06 08:16:50,P-102,11.686,187.726,63.232
+2026-11-06 08:17:00,P-102,12.055,179.022,63.394
+2026-11-06 08:17:10,P-102,11.922,181.296,63.365
+2026-11-06 08:17:20,P-102,12.191,173.393,64.68
+2026-11-06 08:17:30,P-102,12.316,177.765,63.936
+2026-11-06 08:17:40,P-102,12.243,181.053,64.001
+2026-11-06 08:17:50,P-102,11.914,187.93,64.787
+2026-11-06 08:18:00,P-102,11.261,181.11,64.06
+2026-11-06 08:18:10,P-102,11.818,186.919,63.935
+2026-11-06 08:18:20,P-102,12.167,183.204,64.694
+2026-11-06 08:18:30,P-102,11.564,188.137,64.903
+2026-11-06 08:18:40,P-102,12.253,179.705,64.099
+2026-11-06 08:18:50,P-102,12.363,191.951,64.338
+2026-11-06 08:19:00,P-102,12.051,177.573,63.487
+2026-11-06 08:19:10,P-102,10.774,176.168,63.419
+2026-11-06 08:19:20,P-102,11.489,173.95,65.06
+2026-11-06 08:19:30,P-102,12.754,191.655,64.747
+2026-11-06 08:19:40,P-102,11.781,184.097,64.768
+2026-11-06 08:19:50,P-102,11.206,178.329,64.605
+2026-11-06 08:20:00,P-102,12.494,186.245,64.381
+2026-11-06 08:20:10,P-102,12.005,183.438,65.043
+2026-11-06 08:20:20,P-102,12.442,189.831,65.293
+2026-11-06 08:20:30,P-102,12.111,172.261,65.28
+2026-11-06 08:20:40,P-102,12.183,178.673,64.279
+2026-11-06 08:20:50,P-102,12.145,177.687,65.778
+2026-11-06 08:21:00,P-102,12.279,170.132,64.63
+2026-11-06 08:21:10,P-102,12.911,185.302,65.045
+2026-11-06 08:21:20,P-102,12.388,178.648,64.706
+2026-11-06 08:21:30,P-102,11.368,183.698,64.682
+2026-11-06 08:21:40,P-102,12.179,191.799,63.711
+2026-11-06 08:21:50,P-102,12.457,178.234,65.159
+2026-11-06 08:22:00,P-102,12.481,177.894,67.314
+2026-11-06 08:22:10,P-102,11.829,180.593,64.105
+2026-11-06 08:22:20,P-102,12.466,184.936,64.562
+2026-11-06 08:22:30,P-102,12.371,177.166,64.056
+2026-11-06 08:22:40,P-102,13.029,176.845,64.025
+2026-11-06 08:22:50,P-102,11.7,185.286,66.069
+2026-11-06 08:23:00,P-102,11.797,181.893,63.862
+2026-11-06 08:23:10,P-102,11.878,169.776,64.073
+2026-11-06 08:23:20,P-102,11.804,173.873,62.97
+2026-11-06 08:23:30,P-102,11.644,187.506,63.811
+2026-11-06 08:23:40,P-102,10.895,182.01,62.872
+2026-11-06 08:23:50,P-102,11.826,176.189,65.223
+2026-11-06 08:24:00,P-102,12.148,179.148,67.402
+2026-11-06 08:24:10,P-102,11.803,180.066,64.247
+2026-11-06 08:24:20,P-102,12.774,183.679,62.958
+2026-11-06 08:24:30,P-102,12.005,173.209,65.536
+2026-11-06 08:24:40,P-102,12.516,175.795,63.92
+2026-11-06 08:24:50,P-102,12.105,181.365,65.957
+2026-11-06 08:25:00,P-102,12.331,179.501,64.387
+2026-11-06 08:25:10,P-102,12.112,182.859,64.048
+2026-11-06 08:25:20,P-102,12.548,173.852,64.513
+2026-11-06 08:25:30,P-102,12.022,181.786,65.46
+2026-11-06 08:25:40,P-102,12.198,185.458,64.486
+2026-11-06 08:25:50,P-102,12.147,182.434,64.228
+2026-11-06 08:26:00,P-102,11.885,180.696,64.537
+2026-11-06 08:26:10,P-102,12.162,179.521,63.475
+2026-11-06 08:26:20,P-102,12.894,175.826,65.155
+2026-11-06 08:26:30,P-102,11.892,179.64,62.982
+2026-11-06 08:26:40,P-102,12.168,186.299,64.336
+2026-11-06 08:26:50,P-102,12.503,179.405,65.551
+2026-11-06 08:27:00,P-102,12.456,175.551,65.106
+2026-11-06 08:27:10,P-102,12.005,177.539,65.404
+2026-11-06 08:27:20,P-102,11.047,181.004,64.76
+2026-11-06 08:27:30,P-102,12.207,178.132,66.006
+2026-11-06 08:27:40,P-102,11.112,182.427,65.772
+2026-11-06 08:27:50,P-102,12.588,182.614,65.304
+2026-11-06 08:28:00,P-102,12.068,184.445,65.797
+2026-11-06 08:28:10,P-102,12.4,187.118,65.631
+2026-11-06 08:28:20,P-102,11.639,184.979,64.452
+2026-11-06 08:28:30,P-102,11.379,175.329,63.559
+2026-11-06 08:28:40,P-102,12.104,175.482,65.334
+2026-11-06 08:28:50,P-102,11.987,179.621,65.471
+2026-11-06 08:29:00,P-102,11.803,177.744,66.026
+2026-11-06 08:29:10,P-102,12.034,174.308,64.011
+2026-11-06 08:29:20,P-102,13.107,177.792,64.052
+2026-11-06 08:29:30,P-102,12.334,177.627,65.429
+2026-11-06 08:29:40,P-102,12.42,176.641,64.909
+2026-11-06 08:29:50,P-102,12.338,178.421,63.106
+2026-11-06 08:30:00,P-102,12.592,185.892,65.378
+2026-11-06 08:30:10,P-102,11.924,180.996,63.161
+2026-11-06 08:30:20,P-102,11.178,192.359,64.08
+2026-11-06 08:30:30,P-102,11.925,180.853,66.388
+2026-11-06 08:30:40,P-102,12.325,178.55,66.872
+2026-11-06 08:30:50,P-102,11.612,180.021,63.843
+2026-11-06 08:31:00,P-102,11.929,181.282,64.74
+2026-11-06 08:31:10,P-102,11.723,182.176,65.123
+2026-11-06 08:31:20,P-102,12.601,190.384,66.873
+2026-11-06 08:31:30,P-102,12.3,175.773,64.61
+2026-11-06 08:31:40,P-102,12.261,184.295,64.637
+2026-11-06 08:31:50,P-102,12.249,181.039,66.674
+2026-11-06 08:32:00,P-102,11.985,172.145,64.762
+2026-11-06 08:32:10,P-102,12.079,183.853,65.87
+2026-11-06 08:32:20,P-102,11.526,186.338,63.681
+2026-11-06 08:32:30,P-102,11.125,175.714,64.998
+2026-11-06 08:32:40,P-102,12.06,179.955,63.521
+2026-11-06 08:32:50,P-102,12.13,171.373,64.136
+2026-11-06 08:33:00,P-102,12.484,173.706,66.042
+2026-11-06 08:33:10,P-102,12.629,185.24,65.164
+2026-11-06 08:33:20,P-102,12.628,177.668,64.906
+2026-11-06 08:33:30,P-102,11.601,174.456,64.488
+2026-11-06 08:33:40,P-102,11.859,181.147,65.027
+2026-11-06 08:33:50,P-102,12.192,177.388,65.56
+2026-11-06 08:34:00,P-102,13.013,177.646,66.253
+2026-11-06 08:34:10,P-102,12.592,183.697,62.656
+2026-11-06 08:34:20,P-102,12.448,174.191,64.46
+2026-11-06 08:34:30,P-102,11.845,175.473,65.462
+2026-11-06 08:34:40,P-102,11.647,180.77,65.215
+2026-11-06 08:34:50,P-102,12.316,184.847,65.729
+2026-11-06 08:35:00,P-102,11.56,186.191,65.604
+2026-11-06 08:35:10,P-102,11.905,176.675,65.043
+2026-11-06 08:35:20,P-102,11.954,184.687,64.832
+2026-11-06 08:35:30,P-102,12.637,176.079,65.124
+2026-11-06 08:35:40,P-102,11.858,189.131,66.53
+2026-11-06 08:35:50,P-102,11.024,174.331,64.66
+2026-11-06 08:36:00,P-102,12.202,186.642,65.341
+2026-11-06 08:36:10,P-102,11.72,176.718,65.598
+2026-11-06 08:36:20,P-102,12.017,190.066,66.986
+2026-11-06 08:36:30,P-102,11.965,182.957,65.136
+2026-11-06 08:36:40,P-102,11.891,174.92,66.407
+2026-11-06 08:36:50,P-102,12.316,177.389,66.691
+2026-11-06 08:37:00,P-102,11.387,183.906,65.406
+2026-11-06 08:37:10,P-102,11.793,185.263,65.271
+2026-11-06 08:37:20,P-102,12.274,179.646,65.163
+2026-11-06 08:37:30,P-102,11.754,180.223,65.248
+2026-11-06 08:37:40,P-102,11.356,183.792,66.369
+2026-11-06 08:37:50,P-102,12.267,180.659,65.757
+2026-11-06 08:38:00,P-102,12.449,173.532,65.772
+2026-11-06 08:38:10,P-102,11.982,175.935,64.423
+2026-11-06 08:38:20,P-102,11.216,181.176,64.592
+2026-11-06 08:38:30,P-102,12.282,186.077,67.714
+2026-11-06 08:38:40,P-102,11.336,188.08,65.404
+2026-11-06 08:38:50,P-102,12.124,183.898,64.818
+2026-11-06 08:39:00,P-102,11.645,181.436,67.035
+2026-11-06 08:39:10,P-102,11.536,183.744,65.897
+2026-11-06 08:39:20,P-102,11.823,176.666,66.204
+2026-11-06 08:39:30,P-102,11.927,179.234,65.499
+2026-11-06 08:39:40,P-102,11.742,175.879,65.329
+2026-11-06 08:39:50,P-102,12.235,173.756,65.49
+2026-11-06 08:40:00,P-102,11.822,181.668,64.288
+2026-11-06 08:40:10,P-102,12.392,180.928,65.594
+2026-11-06 08:40:20,P-102,12.112,183.438,65.989
+2026-11-06 08:40:30,P-102,11.721,182.731,67.554
+2026-11-06 08:40:40,P-102,11.654,181.213,65.801
+2026-11-06 08:40:50,P-102,11.341,182.783,63.392
+2026-11-06 08:41:00,P-102,12.116,179.787,65.643
+2026-11-06 08:41:10,P-102,11.858,182.064,68.488
+2026-11-06 08:41:20,P-102,11.051,176.369,66.626
+2026-11-06 08:41:30,P-102,11.786,181.849,64.96
+2026-11-06 08:41:40,P-102,11.64,183.342,66.373
+2026-11-06 08:41:50,P-102,12.63,176.652,65.051
+2026-11-06 08:42:00,P-102,11.319,180.259,64.45
+2026-11-06 08:42:10,P-102,11.423,176.882,65.467
+2026-11-06 08:42:20,P-102,12.444,177.367,65.918
+2026-11-06 08:42:30,P-102,12.543,174.375,63.965
+2026-11-06 08:42:40,P-102,11.702,186.329,64.699
+2026-11-06 08:42:50,P-102,11.747,185.328,65.87
+2026-11-06 08:43:00,P-102,12.246,182.687,65.462
+2026-11-06 08:43:10,P-102,11.069,189.861,66.277
+2026-11-06 08:43:20,P-102,11.256,179.054,64.678
+2026-11-06 08:43:30,P-102,11.888,195.159,66.618
+2026-11-06 08:43:40,P-102,12.62,178.862,64.731
+2026-11-06 08:43:50,P-102,11.826,181.276,67.569
+2026-11-06 08:44:00,P-102,12.04,178.011,64.222
+2026-11-06 08:44:10,P-102,12.454,181.862,64.856
+2026-11-06 08:44:20,P-102,12.843,184.178,66.377
+2026-11-06 08:44:30,P-102,11.769,179.43,67.635
+2026-11-06 08:44:40,P-102,11.993,188.884,66.297
+2026-11-06 08:44:50,P-102,12.729,181.196,66.556
+2026-11-06 08:45:00,P-102,11.391,181.352,66.022
+2026-11-06 08:45:10,P-102,12.411,175.455,64.989
+2026-11-06 08:45:20,P-102,11.593,182.092,65.536
+2026-11-06 08:45:30,P-102,12.041,179.33,64.826
+2026-11-06 08:45:40,P-102,11.903,176.959,64.77
+2026-11-06 08:45:50,P-102,11.617,176.118,66.462
+2026-11-06 08:46:00,P-102,11.607,193.359,64.948
+2026-11-06 08:46:10,P-102,12.111,178.683,66.402
+2026-11-06 08:46:20,P-102,12.398,172.715,64.791
+2026-11-06 08:46:30,P-102,,176.509,66.401
+2026-11-06 08:46:40,P-102,12.028,179.742,65.627
+2026-11-06 08:46:50,P-102,12.228,182.733,67.533
+2026-11-06 08:47:00,P-102,11.611,182.774,66.461
+2026-11-06 08:47:10,P-102,12.296,183.383,67.385
+2026-11-06 08:47:20,P-102,12.328,176.776,65.404
+2026-11-06 08:47:30,P-102,13.041,181.702,66.227
+2026-11-06 08:47:40,P-102,11.85,175.62,65.201
+2026-11-06 08:47:50,P-102,11.817,176.902,65.78
+2026-11-06 08:48:00,P-102,12.308,175.411,65.942
+2026-11-06 08:48:10,P-102,12.328,174.058,65.49
+2026-11-06 08:48:20,P-102,11.786,185.321,65.011
+2026-11-06 08:48:30,P-102,11.839,183.443,64.824
+2026-11-06 08:48:40,P-102,12.216,184.883,63.909
+2026-11-06 08:48:50,P-102,11.669,180.197,65.204
+2026-11-06 08:49:00,P-102,11.836,183.884,66.899
+2026-11-06 08:49:10,P-102,12.036,171.194,64.684
+2026-11-06 08:49:20,P-102,12.398,184.315,64.925
+2026-11-06 08:49:30,P-102,11.758,180.886,68.095
+2026-11-06 08:49:40,P-102,11.263,183.136,65.679
+2026-11-06 08:49:50,P-102,11.233,181.9,66.648
+2026-11-06 08:50:00,P-102,11.496,175.065,66.98
+2026-11-06 08:50:10,P-102,11.475,172.51,67.012
+2026-11-06 08:50:20,P-102,11.938,176.44,64.838
+2026-11-06 08:50:30,P-102,11.524,182.825,66.14
+2026-11-06 08:50:40,P-102,11.719,183.45,64.764
+2026-11-06 08:50:50,P-102,11.69,173.16,68.84
+2026-11-06 08:51:00,P-102,,175.095,64.81
+2026-11-06 08:51:10,P-102,12.529,176.384,65.368
+2026-11-06 08:51:20,P-102,12.265,178.213,65.386
+2026-11-06 08:51:30,P-102,12.006,173.19,64.941
+2026-11-06 08:51:40,P-102,11.816,172.89,67.802
+2026-11-06 08:51:50,P-102,11.827,175.891,68.883
+2026-11-06 08:52:00,P-102,12.131,180.431,65.996
+2026-11-06 08:52:10,P-102,12.234,174.097,65.162
+2026-11-06 08:52:20,P-102,11.637,179.421,64.257
+2026-11-06 08:52:30,P-102,12.015,185.293,66.261
+2026-11-06 08:52:40,P-102,11.187,173.491,66.2
+2026-11-06 08:52:50,P-102,12.085,178.968,65.876
+2026-11-06 08:53:00,P-102,12.305,185.124,66.76
+2026-11-06 08:53:10,P-102,11.888,177.909,66.945
+2026-11-06 08:53:20,P-102,12.727,177.132,66.884
+2026-11-06 08:53:30,P-102,11.648,185.817,66.256
+2026-11-06 08:53:40,P-102,12.087,178.469,65.481
+2026-11-06 08:53:50,P-102,11.986,185.522,67.26
+2026-11-06 08:54:00,P-102,11.96,182.145,66.618
+2026-11-06 08:54:10,P-102,11.954,178.286,67.504
+2026-11-06 08:54:20,P-102,12.043,182.398,67.417
+2026-11-06 08:54:30,P-102,12.339,181.602,65.217
+2026-11-06 08:54:40,P-102,12.551,185.104,65.215
+2026-11-06 08:54:50,P-102,11.999,177.258,65.905
+2026-11-06 08:55:00,P-102,12.326,180.206,65.616
+2026-11-06 08:55:10,P-102,11.719,179.692,67.438
+2026-11-06 08:55:20,P-102,12.306,183.521,67.045
+2026-11-06 08:55:30,P-102,11.695,174.874,67.804
+2026-11-06 08:55:40,P-102,12.436,185.085,67.113
+2026-11-06 08:55:50,P-102,12.884,182.332,66.936
+2026-11-06 08:56:00,P-102,11.951,173.415,67.938
+2026-11-06 08:56:10,P-102,,186.041,67.219
+2026-11-06 08:56:20,P-102,11.781,191.17,66.829
+2026-11-06 08:56:30,P-102,12.493,185.009,68.252
+2026-11-06 08:56:40,P-102,12.221,187.135,66.898
+2026-11-06 08:56:50,P-102,12.1,180.427,66.49
+2026-11-06 08:57:00,P-102,12.403,171.692,65.967
+2026-11-06 08:57:10,P-102,12.194,170.779,67.519
+2026-11-06 08:57:20,P-102,12.487,181.275,68.065
+2026-11-06 08:57:30,P-102,12.368,177.04,66.788
+2026-11-06 08:57:40,P-102,12.609,175.427,68.158
+2026-11-06 08:57:50,P-102,12.144,179.686,64.891
+2026-11-06 08:58:00,P-102,12.362,183.078,66.817
+2026-11-06 08:58:10,P-102,11.877,172.736,66.843
+2026-11-06 08:58:20,P-102,11.759,177.756,66.167
+2026-11-06 08:58:30,P-102,12.683,187.549,69.289
+2026-11-06 08:58:40,P-102,12.086,174.624,66.85
+2026-11-06 08:58:50,P-102,12.322,184.349,66.52
+2026-11-06 08:59:00,P-102,12.049,177.576,66.965
+2026-11-06 08:59:10,P-102,11.765,182.729,64.947
+2026-11-06 08:59:20,P-102,12.004,185.487,66.201
+2026-11-06 08:59:30,P-102,12.574,183.706,67.955
+2026-11-06 08:59:40,P-102,12.222,184.546,66.354
+2026-11-06 08:59:50,P-102,11.315,174.242,66.166
+2026-11-06 09:00:00,P-102,11.914,185.568,68.315
+2026-11-06 09:00:10,P-102,12.713,175.737,68.357
+2026-11-06 09:00:20,P-102,11.914,181.298,67.545
+2026-11-06 09:00:30,P-102,11.28,181.291,66.584
+2026-11-06 09:00:40,P-102,12.757,182.426,66.811
+2026-11-06 09:00:50,P-102,12.273,181.685,66.257
+2026-11-06 09:01:00,P-102,11.972,175.781,66.078
+2026-11-06 09:01:10,P-102,11.851,173.251,66.039
+2026-11-06 09:01:20,P-102,12.433,184.362,66.477
+2026-11-06 09:01:30,P-102,11.783,180.929,66.071
+2026-11-06 09:01:40,P-102,12.459,185.747,65.553
+2026-11-06 09:01:50,P-102,12.217,179.776,67.347
+2026-11-06 09:02:00,P-102,12.098,172.974,67.374
+2026-11-06 09:02:10,P-102,12.058,184.831,68.851
+2026-11-06 09:02:20,P-102,11.93,178.148,66.493
+2026-11-06 09:02:30,P-102,12.418,181.71,67.469
+2026-11-06 09:02:40,P-102,11.654,176.763,67.496
+2026-11-06 09:02:50,P-102,11.351,188.222,66.941
+2026-11-06 09:03:00,P-102,12.246,175.617,67.95
+2026-11-06 09:03:10,P-102,12.252,178.313,67.992
+2026-11-06 09:03:20,P-102,12.192,180.537,67.556
+2026-11-06 09:03:30,P-102,12.163,174.539,66.984
+2026-11-06 09:03:40,P-102,11.872,181.653,66.893
+2026-11-06 09:03:50,P-102,12.315,188.06,65.894
+2026-11-06 09:04:00,P-102,11.982,177.548,66.467
+2026-11-06 09:04:10,P-102,11.805,181.976,66.662
+2026-11-06 09:04:20,P-102,11.875,166.253,66.171
+2026-11-06 09:04:30,P-102,12.808,179.805,67.746
+2026-11-06 09:04:40,P-102,11.306,178.435,68.514
+2026-11-06 09:04:50,P-102,12.2,175.908,66.42
+2026-11-06 09:05:00,P-102,11.328,185.407,67.623
+2026-11-06 09:05:10,P-102,11.957,178.369,66.316
+2026-11-06 09:05:20,P-102,11.827,179.113,67.057
+2026-11-06 09:05:30,P-102,11.166,178.114,66.682
+2026-11-06 09:05:40,P-102,12.156,170.416,67.986
+2026-11-06 09:05:50,P-102,,174.34,66.716
+2026-11-06 09:06:00,P-102,11.16,184.805,68.418
+2026-11-06 09:06:10,P-102,12.336,169.45,67.172
+2026-11-06 09:06:20,P-102,12.574,176.168,66.779
+2026-11-06 09:06:30,P-102,12.596,178.001,66.186
+2026-11-06 09:06:40,P-102,12.144,188.718,67.704
+2026-11-06 09:06:50,P-102,12.018,183.843,68.357
+2026-11-06 09:07:00,P-102,12.531,177.214,67.746
+2026-11-06 09:07:10,P-102,12.673,186.523,69.723
+2026-11-06 09:07:20,P-102,12.453,176.297,68.925
+2026-11-06 09:07:30,P-102,11.623,185.045,67.199
+2026-11-06 09:07:40,P-102,12.081,173.206,67.199
+2026-11-06 09:07:50,P-102,12.176,185.366,67.998
+2026-11-06 09:08:00,P-102,11.276,177.969,66.702
+2026-11-06 09:08:10,P-102,12.188,174.857,68.717
+2026-11-06 09:08:20,P-102,12.909,178.914,67.923
+2026-11-06 09:08:30,P-102,12.256,181.387,67.829
+2026-11-06 09:08:40,P-102,12.589,177.526,68.271
+2026-11-06 09:08:50,P-102,12.073,182.77,69.318
+2026-11-06 09:09:00,P-102,11.971,177.085,67.777
+2026-11-06 09:09:10,P-102,12.153,183.102,67.504
+2026-11-06 09:09:20,P-102,11.674,182.211,69.137
+2026-11-06 09:09:30,P-102,11.614,179.695,68.607
+2026-11-06 09:09:40,P-102,12.507,177.793,67.33
+2026-11-06 09:09:50,P-102,12.549,180.758,67.667
+2026-11-06 09:10:00,P-102,11.604,182.751,67.869
+2026-11-06 09:10:10,P-102,12.387,190.309,68.046
+2026-11-06 09:10:20,P-102,11.965,184.697,66.339
+2026-11-06 09:10:30,P-102,12.136,184.88,66.164
+2026-11-06 09:10:40,P-102,12.184,195.443,66.83
+2026-11-06 09:10:50,P-102,12.398,182.58,66.764
+2026-11-06 09:11:00,P-102,12.033,182.423,67.096
+2026-11-06 09:11:10,P-102,12.564,183.52,68.386
+2026-11-06 09:11:20,P-102,12.124,174.955,67.593
+2026-11-06 09:11:30,P-102,12.25,177.78,67.97
+2026-11-06 09:11:40,P-102,11.667,177.469,66.56
+2026-11-06 09:11:50,P-102,12.863,183.238,67.334
+2026-11-06 09:12:00,P-102,12.188,179.129,68.394
+2026-11-06 09:12:10,P-102,12.285,180.691,68.413
+2026-11-06 09:12:20,P-102,11.357,185.437,67.104
+2026-11-06 09:12:30,P-102,11.671,184.316,68.002
+2026-11-06 09:12:40,P-102,12.495,179.191,70.05
+2026-11-06 09:12:50,P-102,12.115,176.65,67.845
+2026-11-06 09:13:00,P-102,11.234,185.676,67.18
+2026-11-06 09:13:10,P-102,12.415,183.869,67.711
+2026-11-06 09:13:20,P-102,11.782,171.969,67.988
+2026-11-06 09:13:30,P-102,12.565,178.434,67.756
+2026-11-06 09:13:40,P-102,11.965,180.828,67.887
+2026-11-06 09:13:50,P-102,12.082,183.894,66.486
+2026-11-06 09:14:00,P-102,12.472,184.551,66.978
+2026-11-06 09:14:10,P-102,11.641,181.42,67.802
+2026-11-06 09:14:20,P-102,11.603,182.926,67.797
+2026-11-06 09:14:30,P-102,11.921,180.321,67.407
+2026-11-06 09:14:40,P-102,12.331,176.631,67.808
+2026-11-06 09:14:50,P-102,,178.694,68.057
+2026-11-06 09:15:00,P-102,11.333,180.095,68.896
+2026-11-06 09:15:10,P-102,12.329,179.827,67.919
+2026-11-06 09:15:20,P-102,11.759,170.572,68.428
+2026-11-06 09:15:30,P-102,11.843,178.013,67.499
+2026-11-06 09:15:40,P-102,11.257,180.281,68.624
+2026-11-06 09:15:50,P-102,11.728,178.593,68.438
+2026-11-06 09:16:00,P-102,11.997,178.766,68.94
+2026-11-06 09:16:10,P-102,12.372,182.44,68.798
+2026-11-06 09:16:20,P-102,11.692,174.605,68.685
+2026-11-06 09:16:30,P-102,12.663,171.86,68.009
+2026-11-06 09:16:40,P-102,11.726,178.259,68.363
+2026-11-06 09:16:50,P-102,11.727,183.821,69.193
+2026-11-06 09:17:00,P-102,12.615,188.438,67.488
+2026-11-06 09:17:10,P-102,12.085,183.911,67.523
+2026-11-06 09:17:20,P-102,11.779,172.659,68.344
+2026-11-06 09:17:30,P-102,11.938,178.838,68.589
+2026-11-06 09:17:40,P-102,12.541,181.283,67.361
+2026-11-06 09:17:50,P-102,12.586,180.888,68.121
+2026-11-06 09:18:00,P-102,11.673,182.697,68.113
+2026-11-06 09:18:10,P-102,11.983,175.415,66.003
+2026-11-06 09:18:20,P-102,12.036,180.325,68.01
+2026-11-06 09:18:30,P-102,11.606,182.102,68.083
+2026-11-06 09:18:40,P-102,11.978,184.128,68.091
+2026-11-06 09:18:50,P-102,12.002,175.971,69.679
+2026-11-06 09:19:00,P-102,12.456,178.274,67.058
+2026-11-06 09:19:10,P-102,12.483,166.481,67.46
+2026-11-06 09:19:20,P-102,11.868,176.893,65.684
+2026-11-06 09:19:30,P-102,11.914,177.378,67.826
+2026-11-06 09:19:40,P-102,12.266,174.902,67.923
+2026-11-06 09:19:50,P-102,11.766,180.828,67.82
+2026-11-06 09:20:00,P-102,12.235,179.72,69.448
+2026-11-06 09:20:10,P-102,11.965,168.704,69.212
+2026-11-06 09:20:20,P-102,11.612,181.078,68.441
+2026-11-06 09:20:30,P-102,11.617,175.358,68.004
+2026-11-06 09:20:40,P-102,12.579,182.428,69.411
+2026-11-06 09:20:50,P-102,11.937,173.591,68.377
+2026-11-06 09:21:00,P-102,12.656,180.15,67.589
+2026-11-06 09:21:10,P-102,11.795,178.584,66.702
+2026-11-06 09:21:20,P-102,12.116,181.516,67.988
+2026-11-06 09:21:30,P-102,12.957,180.623,69.196
+2026-11-06 09:21:40,P-102,11.577,178.724,70.729
+2026-11-06 09:21:50,P-102,11.241,172.319,68.525
+2026-11-06 09:22:00,P-102,12.515,172.614,67.709
+2026-11-06 09:22:10,P-102,11.669,177.081,67.3
+2026-11-06 09:22:20,P-102,11.992,179.959,69.048
+2026-11-06 09:22:30,P-102,11.766,176.959,66.743
+2026-11-06 09:22:40,P-102,12.107,183.126,69.069
+2026-11-06 09:22:50,P-102,11.62,178.604,69.348
+2026-11-06 09:23:00,P-102,11.928,175.175,68.977
+2026-11-06 09:23:10,P-102,12.18,178.61,68.332
+2026-11-06 09:23:20,P-102,11.597,181.129,69.128
+2026-11-06 09:23:30,P-102,11.72,181.624,68.745
+2026-11-06 09:23:40,P-102,11.861,189.392,67.807
+2026-11-06 09:23:50,P-102,11.428,176.655,70.447
+2026-11-06 09:24:00,P-102,11.831,179.633,68.256
+2026-11-06 09:24:10,P-102,12.048,182.131,69.395
+2026-11-06 09:24:20,P-102,11.84,179.883,66.211
+2026-11-06 09:24:30,P-102,11.891,177.497,69.918
+2026-11-06 09:24:40,P-102,11.992,172.254,70.121
+2026-11-06 09:24:50,P-102,11.738,177.446,68.693
+2026-11-06 09:25:00,P-102,12.276,174.631,69.233
+2026-11-06 09:25:10,P-102,12.574,171.367,69.244
+2026-11-06 09:25:20,P-102,11.814,176.022,68.054
+2026-11-06 09:25:30,P-102,12.194,174.765,69.255
+2026-11-06 09:25:40,P-102,11.904,182.919,67.91
+2026-11-06 09:25:50,P-102,12.146,183.005,68.121
+2026-11-06 09:26:00,P-102,12.626,175.481,69.868
+2026-11-06 09:26:10,P-102,12.203,178.699,69.727
+2026-11-06 09:26:20,P-102,11.291,170.709,68.057
+2026-11-06 09:26:30,P-102,12.182,177.528,68.041
+2026-11-06 09:26:40,P-102,12.493,181.995,68.14
+2026-11-06 09:26:50,P-102,11.911,184.125,69.53
+2026-11-06 09:27:00,P-102,12.398,176.435,67.808
+2026-11-06 09:27:10,P-102,11.724,185.259,69.176
+2026-11-06 09:27:20,P-102,11.406,172.422,69.577
+2026-11-06 09:27:30,P-102,12.056,173.762,68.753
+2026-11-06 09:27:40,P-102,11.297,183.548,69.863
+2026-11-06 09:27:50,P-102,11.904,188.356,68.559
+2026-11-06 09:28:00,P-102,12.515,182.467,68.661
+2026-11-06 09:28:10,P-102,11.236,179.798,67.909
+2026-11-06 09:28:20,P-102,11.488,169.024,69.914
+2026-11-06 09:28:30,P-102,11.587,169.888,68.142
+2026-11-06 09:28:40,P-102,11.975,180.647,69.269
+2026-11-06 09:28:50,P-102,11.854,178.21,68.859
+2026-11-06 09:29:00,P-102,12.56,174.471,67.889
+2026-11-06 09:29:10,P-102,12.382,180.328,68.344
+2026-11-06 09:29:20,P-102,12.198,180.576,70.347
+2026-11-06 09:29:30,P-102,12.058,184.722,69.177
+2026-11-06 09:29:40,P-102,12.157,180.683,67.977
+2026-11-06 09:29:50,P-102,11.655,186.142,69.303
+2026-11-06 09:30:00,P-102,12.343,180.04,68.862
+2026-11-06 09:30:10,P-102,11.555,179.423,72.474
+2026-11-06 09:30:20,P-102,12.334,185.322,69.316
+2026-11-06 09:30:30,P-102,12.474,175.108,68.661
+2026-11-06 09:30:40,P-102,12.084,194.072,70.473
+2026-11-06 09:30:50,P-102,11.93,181.876,67.223
+2026-11-06 09:31:00,P-102,12.16,181.904,69.169
+2026-11-06 09:31:10,P-102,10.855,182.625,71.081
+2026-11-06 09:31:20,P-102,12.121,189.386,68.981
+2026-11-06 09:31:30,P-102,11.955,183.963,67.375
+2026-11-06 09:31:40,P-102,11.98,171.975,68.724
+2026-11-06 09:31:50,P-102,12.283,187.017,70.585
+2026-11-06 09:32:00,P-102,11.97,178.296,69.518
+2026-11-06 09:32:10,P-102,11.544,180.536,68.314
+2026-11-06 09:32:20,P-102,11.943,173.604,68.479
+2026-11-06 09:32:30,P-102,12.027,182.631,68.616
+2026-11-06 09:32:40,P-102,12.046,176.743,69.548
+2026-11-06 09:32:50,P-102,12.065,180.501,70.536
+2026-11-06 09:33:00,P-102,12.416,178.58,70.261
+2026-11-06 09:33:10,P-102,11.566,189.625,69.676
+2026-11-06 09:33:20,P-102,12.07,189.911,67.899
+2026-11-06 09:33:30,P-102,12.516,180.233,67.914
+2026-11-06 09:33:40,P-102,11.306,180.32,69.786
+2026-11-06 09:33:50,P-102,12.072,178.133,67.919
+2026-11-06 09:34:00,P-102,11.766,185.53,69.05
+2026-11-06 09:34:10,P-102,11.852,178.355,69.591
+2026-11-06 09:34:20,P-102,11.638,177.893,68.16
+2026-11-06 09:34:30,P-102,11.932,184.893,68.284
+2026-11-06 09:34:40,P-102,12.737,177.309,69.226
+2026-11-06 09:34:50,P-102,11.494,179.811,69.835
+2026-11-06 09:35:00,P-102,11.532,172.093,70.404
+2026-11-06 09:35:10,P-102,11.479,183.521,69.96
+2026-11-06 09:35:20,P-102,12.691,189.704,69.438
+2026-11-06 09:35:30,P-102,11.815,183.606,68.629
+2026-11-06 09:35:40,P-102,11.99,187.309,68.316
+2026-11-06 09:35:50,P-102,11.839,175.769,68.887
+2026-11-06 09:36:00,P-102,11.622,181.182,69.819
+2026-11-06 09:36:10,P-102,11.906,175.284,69.757
+2026-11-06 09:36:20,P-102,12.983,178.861,67.331
+2026-11-06 09:36:30,P-102,12.557,183.861,68.657
+2026-11-06 09:36:40,P-102,11.659,180.509,70.658
+2026-11-06 09:36:50,P-102,12.259,172.937,69.544
+2026-11-06 09:37:00,P-102,11.667,173.425,68.059
+2026-11-06 09:37:10,P-102,11.289,173.094,71.843
+2026-11-06 09:37:20,P-102,12.408,182.457,69.469
+2026-11-06 09:37:30,P-102,12.464,190.319,69.893
+2026-11-06 09:37:40,P-102,12.418,186.812,69.92
+2026-11-06 09:37:50,P-102,11.812,170.52,69.164
+2026-11-06 09:38:00,P-102,11.933,187.105,68.815
+2026-11-06 09:38:10,P-102,12.3,184.682,71.675
+2026-11-06 09:38:20,P-102,12.157,172.274,70.568
+2026-11-06 09:38:30,P-102,11.843,178.484,70.398
+2026-11-06 09:38:40,P-102,11.352,185.478,70.163
+2026-11-06 09:38:50,P-102,12.046,182.752,68.595
+2026-11-06 09:39:00,P-102,12.432,173.455,70.887
+2026-11-06 09:39:10,P-102,11.521,183.054,68.777
+2026-11-06 09:39:20,P-102,12.256,169.312,69.915
+2026-11-06 09:39:30,P-102,12.133,178.886,68.003
+2026-11-06 09:39:40,P-102,12.3,185.69,67.613
+2026-11-06 09:39:50,P-102,12.349,180.092,69.983
+2026-11-06 09:40:00,P-102,11.722,177.505,68.867
+2026-11-06 09:40:10,P-102,11.353,184.447,67.698
+2026-11-06 09:40:20,P-102,11.787,184.874,70.239
+2026-11-06 09:40:30,P-102,12.356,186.55,71.829
+2026-11-06 09:40:40,P-102,11.783,184.372,71.11
+2026-11-06 09:40:50,P-102,11.722,185.407,69.474
+2026-11-06 09:41:00,P-102,11.271,183.665,69.752
+2026-11-06 09:41:10,P-102,12.701,181.328,70.642
+2026-11-06 09:41:20,P-102,12.33,180.448,70.887
+2026-11-06 09:41:30,P-102,12.308,178.814,69.433
+2026-11-06 09:41:40,P-102,12.387,173.938,69.821
+2026-11-06 09:41:50,P-102,12.231,175.223,70.099
+2026-11-06 09:42:00,P-102,11.902,178.59,70.363
+2026-11-06 09:42:10,P-102,11.934,181.207,69.688
+2026-11-06 09:42:20,P-102,11.829,187.204,69.712
+2026-11-06 09:42:30,P-102,13.054,179.791,71.223
+2026-11-06 09:42:40,P-102,12.222,185.017,71.063
+2026-11-06 09:42:50,P-102,11.814,185.397,70.343
+2026-11-06 09:43:00,P-102,12.186,178.843,71.595
+2026-11-06 09:43:10,P-102,11.596,182.973,69.501
+2026-11-06 09:43:20,P-102,12.602,178.782,71.521
+2026-11-06 09:43:30,P-102,11.736,186.611,70.452
+2026-11-06 09:43:40,P-102,12.2,183.179,73.192
+2026-11-06 09:43:50,P-102,12.426,174.048,68.739
+2026-11-06 09:44:00,P-102,12.154,180.808,69.254
+2026-11-06 09:44:10,P-102,12.038,188.241,70.373
+2026-11-06 09:44:20,P-102,12.004,179.094,70.362
+2026-11-06 09:44:30,P-102,11.977,182.239,71.926
+2026-11-06 09:44:40,P-102,11.769,182.729,69.319
+2026-11-06 09:44:50,P-102,11.82,174.817,70.904
+2026-11-06 09:45:00,P-102,12.592,182.554,71.172
+2026-11-06 09:45:10,P-102,11.957,173.031,70.726
+2026-11-06 09:45:20,P-102,11.686,176.872,69.071
+2026-11-06 09:45:30,P-102,11.793,185.379,71.451
+2026-11-06 09:45:40,P-102,12.44,181.784,71.284
+2026-11-06 09:45:50,P-102,12.27,178.478,70.665
+2026-11-06 09:46:00,P-102,12.224,182.232,70.281
+2026-11-06 09:46:10,P-102,11.897,179.7,70.314
+2026-11-06 09:46:20,P-102,12.229,180.063,70.02
+2026-11-06 09:46:30,P-102,11.743,183.284,70.751
+2026-11-06 09:46:40,P-102,11.896,176.363,70.152
+2026-11-06 09:46:50,P-102,12.388,180.202,69.284
+2026-11-06 09:47:00,P-102,12.35,178.439,69.095
+2026-11-06 09:47:10,P-102,11.607,172.072,69.402
+2026-11-06 09:47:20,P-102,11.658,175.398,70.388
+2026-11-06 09:47:30,P-102,12.043,187.841,70.15
+2026-11-06 09:47:40,P-102,12.69,181.924,70.403
+2026-11-06 09:47:50,P-102,11.234,173.382,70.7
+2026-11-06 09:48:00,P-102,12.111,172.298,69.583
+2026-11-06 09:48:10,P-102,11.45,179.439,71.687
+2026-11-06 09:48:20,P-102,11.746,187.806,69.965
+2026-11-06 09:48:30,P-102,11.854,179.066,69.81
+2026-11-06 09:48:40,P-102,11.832,186.535,70.737
+2026-11-06 09:48:50,P-102,11.786,181.76,68.429
+2026-11-06 09:49:00,P-102,12.003,182.356,69.486
+2026-11-06 09:49:10,P-102,11.786,173.321,71.269
+2026-11-06 09:49:20,P-102,11.415,184.688,68.562
+2026-11-06 09:49:30,P-102,12.312,178.325,71.428
+2026-11-06 09:49:40,P-102,12.301,184.315,71.832
+2026-11-06 09:49:50,P-102,12.337,175.929,69.963
+2026-11-06 09:50:00,P-102,11.384,180.885,70.635
+2026-11-06 09:50:10,P-102,12.12,181.76,71.474
+2026-11-06 09:50:20,P-102,12.27,179.23,69.899
+2026-11-06 09:50:30,P-102,11.588,182.023,71.245
+2026-11-06 09:50:40,P-102,12.004,178.668,69.717
+2026-11-06 09:50:50,P-102,11.953,176.755,69.922
+2026-11-06 09:51:00,P-102,12.737,188.027,69.611
+2026-11-06 09:51:10,P-102,12.77,185.245,70.764
+2026-11-06 09:51:20,P-102,11.334,176.196,71.591
+2026-11-06 09:51:30,P-102,12.998,182.842,70.26
+2026-11-06 09:51:40,P-102,11.634,176.287,69.184
+2026-11-06 09:51:50,P-102,12.392,174.077,69.383
+2026-11-06 09:52:00,P-102,12.388,185.169,70.872
+2026-11-06 09:52:10,P-102,11.602,171.48,72.332
+2026-11-06 09:52:20,P-102,12.197,184.634,70.893
+2026-11-06 09:52:30,P-102,11.957,178.736,70.124
+2026-11-06 09:52:40,P-102,12.222,178.942,71.288
+2026-11-06 09:52:50,P-102,12.092,182.049,70.293
+2026-11-06 09:53:00,P-102,12.479,182.906,70.166
+2026-11-06 09:53:10,P-102,12.03,175.868,69.596
+2026-11-06 09:53:20,P-102,11.933,183.589,71.975
+2026-11-06 09:53:30,P-102,12.438,180.567,68.038
+2026-11-06 09:53:40,P-102,12.232,173.72,71.386
+2026-11-06 09:53:50,P-102,12.293,179.151,70.136
+2026-11-06 09:54:00,P-102,11.734,186.906,70.2
+2026-11-06 09:54:10,P-102,11.582,188.383,72.314
+2026-11-06 09:54:20,P-102,11.773,183.587,69.919
+2026-11-06 09:54:30,P-102,11.752,174.048,69.964
+2026-11-06 09:54:40,P-102,12.314,182.453,70.525
+2026-11-06 09:54:50,P-102,12.39,184.254,71.08
+2026-11-06 09:55:00,P-102,11.938,183.629,70.235
+2026-11-06 09:55:10,P-102,12.572,183.925,69.115
+2026-11-06 09:55:20,P-102,12.547,181.816,69.834
+2026-11-06 09:55:30,P-102,12.097,179.638,71.992
+2026-11-06 09:55:40,P-102,11.671,176.958,70.797
+2026-11-06 09:55:50,P-102,12.63,170.51,71.184
+2026-11-06 09:56:00,P-102,11.976,181.698,70.483
+2026-11-06 09:56:10,P-102,12.05,180.93,70.096
+2026-11-06 09:56:20,P-102,11.864,182.788,72.444
+2026-11-06 09:56:30,P-102,11.644,173.312,70.589
+2026-11-06 09:56:40,P-102,11.413,183.225,70.345
+2026-11-06 09:56:50,P-102,12.253,190.273,71.896
+2026-11-06 09:57:00,P-102,11.687,180.392,70.088
+2026-11-06 09:57:10,P-102,13.224,179.692,70.227
+2026-11-06 09:57:20,P-102,11.566,181.545,70.843
+2026-11-06 09:57:30,P-102,11.497,190.162,70.844
+2026-11-06 09:57:40,P-102,12.204,174.882,71.115
+2026-11-06 09:57:50,P-102,12.612,181.016,72.958
+2026-11-06 09:58:00,P-102,12.026,176.002,71.214
+2026-11-06 09:58:10,P-102,12.229,177.265,68.511
+2026-11-06 09:58:20,P-102,11.263,179.752,71.226
+2026-11-06 09:58:30,P-102,11.729,186.737,70.949
+2026-11-06 09:58:40,P-102,11.571,189.005,70.966
+2026-11-06 09:58:50,P-102,11.242,182.241,71.179
+2026-11-06 09:59:00,P-102,12.047,178.117,70.916
+2026-11-06 09:59:10,P-102,11.103,192.464,70.903
+2026-11-06 09:59:20,P-102,11.471,177.862,71.58
+2026-11-06 09:59:30,P-102,11.785,177.144,70.436
+2026-11-06 09:59:40,P-102,11.667,171.125,70.041
+2026-11-06 09:59:50,P-102,12.159,186.98,74.024
diff --git a/examples/heat_exchanger_estimate.py b/examples/heat_exchanger_estimate.py
new file mode 100644
index 0000000..2121660
--- /dev/null
+++ b/examples/heat_exchanger_estimate.py
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""heat_exchanger_estimate.py - first-pass size of a water-to-water exchanger.
+
+QUESTION
+ Hot water (2 kg/s) must be cooled from 80 to 50 degC using cooling water
+ (3 kg/s) entering at 20 degC. What is the duty, the cooling-water outlet
+ temperature, and roughly how much area does a counterflow exchanger need?
+
+APPROACH
+ 1. Specific heats from the property table at each stream's mean temperature.
+ 2. Energy balance: Q = m*cp*dT on the hot side; the cold side absorbs it.
+ 3. LMTD for counterflow; area A = Q / (U * LMTD) with an assumed U.
+ 4. Compare with parallel flow to show why counterflow is preferred.
+
+Run it: uv run examples/heat_exchanger_estimate.py
+"""
+
+from fpc import Properties, Thermo
+
+props, thermo = Properties(), Thermo()
+
+HOT = {"mass_flow_kg_s": 2.0, "t_in_c": 80.0, "t_out_c": 50.0}
+COLD = {"mass_flow_kg_s": 3.0, "t_in_c": 20.0}
+OVERALL_U = 1500.0 # W/(m^2*K), typical for clean water-water plate/shell (assumed)
+
+
+def main():
+ cp_hot = props.water_liquid(
+ temperature_c=(HOT["t_in_c"] + HOT["t_out_c"]) / 2, property="specific_heat_j_kg_k"
+ )
+ duty = HOT["mass_flow_kg_s"] * cp_hot * (HOT["t_in_c"] - HOT["t_out_c"]) # [W]
+
+ # Cold outlet from the energy balance. cp depends on the (unknown) outlet,
+ # so iterate: guess, update cp, recompute - converges in a few passes.
+ t_cold_out = COLD["t_in_c"] + 10.0
+ for _ in range(5):
+ cp_cold = props.water_liquid(
+ temperature_c=(COLD["t_in_c"] + t_cold_out) / 2, property="specific_heat_j_kg_k"
+ )
+ t_cold_out = COLD["t_in_c"] + duty / (COLD["mass_flow_kg_s"] * cp_cold)
+
+ print(f"Duty: Q = {duty / 1000:.1f} kW")
+ print(f"Cold-water outlet: {t_cold_out:.1f} degC\n")
+
+ arrangements = {
+ "counterflow": (HOT["t_in_c"] - t_cold_out, HOT["t_out_c"] - COLD["t_in_c"]),
+ "parallel flow": (HOT["t_in_c"] - COLD["t_in_c"], HOT["t_out_c"] - t_cold_out),
+ }
+ for name, (dt1, dt2) in arrangements.items():
+ lmtd = thermo.log_mean_temperature_difference(delta_t_1=dt1, delta_t_2=dt2)
+ area = duty / (OVERALL_U * lmtd)
+ print(f"{name:>14}: LMTD = {lmtd:5.1f} K -> area = {area:5.2f} m^2")
+ print(
+ "\nCounterflow needs less area for the same duty - a larger average "
+ "driving temperature difference."
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/motor_sizing.py b/examples/motor_sizing.py
new file mode 100644
index 0000000..0ef9f24
--- /dev/null
+++ b/examples/motor_sizing.py
@@ -0,0 +1,104 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""motor_sizing.py - size a servo motor for a ball-screw axis.
+
+QUESTION
+ A 40 kg carriage on a ball screw (10 mm lead) must move 300 mm in 0.6 s
+ with a trapezoidal profile, then dwell 0.4 s, repeatedly. Which motor
+ from a short catalogue handles the peak and RMS torque, and keeps the
+ load-to-rotor inertia ratio reasonable?
+
+APPROACH
+ 1. Motion profile -> peak speed and acceleration (1/3 accel, 1/3 cruise,
+ 1/3 decel - a common starting assumption).
+ 2. Thrust from friction + inertia; torque via Mechatronics.ballscrew_torque.
+ 3. Add the torque to spin up rotating inertia (screw + reflected mass).
+ 4. RMS torque over the cycle - motors heat up on RMS, not peak.
+ 5. Screen a catalogue (a list of dicts) with if/else checks.
+
+Run it: uv run examples/motor_sizing.py
+"""
+
+import math
+
+from fpc import Mechatronics
+
+mech = Mechatronics()
+
+MASS_KG = 40.0
+LEAD_M = 0.010
+STROKE_M = 0.300
+MOVE_TIME_S = 0.6
+DWELL_S = 0.4
+FRICTION_COEFF = 0.05 # linear guide
+SCREW_INERTIA = 1.2e-4 # kg*m^2 (from screw supplier)
+SCREW_EFFICIENCY = 0.9
+SAFETY_FACTOR = 1.5
+
+# A tiny catalogue: in practice, export this from the vendor's sizing tool.
+MOTORS = [
+ {"model": "S-200W", "rated_nm": 0.64, "peak_nm": 1.9, "max_rpm": 5000, "rotor": 0.18e-4},
+ {"model": "S-400W", "rated_nm": 1.27, "peak_nm": 3.8, "max_rpm": 5000, "rotor": 0.34e-4},
+ {"model": "S-750W", "rated_nm": 2.39, "peak_nm": 7.2, "max_rpm": 4500, "rotor": 1.05e-4},
+ {"model": "S-1kW", "rated_nm": 3.18, "peak_nm": 9.5, "max_rpm": 3000, "rotor": 2.6e-4},
+]
+
+
+def main():
+ # --- 1. Motion profile ------------------------------------------------
+ t_acc = MOVE_TIME_S / 3
+ # Area under a 1/3-1/3-1/3 trapezoid: stroke = v_peak * (2/3) * move_time
+ v_peak = STROKE_M / (MOVE_TIME_S * 2 / 3)
+ accel = v_peak / t_acc
+ rpm_peak = v_peak / LEAD_M * 60
+ print(f"Peak speed {v_peak:.2f} m/s ({rpm_peak:.0f} rpm), accel {accel:.2f} m/s^2")
+
+ # --- 2-3. Torque in each phase ---------------------------------------
+ friction = FRICTION_COEFF * MASS_KG * 9.81
+ alpha = accel / LEAD_M * 2 * math.pi # screw angular accel [rad/s^2]
+
+ def phase_torque(force, rotor_inertia):
+ """Torque = ball-screw torque for the linear force + spin-up torque."""
+ linear = mech.ballscrew_torque(thrust_force=force, lead=LEAD_M, efficiency=SCREW_EFFICIENCY)
+ spin = mech.acceleration_torque(
+ inertia=SCREW_INERTIA + rotor_inertia, angular_acceleration=alpha
+ )
+ return linear, spin
+
+ reflected_mass = MASS_KG * (LEAD_M / (2 * math.pi)) ** 2
+ print(f"Load inertia at motor: {SCREW_INERTIA + reflected_mass:.2e} kg*m^2\n")
+
+ # --- 4-5. Screen each motor -----------------------------------------
+ for motor in MOTORS:
+ lin_acc, spin = phase_torque(friction + MASS_KG * accel, motor["rotor"])
+ lin_dec, _ = phase_torque(friction - MASS_KG * accel, motor["rotor"])
+ t_accel = lin_acc + spin
+ t_cruise = mech.ballscrew_torque(
+ thrust_force=friction, lead=LEAD_M, efficiency=SCREW_EFFICIENCY
+ )
+ t_decel = lin_dec - spin
+ cycle = MOVE_TIME_S + DWELL_S
+ rms = math.sqrt((t_accel**2 + t_cruise**2 + t_decel**2) * t_acc / cycle)
+ ratio = (SCREW_INERTIA + reflected_mass) / motor["rotor"]
+
+ # Each check is a boolean; all() requires every one to pass.
+ checks = {
+ "peak": abs(t_accel) * SAFETY_FACTOR <= motor["peak_nm"],
+ "rms": rms * SAFETY_FACTOR <= motor["rated_nm"],
+ "speed": rpm_peak <= motor["max_rpm"],
+ "inertia<=10": ratio <= 10,
+ }
+ verdict = (
+ "OK"
+ if all(checks.values())
+ else "fails " + ", ".join(name for name, ok in checks.items() if not ok)
+ )
+ print(
+ f"{motor['model']:>7}: peak {t_accel:4.2f} N*m, RMS {rms:4.2f} N*m, "
+ f"J ratio {ratio:5.1f} -> {verdict}"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/pid_tuning.py b/examples/pid_tuning.py
new file mode 100644
index 0000000..438a5c6
--- /dev/null
+++ b/examples/pid_tuning.py
@@ -0,0 +1,115 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""pid_tuning.py - tune a PI controller by brute-force search.
+
+QUESTION
+ A heater behaves like a first-order plant (time constant 4 s, gain 2).
+ Which PI gains reach the setpoint fastest with no more than 5% overshoot?
+
+APPROACH
+ 1. Simulate the closed loop for every (kp, ki) pair on a grid - computers
+ are cheap, so try them all.
+ 2. Measure each response with NumPy: overshoot and 2% settling time.
+ 3. Keep the candidates that meet the overshoot limit; pick the fastest.
+ 4. Plot the best response against a too-aggressive one.
+
+This "simulate many, score, pick" loop is the same pattern behind design
+studies and optimization, just with an exhaustive search instead of a solver.
+
+Run it: uv run examples/pid_tuning.py
+"""
+
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from fpc import Controls
+
+controls = Controls()
+
+PLANT = {"time_constant": 4.0, "gain": 2.0}
+SETPOINT = 1.0
+MAX_OVERSHOOT = 0.05
+DURATION = 20.0
+
+
+def score(*, time, output):
+ """Return (overshoot fraction, 2% settling time [s]) for a step response."""
+ overshoot = max(output.max() - SETPOINT, 0.0) / SETPOINT
+ # Boolean mask: True wherever the output is OUTSIDE the 2% band.
+ outside = np.abs(output - SETPOINT) > 0.02 * SETPOINT
+ if outside[-1]:
+ return overshoot, np.inf # never settled within the run
+ # Settling time = just after the last moment the output was outside.
+ last_outside = np.nonzero(outside)[0].max()
+ return overshoot, time[last_outside + 1]
+
+
+def main():
+ kp_grid = np.linspace(0.5, 6.0, 12)
+ ki_grid = np.linspace(0.1, 3.0, 12)
+ results = []
+ for kp in kp_grid:
+ for ki in ki_grid:
+ time, output = controls.simulate_pid_first_order(
+ setpoint=SETPOINT,
+ kp=kp,
+ ki=ki,
+ duration=DURATION,
+ **PLANT,
+ )
+ overshoot, settling = score(time=time, output=output)
+ results.append({"kp": kp, "ki": ki, "overshoot": overshoot, "settling": settling})
+
+ # A list comprehension with a condition filters the candidates.
+ acceptable = [r for r in results if r["overshoot"] <= MAX_OVERSHOOT]
+ best = min(acceptable, key=lambda r: r["settling"])
+ worst = max(results, key=lambda r: r["overshoot"])
+
+ print(
+ f"Tried {len(results)} gain pairs; {len(acceptable)} meet <= {MAX_OVERSHOOT:.0%} overshoot."
+ )
+ print(
+ f"Best: kp = {best['kp']:.2f}, ki = {best['ki']:.2f} -> "
+ f"settles in {best['settling']:.2f} s, overshoot {best['overshoot']:.1%}"
+ )
+ # A best result on the edge of the search grid means the true optimum may
+ # lie outside it. (Here: more kp is always faster for a first-order plant,
+ # until actuator limits - which this simple model leaves out - bite.)
+ if best["kp"] in (kp_grid[0], kp_grid[-1]) or best["ki"] in (ki_grid[0], ki_grid[-1]):
+ print(
+ " Note: best gains are on the edge of the grid - widen it, or add "
+ "the actuator limit that really caps kp."
+ )
+ print(
+ f"Most aggressive: kp = {worst['kp']:.2f}, ki = {worst['ki']:.2f} -> "
+ f"overshoot {worst['overshoot']:.1%}"
+ )
+
+ figure, axis = plt.subplots(figsize=(7, 4))
+ for label, gains in (("best", best), ("most overshoot", worst)):
+ time, output = controls.simulate_pid_first_order(
+ setpoint=SETPOINT,
+ kp=gains["kp"],
+ ki=gains["ki"],
+ duration=DURATION,
+ **PLANT,
+ )
+ axis.plot(time, output, label=f"{label}: kp={gains['kp']:.1f}, ki={gains['ki']:.1f}")
+ axis.axhline(
+ SETPOINT * (1 + MAX_OVERSHOOT), color="gray", linestyle=":", label="overshoot limit"
+ )
+ axis.set_xlabel("Time [s]")
+ axis.set_ylabel("Output")
+ axis.grid(True, alpha=0.3)
+ axis.legend()
+ output_dir = Path(__file__).resolve().parent / "output"
+ output_dir.mkdir(exist_ok=True)
+ figure.savefig(output_dir / "pid_tuning.png", dpi=120)
+ print(f"Saved plot -> {output_dir / 'pid_tuning.png'}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/pump_sizing.py b/examples/pump_sizing.py
new file mode 100644
index 0000000..ef311cd
--- /dev/null
+++ b/examples/pump_sizing.py
@@ -0,0 +1,140 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""pump_sizing.py - choose a pipe size and the pump to drive it.
+
+QUESTION
+ We need to move 12 L/s of 20 degC water up 15 m through 120 m of
+ commercial steel pipe. Which standard pipe size keeps velocity under
+ 2.5 m/s, and how much head and power must the pump deliver?
+
+APPROACH
+ 1. Look up water density and viscosity at 20 degC (Properties, interpolated).
+ 2. For each candidate pipe: velocity -> Reynolds -> Colebrook friction
+ factor (a root-finding solve) -> Darcy-Weisbach head loss.
+ 3. Pick the smallest pipe meeting the velocity limit.
+ 4. Pump head = static lift + friction + minor losses; shaft power from
+ the hydraulic power and an assumed pump efficiency.
+ 5. Plot the system curve (head vs flow) - the curve a pump vendor will
+ ask you for.
+
+Run it: uv run examples/pump_sizing.py
+"""
+
+import math
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from fpc import Fluids, Properties
+
+fluids = Fluids()
+props = Properties()
+
+# --- Inputs -------------------------------------------------------------
+FLOW_M3_S = 0.012 # 12 L/s design flow
+STATIC_LIFT_M = 15.0 # elevation gain
+PIPE_LENGTH_M = 120.0
+ROUGHNESS_M = 0.045e-3 # commercial steel, eps = 0.045 mm
+MINOR_LOSS_K = 6.0 # sum of fittings/valve K factors (estimate)
+MAX_VELOCITY_M_S = 2.5
+PUMP_EFFICIENCY = 0.70
+WATER_TEMPERATURE_C = 20.0
+
+# Nominal size -> inner diameter [m] (Schedule 40 steel pipe).
+PIPE_SIZES = {
+ '2"': 0.0525,
+ '2-1/2"': 0.0627,
+ '3"': 0.0779,
+ '4"': 0.1023,
+ '5"': 0.1282,
+}
+
+
+def friction_head(*, flow, diameter, density, viscosity):
+ """Total pipe head loss (major + minor) for a given flow and pipe [m]."""
+ area = math.pi * diameter**2 / 4
+ velocity = flow / area
+ reynolds = fluids.reynolds_number(
+ density=density,
+ velocity=velocity,
+ length=diameter,
+ viscosity=viscosity,
+ )
+ f = fluids.colebrook_friction_factor(
+ reynolds=reynolds,
+ relative_roughness=ROUGHNESS_M / diameter,
+ )
+ major = fluids.pipe_head_loss(
+ friction_factor=f,
+ length=PIPE_LENGTH_M,
+ diameter=diameter,
+ velocity=velocity,
+ )
+ # Minor losses scale with velocity head: h = K * v^2 / 2g.
+ minor = MINOR_LOSS_K * velocity**2 / (2 * fluids.GRAVITY)
+ return {"velocity": velocity, "reynolds": reynolds, "f": f, "head": major + minor}
+
+
+def main():
+ density = props.water_liquid(temperature_c=WATER_TEMPERATURE_C, property="density_kg_m3")
+ viscosity = props.water_liquid(
+ temperature_c=WATER_TEMPERATURE_C,
+ property="dynamic_viscosity_pa_s",
+ )
+ print(
+ f"Water at {WATER_TEMPERATURE_C} degC: rho = {density:.1f} kg/m^3, "
+ f"mu = {viscosity:.3e} Pa*s\n"
+ )
+
+ # --- Evaluate every candidate and pick the first that passes -------
+ print(f"{'Pipe':>7} {'v [m/s]':>8} {'Re':>9} {'f':>7} {'h_loss [m]':>11} ok?")
+ chosen = None
+ for name, diameter in PIPE_SIZES.items(): # dicts keep insertion order
+ r = friction_head(flow=FLOW_M3_S, diameter=diameter, density=density, viscosity=viscosity)
+ ok = r["velocity"] <= MAX_VELOCITY_M_S
+ print(
+ f"{name:>7} {r['velocity']:8.2f} {r['reynolds']:9.0f} "
+ f"{r['f']:7.4f} {r['head']:11.2f} {'yes' if ok else 'no'}"
+ )
+ if ok and chosen is None:
+ chosen = (name, diameter, r)
+
+ if chosen is None:
+ raise SystemExit("No candidate pipe meets the velocity limit - add bigger sizes.")
+ name, diameter, r = chosen
+
+ # --- Pump duty --------------------------------------------------------
+ total_head = STATIC_LIFT_M + r["head"]
+ hydraulic_power = density * fluids.GRAVITY * FLOW_M3_S * total_head # P = rho*g*Q*H
+ shaft_power = hydraulic_power / PUMP_EFFICIENCY
+ print(f"\nChosen pipe: {name} (ID {diameter * 1000:.1f} mm)")
+ print(f"Pump duty point: {FLOW_M3_S * 1000:.0f} L/s at {total_head:.1f} m head")
+ print(f"Shaft power: {shaft_power / 1000:.2f} kW at {PUMP_EFFICIENCY:.0%} efficiency")
+
+ # --- System curve: head required across a range of flows -------------
+ flows = np.linspace(0.001, 1.5 * FLOW_M3_S, 40)
+ # colebrook_friction_factor works on one number at a time, so we use a
+ # list comprehension to evaluate it across the flows.
+ heads = [
+ STATIC_LIFT_M
+ + friction_head(flow=q, diameter=diameter, density=density, viscosity=viscosity)["head"]
+ for q in flows
+ ]
+
+ figure, axis = plt.subplots(figsize=(7, 4))
+ axis.plot(flows * 1000, heads, label=f"system curve ({name} pipe)")
+ axis.plot(FLOW_M3_S * 1000, total_head, "ro", label="design duty point")
+ axis.set_xlabel("Flow [L/s]")
+ axis.set_ylabel("Head [m]")
+ axis.grid(True, alpha=0.3)
+ axis.legend()
+ output_dir = Path(__file__).resolve().parent / "output"
+ output_dir.mkdir(exist_ok=True)
+ figure.savefig(output_dir / "pump_system_curve.png", dpi=120)
+ print(f"\nSaved system curve -> {output_dir / 'pump_system_curve.png'}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/radiology_demo.py b/examples/radiology_demo.py
similarity index 75%
rename from radiology_demo.py
rename to examples/radiology_demo.py
index e1e4fd3..1f2ba6b 100644
--- a/radiology_demo.py
+++ b/examples/radiology_demo.py
@@ -3,19 +3,21 @@
"""radiology_demo.py - a worked tour of the medical-imaging classes.
-Run it with: uv run radiology_demo.py
+Run it with: uv run examples/radiology_demo.py
It walks through one calculation per modality, then loads a real (sample,
anonymous) CT slice with pydicom, converts it to Hounsfield units, and shows
it in three different windows - the same slice, three very different images.
-The figure is saved to radiology.png.
+The figure is saved to examples/output/radiology.png.
"""
+from pathlib import Path
+
import matplotlib.pyplot as plt
import numpy as np
from pydicom.data import get_testdata_file
-from library import CT, MRI, Dicom, Radiation, Radiography, Ultrasound
+from fpc import CT, MRI, Dicom, Radiation, Radiography, Ultrasound
def radiation_protection():
@@ -26,13 +28,16 @@ def radiation_protection():
# A loop over distances shows the inverse square law in action.
for distance_m in (1, 2, 3, 4):
rate = radiation.inverse_square_law(
- initial_intensity=100.0, initial_distance=1.0, new_distance=distance_m,
+ initial_intensity=100.0,
+ initial_distance=1.0,
+ new_distance=distance_m,
)
print(f" {distance_m} m from source : {rate:6.2f} uGy/h")
# Lead at ~100 keV: mu/rho ~5.55 cm^2/g (NIST XCOM), rho = 11.35 g/cm^3.
mu_lead = radiation.linear_attenuation_coefficient(
- mass_attenuation_coefficient=5.55, density=11.35,
+ mass_attenuation_coefficient=5.55,
+ density=11.35,
)
hvl_mm = radiation.half_value_layer(attenuation_coefficient=mu_lead) * 10
print(f" HVL of lead at 100 keV : {hvl_mm:.2f} mm\n")
@@ -42,12 +47,13 @@ def radiography_technique():
"""Adjust a technique chart for a new SID and a grid."""
radiography = Radiography()
mas = radiography.mas(tube_current_ma=200, exposure_time_s=0.05)
- new_mas = radiography.exposure_maintenance_mas(original_mas=mas, original_sid=100,
- new_sid=180)
+ new_mas = radiography.exposure_maintenance_mas(original_mas=mas, original_sid=100, new_sid=180)
gridded = radiography.grid_mas(original_mas=new_mas, original_grid="none", new_grid="12:1")
print("Radiography technique")
print("-" * 48)
- print(f" 10 mAs at 100 cm -> {new_mas:.1f} mAs at 180 cm -> {gridded:.0f} mAs with 12:1 grid\n")
+ print(
+ f" 10 mAs at 100 cm -> {new_mas:.1f} mAs at 180 cm -> {gridded:.0f} mAs with 12:1 grid\n"
+ )
def mri_contrast():
@@ -61,8 +67,10 @@ def mri_contrast():
print("MRI spin-echo signal (relative)")
print("-" * 48)
for name, timing in sequences.items():
- signals = {tissue: mri.spin_echo_signal(**timing, tissue=tissue)
- for tissue in ("fat", "white_matter", "csf")}
+ signals = {
+ tissue: mri.spin_echo_signal(**timing, tissue=tissue)
+ for tissue in ("fat", "white_matter", "csf")
+ }
brightest = max(signals, key=lambda tissue: signals[tissue])
row = " ".join(f"{t}={s:.2f}" for t, s in signals.items())
print(f" {name:20s} {row} -> brightest: {brightest}")
@@ -77,8 +85,9 @@ def ultrasound_doppler():
shift = ultrasound.doppler_shift(transmitted_frequency_mhz=5, velocity=0.5, angle_deg=60)
# If the operator mis-sets the angle cursor, the velocity estimate is wrong.
for assumed_angle in (50, 60, 70):
- v = ultrasound.doppler_velocity(transmitted_frequency_mhz=5, doppler_shift_hz=shift,
- angle_deg=assumed_angle)
+ v = ultrasound.doppler_velocity(
+ transmitted_frequency_mhz=5, doppler_shift_hz=shift, angle_deg=assumed_angle
+ )
print(f" angle cursor at {assumed_angle} deg -> {v:.2f} m/s")
print()
@@ -95,8 +104,10 @@ def ct_windowing():
if value is not None:
print(f" {key:18s}: {value}")
print(f" HU range : {hu.min():.0f} to {hu.max():.0f}")
- print(f" centre pixel : {hu[64, 64]:.0f} HU "
- f"({ct.classify_hounsfield(hounsfield=hu[64, 64])})")
+ print(
+ f" centre pixel : {hu[64, 64]:.0f} HU "
+ f"({ct.classify_hounsfield(hounsfield=hu[64, 64])})"
+ )
presets = ["abdomen", "lung", "bone"]
figure, axes = plt.subplots(1, len(presets) + 1, figsize=(14, 4))
@@ -105,21 +116,22 @@ def ct_windowing():
thickness_cm = np.linspace(0, 10, 200)
for label, mu in {"soft tissue (0.18/cm)": 0.18, "bone (0.48/cm)": 0.48}.items():
axes[0].plot(thickness_cm, np.exp(-mu * thickness_cm) * 100, label=label)
- axes[0].set(title="Attenuation at ~60 keV", xlabel="thickness [cm]",
- ylabel="transmitted [%]")
+ axes[0].set(title="Attenuation at ~60 keV", xlabel="thickness [cm]", ylabel="transmitted [%]")
axes[0].legend()
# Panels 2-4: the same HU data through different windows.
- for axis, preset in zip(axes[1:], presets):
+ for axis, preset in zip(axes[1:], presets, strict=True):
width, level = CT.WINDOW_PRESETS[preset]
- axis.imshow(ct.apply_window(hounsfield=hu, preset=preset), cmap="gray",
- vmin=0, vmax=255)
+ axis.imshow(ct.apply_window(hounsfield=hu, preset=preset), cmap="gray", vmin=0, vmax=255)
axis.set_title(f"{preset} window (W{width} / L{level})")
axis.axis("off")
figure.tight_layout()
- figure.savefig("radiology.png", dpi=120)
- print("\n Saved figure -> radiology.png")
+ # Save next to this script (not wherever it happened to be run from).
+ output_dir = Path(__file__).resolve().parent / "output"
+ output_dir.mkdir(exist_ok=True)
+ figure.savefig(output_dir / "radiology.png", dpi=120)
+ print(f"\n Saved figure -> {output_dir / 'radiology.png'}")
if __name__ == "__main__":
diff --git a/examples/sensor_log_analysis.py b/examples/sensor_log_analysis.py
new file mode 100644
index 0000000..b22a9d2
--- /dev/null
+++ b/examples/sensor_log_analysis.py
@@ -0,0 +1,97 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""sensor_log_analysis.py - from a raw data-logger CSV to a summary report.
+
+QUESTION
+ We have two hours of logged data from two pumps (examples/data/
+ pump_sensor_log.csv - synthetic). What are the average operating points,
+ when did either bearing exceed its 68 degC alarm, and can we hand the
+ results to someone who lives in Excel?
+
+APPROACH (the everyday pandas workflow)
+ 1. Read the CSV; parse the timestamp column as real dates.
+ 2. Clean: count and drop missing readings.
+ 3. Group by pump and summarize (mean / max).
+ 4. Resample to 10-minute averages to see trends without the noise.
+ 5. Filter with a boolean mask to find alarm events.
+ 6. Export a CSV and a multi-sheet Excel workbook, and plot the trend.
+
+Run it: uv run examples/sensor_log_analysis.py
+"""
+
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import pandas as pd
+
+from fpc import Utility
+
+HERE = Path(__file__).resolve().parent
+LOG_FILE = HERE / "data" / "pump_sensor_log.csv"
+OUTPUT_DIR = HERE / "output"
+BEARING_ALARM_C = 68.0
+
+
+def main():
+ # --- 1. Read ----------------------------------------------------------
+ log = Utility().read_table(file_path=LOG_FILE)
+ # CSV has no date type; convert the text column so we can resample by time.
+ log["timestamp"] = pd.to_datetime(log["timestamp"])
+ print(f"Loaded {len(log)} rows, columns: {list(log.columns)}\n")
+
+ # --- 2. Clean ---------------------------------------------------------
+ missing = log.isna().sum()
+ print("Missing values per column:")
+ print(missing[missing > 0].to_string(), "\n")
+ log = log.dropna()
+
+ # --- 3. Group and summarize ------------------------------------------
+ summary = (
+ log.groupby("pump_id")
+ .agg(
+ mean_flow_l_s=("flow_l_s", "mean"),
+ mean_discharge_kpa=("discharge_kpa", "mean"),
+ max_bearing_temp_c=("bearing_temp_c", "max"),
+ )
+ .round(2)
+ )
+ print("Per-pump summary:")
+ print(summary.to_string(), "\n")
+
+ # --- 4. Resample: 10-minute averages per pump -------------------------
+ trend = (
+ log.set_index("timestamp")
+ .groupby("pump_id")["bearing_temp_c"]
+ .resample("10min")
+ .mean()
+ .unstack(level="pump_id") # one column per pump
+ )
+
+ # --- 5. Boolean mask: alarm events -----------------------------------
+ alarms = log[log["bearing_temp_c"] > BEARING_ALARM_C]
+ first_alarm = alarms.groupby("pump_id")["timestamp"].min()
+ print(f"Readings above {BEARING_ALARM_C} degC: {len(alarms)}")
+ for pump_id, when in first_alarm.items():
+ print(f" {pump_id} first alarm at {when:%H:%M:%S}")
+
+ # --- 6. Export and plot ----------------------------------------------
+ OUTPUT_DIR.mkdir(exist_ok=True)
+ summary.to_csv(OUTPUT_DIR / "pump_summary.csv")
+ # ExcelWriter puts several tables in one workbook, one per sheet.
+ with pd.ExcelWriter(OUTPUT_DIR / "pump_report.xlsx") as workbook:
+ summary.to_excel(workbook, sheet_name="summary")
+ trend.to_excel(workbook, sheet_name="bearing_trend_10min")
+ alarms.to_excel(workbook, sheet_name="alarms", index=False)
+
+ axis = trend.plot(figsize=(7, 4), marker="o", ylabel="Bearing temperature [degC]")
+ axis.axhline(BEARING_ALARM_C, color="red", linestyle="--", label="alarm")
+ axis.legend()
+ axis.grid(True, alpha=0.3)
+ plt.tight_layout()
+ plt.savefig(OUTPUT_DIR / "bearing_trend.png", dpi=120)
+ print(f"\nWrote pump_summary.csv, pump_report.xlsx, bearing_trend.png to {OUTPUT_DIR}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/shielding_check.py b/examples/shielding_check.py
new file mode 100644
index 0000000..0084724
--- /dev/null
+++ b/examples/shielding_check.py
@@ -0,0 +1,73 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""shielding_check.py - narrow-beam lead vs concrete shielding estimate.
+
+QUESTION
+ A 100 keV photon beam must be cut to 0.1% (transmission 1e-3). How thick
+ must a lead or a concrete barrier be, and how much does moving the
+ workstation from 2 m to 4 m help?
+
+APPROACH
+ 1. Linear attenuation mu = (mu/rho) * rho from NIST mass attenuation data.
+ 2. Thickness from I/I0 = exp(-mu*x) -> x = -ln(T)/mu.
+ 3. Half- and tenth-value layers for a quick mental check.
+ 4. Inverse-square law for the distance option.
+
+Data: mass attenuation coefficients at 100 keV from NIST X-Ray Mass
+Attenuation Coefficients (Hubbell & Seltzer), https://physics.nist.gov/xaamdi
+
+TEACHING USE ONLY. Real barriers need broad-beam data with build-up, workload
+and occupancy factors (NCRP Report 147) and a qualified medical physicist.
+
+Run it: uv run examples/shielding_check.py
+"""
+
+import math
+
+from fpc import Radiation
+
+radiation = Radiation()
+
+TARGET_TRANSMISSION = 1e-3
+MATERIALS = {
+ # name: (mass attenuation coefficient [cm^2/g] at 100 keV, density [g/cm^3])
+ "lead": (5.549, 11.35),
+ "ordinary concrete": (0.1710, 2.30),
+}
+
+
+def main():
+ print(f"Target transmission: {TARGET_TRANSMISSION:g} at 100 keV\n")
+ for name, (mass_mu, density) in MATERIALS.items():
+ mu = radiation.linear_attenuation_coefficient(
+ mass_attenuation_coefficient=mass_mu,
+ density=density,
+ )
+ thickness = radiation.shielding_thickness(
+ attenuation_coefficient=mu,
+ transmission=TARGET_TRANSMISSION,
+ )
+ hvl = radiation.half_value_layer(attenuation_coefficient=mu)
+ tvl = radiation.tenth_value_layer(attenuation_coefficient=mu)
+ print(
+ f"{name:>18}: mu = {mu:6.2f} /cm, HVL = {hvl * 10:6.2f} mm, "
+ f"TVL = {tvl * 10:6.2f} mm -> {thickness * 10:7.1f} mm needed"
+ )
+
+ # Distance is free shielding: doubling it cuts intensity to a quarter.
+ relative = radiation.inverse_square_law(
+ initial_intensity=1.0,
+ initial_distance=2.0,
+ new_distance=4.0,
+ )
+ # Each HVL halves the intensity, so a factor of 1/relative is log2 of it HVLs.
+ equivalent_hvls = math.log2(1 / relative)
+ print(
+ f"\nMoving from 2 m to 4 m leaves {relative:.0%} of the intensity "
+ f"- the same as adding {equivalent_hvls:.0f} HVLs of shielding."
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/tools/step1_script.py b/examples/tools/step1_script.py
new file mode 100644
index 0000000..af7343d
--- /dev/null
+++ b/examples/tools/step1_script.py
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Rung 1: a script. Inputs are edited in the file; results print.
+
+Perfect for a one-off answer. The limitation: to try another case you edit
+the code. See docs/building-tools.md for the whole ladder.
+
+Run it: uv run examples/tools/step1_script.py
+"""
+
+from fpc import Fluids, Properties
+
+fluids, props = Fluids(), Properties()
+
+TEMPERATURE_C = 20.0
+result = fluids.pipe_flow(
+ flow=0.012, # m^3/s
+ diameter=0.1023, # m
+ length=120.0, # m
+ roughness=0.045e-3, # m, commercial steel
+ density=props.water_liquid(temperature_c=TEMPERATURE_C, property="density_kg_m3"),
+ viscosity=props.water_liquid(temperature_c=TEMPERATURE_C, property="dynamic_viscosity_pa_s"),
+)
+for name, value in result.items():
+ print(f"{name:>16}: {value:.4g}")
diff --git a/examples/tools/step2_cli.py b/examples/tools/step2_cli.py
new file mode 100644
index 0000000..4709b25
--- /dev/null
+++ b/examples/tools/step2_cli.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Rung 2: a command-line tool. Inputs come from arguments, not code edits.
+
+argparse (standard library) turns flags into Python values, checks their
+types, and writes the --help text for you. Now anyone can run new cases, and
+you can call it from batch files or other programs.
+
+Run it:
+ uv run examples/tools/step2_cli.py --help
+ uv run examples/tools/step2_cli.py --flow-l-s 12 --diameter-mm 102.3 --length-m 120
+"""
+
+import argparse
+
+from fpc import Fluids, Properties
+
+
+def build_parser():
+ """Describe the inputs: name, type, default, and help text for each."""
+ parser = argparse.ArgumentParser(
+ description="Pressure drop of water flowing through a straight pipe.",
+ )
+ # Units go in the flag names, so nobody has to guess.
+ parser.add_argument("--flow-l-s", type=float, required=True, help="flow rate [L/s]")
+ parser.add_argument("--diameter-mm", type=float, required=True, help="inner diameter [mm]")
+ parser.add_argument("--length-m", type=float, required=True, help="pipe length [m]")
+ parser.add_argument(
+ "--roughness-mm",
+ type=float,
+ default=0.045,
+ help="absolute roughness [mm] (default: commercial steel 0.045)",
+ )
+ parser.add_argument(
+ "--temperature-c", type=float, default=20.0, help="water temperature [degC] (default: 20)"
+ )
+ return parser
+
+
+def main(argv=None):
+ """Parse arguments, convert to SI, calculate, print.
+
+ ``argv=None`` means "use the real command line"; tests can pass a list.
+ """
+ args = build_parser().parse_args(argv)
+ props = Properties()
+ result = Fluids().pipe_flow(
+ flow=args.flow_l_s / 1000,
+ diameter=args.diameter_mm / 1000,
+ length=args.length_m,
+ roughness=args.roughness_mm / 1000,
+ density=props.water_liquid(temperature_c=args.temperature_c, property="density_kg_m3"),
+ viscosity=props.water_liquid(
+ temperature_c=args.temperature_c, property="dynamic_viscosity_pa_s"
+ ),
+ )
+ print(f"velocity {result['velocity']:.3f} m/s")
+ print(f"Reynolds {result['reynolds']:.0f}")
+ print(f"friction factor {result['friction_factor']:.4f}")
+ print(f"head loss {result['head_loss']:.3f} m")
+ print(f"pressure drop {result['pressure_drop'] / 1000:.2f} kPa")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/tools/step3_gui.py b/examples/tools/step3_gui.py
new file mode 100644
index 0000000..3122caa
--- /dev/null
+++ b/examples/tools/step3_gui.py
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Rung 3: a desktop form with tkinter (ships with Python).
+
+Labelled entry boxes and a Calculate button - usable by people who will
+never open a terminal. For an open-ended calculator, see the project's
+gui.py console instead; this is the "single-purpose app" version.
+
+Run it: uv run examples/tools/step3_gui.py
+"""
+
+import tkinter as tk
+from tkinter import ttk
+
+from fpc import Fluids, Properties
+
+# (label, default) for each input - adding a field is one line here.
+FIELDS = {
+ "flow_l_s": ("Flow [L/s]", 12.0),
+ "diameter_mm": ("Inner diameter [mm]", 102.3),
+ "length_m": ("Length [m]", 120.0),
+ "roughness_mm": ("Roughness [mm]", 0.045),
+ "temperature_c": ("Water temperature [degC]", 20.0),
+}
+
+
+def calculate(values):
+ """Same calculation as rungs 1 and 2: the GUI is only a new front end."""
+ props = Properties()
+ t = values["temperature_c"]
+ return Fluids().pipe_flow(
+ flow=values["flow_l_s"] / 1000,
+ diameter=values["diameter_mm"] / 1000,
+ length=values["length_m"],
+ roughness=values["roughness_mm"] / 1000,
+ density=props.water_liquid(temperature_c=t, property="density_kg_m3"),
+ viscosity=props.water_liquid(temperature_c=t, property="dynamic_viscosity_pa_s"),
+ )
+
+
+def main():
+ root = tk.Tk()
+ root.title("Pipe pressure drop")
+ frame = ttk.Frame(root, padding=12)
+ frame.grid()
+
+ entries = {}
+ for row, (key, (label, default)) in enumerate(FIELDS.items()):
+ ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=2)
+ entry = ttk.Entry(frame, width=12)
+ entry.insert(0, str(default))
+ entry.grid(row=row, column=1, pady=2)
+ entries[key] = entry
+
+ output = tk.StringVar(value="Press Calculate")
+ ttk.Label(frame, textvariable=output, justify="left").grid(
+ row=len(FIELDS) + 1, column=0, columnspan=2, sticky="w", pady=(8, 0)
+ )
+
+ def on_calculate():
+ # Never trust typed input: a GUI must handle "abc" gracefully.
+ try:
+ values = {key: float(entry.get()) for key, entry in entries.items()}
+ r = calculate(values)
+ except ValueError as error:
+ output.set(f"Input error: {error}")
+ return
+ output.set(
+ f"Velocity: {r['velocity']:.2f} m/s\n"
+ f"Reynolds: {r['reynolds']:.0f}\n"
+ f"Friction factor: {r['friction_factor']:.4f}\n"
+ f"Pressure drop: {r['pressure_drop'] / 1000:.2f} kPa"
+ )
+
+ ttk.Button(frame, text="Calculate", command=on_calculate).grid(
+ row=len(FIELDS), column=0, columnspan=2, pady=(8, 0)
+ )
+ root.mainloop()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/tools/step4_web_app.py b/examples/tools/step4_web_app.py
new file mode 100644
index 0000000..3655a78
--- /dev/null
+++ b/examples/tools/step4_web_app.py
@@ -0,0 +1,63 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Rung 4: a web app with Streamlit - share a link instead of a file.
+
+Streamlit reruns this whole script top to bottom every time a widget
+changes, so the code reads like a plain script: widgets *return* values.
+Install the optional dependency group first.
+
+Run it:
+ uv sync --group apps
+ uv run streamlit run examples/tools/step4_web_app.py
+"""
+
+import numpy as np
+import pandas as pd
+import streamlit as st
+
+from fpc import Fluids, Properties
+
+fluids, props = Fluids(), Properties()
+
+st.title("Pipe pressure drop")
+st.caption("Darcy-Weisbach with the Colebrook friction factor - water at 1 atm.")
+
+# Sidebar inputs: each widget returns its current value.
+with st.sidebar:
+ flow_l_s = st.number_input("Flow [L/s]", min_value=0.1, value=12.0)
+ diameter_mm = st.number_input("Inner diameter [mm]", min_value=1.0, value=102.3)
+ length_m = st.number_input("Length [m]", min_value=0.1, value=120.0)
+ roughness_mm = st.number_input("Roughness [mm]", min_value=0.0, value=0.045, format="%.4f")
+ temperature_c = st.slider("Water temperature [degC]", 1, 99, 20)
+
+density = props.water_liquid(temperature_c=temperature_c, property="density_kg_m3")
+viscosity = props.water_liquid(temperature_c=temperature_c, property="dynamic_viscosity_pa_s")
+
+
+def run(flow_l_s):
+ return fluids.pipe_flow(
+ flow=flow_l_s / 1000,
+ diameter=diameter_mm / 1000,
+ length=length_m,
+ roughness=roughness_mm / 1000,
+ density=density,
+ viscosity=viscosity,
+ )
+
+
+result = run(flow_l_s)
+left, middle, right = st.columns(3)
+left.metric("Velocity", f"{result['velocity']:.2f} m/s")
+middle.metric("Reynolds", f"{result['reynolds']:,.0f}")
+right.metric("Pressure drop", f"{result['pressure_drop'] / 1000:.2f} kPa")
+
+# A curve is more useful than one number: dp across a range of flows.
+flows = np.linspace(0.1 * flow_l_s, 2 * flow_l_s, 40)
+curve = pd.DataFrame(
+ {
+ "flow [L/s]": flows,
+ "pressure drop [kPa]": [run(q)["pressure_drop"] / 1000 for q in flows],
+ }
+).set_index("flow [L/s]")
+st.line_chart(curve)
diff --git a/examples/vectorization.py b/examples/vectorization.py
new file mode 100644
index 0000000..eaf4e58
--- /dev/null
+++ b/examples/vectorization.py
@@ -0,0 +1,94 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""vectorization.py - loops vs NumPy arrays, side by side.
+
+QUESTION
+ A shaft has 1,000,000 measured (or simulated) load cases. For each one,
+ what is the bending stress, and which cases exceed yield?
+
+LESSON
+ Coming from Excel or plain Python, the instinct is to loop over every
+ case. NumPy lets you write the formula once and apply it to the whole
+ array - "vectorization". It is shorter, closer to the math, and often
+ tens of times faster because the loop runs in compiled C, not Python.
+
+ Three ideas to take away:
+ 1. Vectorized arithmetic: stress = moment * c / I works on arrays.
+ 2. Broadcasting: a (N,1) column with a (M,) row gives an (N,M)
+ grid - every combination, no nested loops.
+ 3. Boolean masking: stress[stress > yield] picks the failures.
+
+Run it: uv run examples/vectorization.py
+"""
+
+import time
+
+import numpy as np
+
+YIELD_STRENGTH = 250e6 # Pa
+DIAMETER = 0.04 # m, solid round shaft
+SECOND_MOMENT = np.pi * DIAMETER**4 / 64
+FIBRE_DISTANCE = DIAMETER / 2
+
+rng = np.random.default_rng(seed=0)
+moments = rng.normal(loc=1000, scale=300, size=1_000_000) # N*m, one per case
+
+
+def with_loop():
+ """The plain-Python way: one case at a time."""
+ stresses = []
+ for moment in moments:
+ stresses.append(moment * FIBRE_DISTANCE / SECOND_MOMENT)
+ over = 0
+ for stress in stresses:
+ if stress > YIELD_STRENGTH:
+ over += 1
+ return over
+
+
+def vectorized():
+ """The NumPy way: the formula, applied to every case at once."""
+ stresses = moments * FIBRE_DISTANCE / SECOND_MOMENT
+ return int((stresses > YIELD_STRENGTH).sum()) # True counts as 1
+
+
+def timed(function):
+ start = time.perf_counter()
+ result = function()
+ return result, time.perf_counter() - start
+
+
+def main():
+ loop_count, loop_time = timed(with_loop)
+ vec_count, vec_time = timed(vectorized)
+ assert loop_count == vec_count # same answer, different speed
+ print(f"Cases above yield: {vec_count} of {len(moments):,}")
+ print(f" Python loop : {loop_time * 1000:8.1f} ms")
+ print(f" NumPy : {vec_time * 1000:8.1f} ms ({loop_time / vec_time:.0f}x faster)\n")
+
+ # --- Boolean masking: pull out just the failing cases ----------------
+ stresses = moments * FIBRE_DISTANCE / SECOND_MOMENT
+ failing = stresses[stresses > YIELD_STRENGTH]
+ print(
+ f"Worst stress {failing.max() / 1e6:.0f} MPa; "
+ f"mean of the failures {failing.mean() / 1e6:.0f} MPa"
+ )
+
+ # --- Broadcasting: a design table over two variables -----------------
+ # Column of diameters (shape (5, 1)) against a row of moments (shape (4,)).
+ diameters = np.array([0.030, 0.035, 0.040, 0.045, 0.050])[:, np.newaxis]
+ design_moments = np.array([500, 1000, 1500, 2000])
+ # NumPy stretches both to (5, 4): every diameter x every moment.
+ table = design_moments * (diameters / 2) / (np.pi * diameters**4 / 64) / 1e6
+ print("\nStress [MPa] (rows: diameter, columns: moment [N*m])")
+ print(" d [mm] " + "".join(f"{m:>8}" for m in design_moments))
+ for d, row in zip(diameters.ravel(), table, strict=True):
+ # Mark cells above yield with '*'.
+ cells = "".join(f"{s:7.0f}{'*' if s > YIELD_STRENGTH / 1e6 else ' '}" for s in row)
+ print(f" {d * 1000:6.0f} {cells}")
+ print(" (* = above yield)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/library/__init__.py b/fpc/__init__.py
similarity index 54%
rename from library/__init__.py
rename to fpc/__init__.py
index 3161c79..0591f73 100644
--- a/library/__init__.py
+++ b/fpc/__init__.py
@@ -6,8 +6,8 @@
Each module in this package holds one class for one engineering domain. Import
the class you need, create an instance, and call its methods::
- from library.dynamics import Dynamics
- from library.statics import Statics
+ from fpc.dynamics import Dynamics
+ from fpc.statics import Statics
dyn = Dynamics()
x = dyn.displacement_constant_acceleration(
@@ -20,31 +20,35 @@
"""
# Re-export the domain classes so callers can also write
-# ``from library import Dynamics``. Keeping this list in one place documents
+# ``from fpc import Dynamics``. Keeping this list in one place documents
# everything the library currently offers.
-from library.dynamics import Dynamics
-from library.statics import Statics
-from library.modal import Modal
-from library.thermo import Thermo
-from library.fluids import Fluids
-from library.material import Material
-from library.gear import Gear
-from library.electrical import Electrical
-from library.mechatronics import Mechatronics
-from library.controls import Controls
-from library.utility import Utility
+from fpc.beam import Beam
+from fpc.controls import Controls
+from fpc.ct import CT
+
+# Design (root finding / optimization) and reference-data lookups.
+from fpc.design import Design
+from fpc.dicom import Dicom
+from fpc.dynamics import Dynamics
+from fpc.electrical import Electrical
+from fpc.fluids import Fluids
+from fpc.gear import Gear
+from fpc.material import Material
+from fpc.mechatronics import Mechatronics
+from fpc.modal import Modal
+from fpc.mri import MRI
+from fpc.properties import Properties
# Medical imaging / radiology classes.
-from library.radiation import Radiation
-from library.radiography import Radiography
-from library.ct import CT
-from library.mri import MRI
-from library.ultrasound import Ultrasound
-from library.dicom import Dicom
+from fpc.radiation import Radiation
+from fpc.radiography import Radiography
+from fpc.statics import Statics
+from fpc.thermo import Thermo
+from fpc.ultrasound import Ultrasound
# Unit-aware modelling: the shared Pint registry and the Beam dataclass.
-from library.units import ureg, Q_
-from library.beam import Beam
+from fpc.units import Q_, ureg
+from fpc.utility import Utility
__all__ = [
"Dynamics",
@@ -58,6 +62,8 @@
"Mechatronics",
"Controls",
"Utility",
+ "Design",
+ "Properties",
"Radiation",
"Radiography",
"CT",
diff --git a/library/beam.py b/fpc/beam.py
similarity index 88%
rename from library/beam.py
rename to fpc/beam.py
index bed94d3..8b39d3d 100644
--- a/library/beam.py
+++ b/fpc/beam.py
@@ -13,8 +13,8 @@
Build one like this::
- from library.units import Q_
- from library.beam import Beam
+ from fpc.units import Q_
+ from fpc.beam import Beam
beam = Beam(
length=Q_(2.0, "m"),
@@ -34,12 +34,11 @@
from pint import Quantity
-from library.units import Q_
-
+from fpc.units import Q_
# A sensible default material so a beam can be created with just its geometry.
-STEEL_MODULUS = Q_(200, "GPa") # Young's modulus of structural steel
-STEEL_DENSITY = Q_(7850, "kg/m**3") # density of structural steel
+STEEL_MODULUS = Q_(200, "GPa") # Young's modulus of structural steel
+STEEL_DENSITY = Q_(7850, "kg/m**3") # density of structural steel
@dataclass
@@ -81,9 +80,7 @@ def _require(quantity, dimensionality, name):
"""Raise a helpful error unless ``quantity`` is positive with the right units."""
# ``.check()`` confirms the quantity has the expected dimensionality.
if not hasattr(quantity, "check") or not quantity.check(dimensionality):
- raise ValueError(
- f"{name} must be a Pint quantity with dimensionality {dimensionality}"
- )
+ raise ValueError(f"{name} must be a Pint quantity with dimensionality {dimensionality}")
if quantity.magnitude <= 0:
raise ValueError(f"{name} must be positive, got {quantity}")
@@ -100,7 +97,7 @@ def moment_of_inertia(self):
This is the property that resists bending; a taller section (bigger h)
is dramatically stiffer because h is cubed.
"""
- return self.width * self.height ** 3 / 12
+ return self.width * self.height**3 / 12
@property
def mass(self):
@@ -118,7 +115,7 @@ def max_deflection_point_load(self, *, load):
:returns: maximum deflection (a ``[length]`` quantity)
"""
self._require(load, "[force]", "load")
- deflection = load * self.length ** 3 / (48 * self.modulus * self.moment_of_inertia)
+ deflection = load * self.length**3 / (48 * self.modulus * self.moment_of_inertia)
# Reduce to base units so the result reads cleanly as metres.
return deflection.to_base_units()
@@ -148,10 +145,10 @@ def euler_buckling_load(self, *, end_factor=1.0):
:returns: critical axial load (a ``[force]`` quantity)
"""
# Second moments about both axes; buckling follows the smaller one.
- inertia_strong = self.width * self.height ** 3 / 12
- inertia_weak = self.height * self.width ** 3 / 12
+ inertia_strong = self.width * self.height**3 / 12
+ inertia_weak = self.height * self.width**3 / 12
inertia_min = min(inertia_strong, inertia_weak)
effective_length = end_factor * self.length
- load = math.pi ** 2 * self.modulus * inertia_min / effective_length ** 2
+ load = math.pi**2 * self.modulus * inertia_min / effective_length**2
return load.to_base_units()
diff --git a/fpc/cli.py b/fpc/cli.py
new file mode 100644
index 0000000..60c83ad
--- /dev/null
+++ b/fpc/cli.py
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""cli.py - the ``fpc`` command: library calculations from the terminal.
+
+Rung 5 of docs/building-tools.md: a command that is *installed with the
+package*. ``pyproject.toml`` maps the name to this module's ``main``::
+
+ [project.scripts]
+ fpc = "fpc.cli:main"
+
+so after installing (``uv sync`` here, or ``uv tool install
+git+https://github.com/kovirlabs/first-principles-code`` anywhere) you can run::
+
+ fpc --help
+ fpc pipe-flow --flow-l-s 12 --diameter-mm 102.3 --length-m 120
+ fpc water --temperature-c 37
+
+Subcommands (``argparse`` "subparsers") let one program offer several tools,
+the way ``git commit`` and ``git push`` share the ``git`` command.
+"""
+
+import argparse
+
+from fpc.fluids import Fluids
+from fpc.properties import Properties
+
+
+def _pipe_flow(args):
+ """Handle ``fpc pipe-flow``."""
+ props = Properties()
+ t = args.temperature_c
+ result = Fluids().pipe_flow(
+ flow=args.flow_l_s / 1000,
+ diameter=args.diameter_mm / 1000,
+ length=args.length_m,
+ roughness=args.roughness_mm / 1000,
+ density=props.water_liquid(temperature_c=t, property="density_kg_m3"),
+ viscosity=props.water_liquid(temperature_c=t, property="dynamic_viscosity_pa_s"),
+ )
+ print(f"velocity {result['velocity']:.3f} m/s")
+ print(f"Reynolds {result['reynolds']:.0f}")
+ print(f"friction factor {result['friction_factor']:.4f}")
+ print(f"head loss {result['head_loss']:.3f} m")
+ print(f"pressure drop {result['pressure_drop'] / 1000:.2f} kPa")
+
+
+def _water(args):
+ """Handle ``fpc water``: print every liquid-water property at a temperature."""
+ props = Properties()
+ columns = props.load_table(name="water_liquid_1atm").columns.drop("temperature_c")
+ print(f"Liquid water at {args.temperature_c} degC, 1 atm")
+ for column in columns:
+ value = props.water_liquid(temperature_c=args.temperature_c, property=column)
+ print(f" {column:<28} {value:.5g}")
+
+
+def build_parser():
+ """Build the top-level parser with one subparser per tool."""
+ parser = argparse.ArgumentParser(
+ prog="fpc",
+ description="First Principles Code engineering calculations.",
+ )
+ tools = parser.add_subparsers(dest="tool", required=True)
+
+ pipe = tools.add_parser("pipe-flow", help="pressure drop of water in a straight pipe")
+ pipe.add_argument("--flow-l-s", type=float, required=True, help="flow rate [L/s]")
+ pipe.add_argument("--diameter-mm", type=float, required=True, help="inner diameter [mm]")
+ pipe.add_argument("--length-m", type=float, required=True, help="pipe length [m]")
+ pipe.add_argument(
+ "--roughness-mm",
+ type=float,
+ default=0.045,
+ help="absolute roughness [mm] (default 0.045, commercial steel)",
+ )
+ pipe.add_argument(
+ "--temperature-c", type=float, default=20.0, help="water temperature [degC] (default 20)"
+ )
+ # set_defaults(handler=...) attaches the function that runs this tool.
+ pipe.set_defaults(handler=_pipe_flow)
+
+ water = tools.add_parser("water", help="liquid water properties at 1 atm")
+ water.add_argument(
+ "--temperature-c", type=float, required=True, help="temperature [degC], 1 to 99"
+ )
+ water.set_defaults(handler=_water)
+ return parser
+
+
+def main(argv=None):
+ """Entry point for the ``fpc`` command.
+
+ :param argv: list of arguments (defaults to the real command line);
+ tests pass a list so they do not need a subprocess
+ """
+ args = build_parser().parse_args(argv)
+ args.handler(args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/library/controls.py b/fpc/controls.py
similarity index 88%
rename from library/controls.py
rename to fpc/controls.py
index 2cdd695..b733efb 100644
--- a/library/controls.py
+++ b/fpc/controls.py
@@ -43,7 +43,7 @@ def damping_from_overshoot(self, *, percent_overshoot):
:returns: required damping ratio zeta [-]
"""
ln_os = math.log(percent_overshoot / 100.0)
- return -ln_os / math.sqrt(math.pi ** 2 + ln_os ** 2)
+ return -ln_os / math.sqrt(math.pi**2 + ln_os**2)
def settling_time(self, *, damping_ratio, natural_frequency, tolerance=0.02):
"""Approximate settling time of a second-order system.
@@ -59,8 +59,16 @@ def settling_time(self, *, damping_ratio, natural_frequency, tolerance=0.02):
return -math.log(tolerance) / (damping_ratio * natural_frequency)
def simulate_pid_first_order(
- self, *, setpoint, time_constant, gain, kp, ki=0.0, kd=0.0,
- dt=0.01, duration=10.0,
+ self,
+ *,
+ setpoint,
+ time_constant,
+ gain,
+ kp,
+ ki=0.0,
+ kd=0.0,
+ dt=0.01,
+ duration=10.0,
):
"""Simulate a PID loop controlling a first-order plant.
@@ -82,8 +90,8 @@ def simulate_pid_first_order(
time = np.linspace(0.0, duration, steps)
output = np.zeros(steps)
- integral = 0.0 # running sum of error*dt
- previous_error = 0.0 # for the derivative term
+ integral = 0.0 # running sum of error*dt
+ previous_error = 0.0 # for the derivative term
# March forward one time step at a time.
for i in range(1, steps):
@@ -93,8 +101,12 @@ def simulate_pid_first_order(
# Controller decides the command u from the current error state.
command = self.pid_output(
- error=error, integral=integral, derivative=derivative,
- kp=kp, ki=ki, kd=kd,
+ error=error,
+ integral=integral,
+ derivative=derivative,
+ kp=kp,
+ ki=ki,
+ kd=kd,
)
# Plant responds: Euler step of y' = (gain*u - y) / tau.
diff --git a/library/ct.py b/fpc/ct.py
similarity index 92%
rename from library/ct.py
rename to fpc/ct.py
index 8787949..995e353 100644
--- a/library/ct.py
+++ b/fpc/ct.py
@@ -64,7 +64,7 @@ def rescale_to_hounsfield(self, *, stored_values, rescale_slope=1.0, rescale_int
DICOM files usually store unsigned integers; the RescaleSlope and
RescaleIntercept tags (0028,1053 / 0028,1052) say how to recover HU.
Forgetting this step is a classic beginner bug: every value is off by
- ~1024. See :class:`library.dicom.Dicom` for reading these from a file.
+ ~1024. See :class:`fpc.dicom.Dicom` for reading these from a file.
:param stored_values: raw pixel values (number or array)
:param rescale_slope: DICOM RescaleSlope
@@ -88,8 +88,9 @@ def window_bounds(self, *, window_width=None, window_level=None, preset=None):
width, level = self._resolve_window(window_width, window_level, preset)
return level - width / 2.0, level + width / 2.0
- def apply_window(self, *, hounsfield, window_width=None, window_level=None, preset=None,
- output_max=255):
+ def apply_window(
+ self, *, hounsfield, window_width=None, window_level=None, preset=None, output_max=255
+ ):
"""Map HU values to display grey levels for a chosen window.
Values are clipped to the window, then scaled linearly to 0..output_max.
@@ -105,7 +106,9 @@ def apply_window(self, *, hounsfield, window_width=None, window_level=None, pres
:returns: numpy array of grey levels (uint8 when output_max == 255)
"""
lower, upper = self.window_bounds(
- window_width=window_width, window_level=window_level, preset=preset,
+ window_width=window_width,
+ window_level=window_level,
+ preset=preset,
)
hu = np.asarray(hounsfield, dtype=float)
# np.clip and the arithmetic below run over the whole image at once -
@@ -160,8 +163,9 @@ def effective_dose_from_dlp(self, *, dlp_mgy_cm, region):
:returns: effective dose estimate [mSv]
"""
if region not in self.DLP_TO_EFFECTIVE_DOSE:
- raise ValueError(f"unknown region {region!r}; use one of: "
- f"{', '.join(self.DLP_TO_EFFECTIVE_DOSE)}")
+ raise ValueError(
+ f"unknown region {region!r}; use one of: {', '.join(self.DLP_TO_EFFECTIVE_DOSE)}"
+ )
return self.DLP_TO_EFFECTIVE_DOSE[region] * dlp_mgy_cm
# ------------------------------------------------------------------
@@ -175,8 +179,9 @@ def _resolve_window(self, width, level, preset):
"""
if preset is not None:
if preset not in self.WINDOW_PRESETS:
- raise ValueError(f"unknown preset {preset!r}; use one of: "
- f"{', '.join(self.WINDOW_PRESETS)}")
+ raise ValueError(
+ f"unknown preset {preset!r}; use one of: {', '.join(self.WINDOW_PRESETS)}"
+ )
return self.WINDOW_PRESETS[preset]
if width is None or level is None:
raise ValueError("give window_width and window_level, or a preset")
diff --git a/fpc/data/README.md b/fpc/data/README.md
new file mode 100644
index 0000000..aa51241
--- /dev/null
+++ b/fpc/data/README.md
@@ -0,0 +1,22 @@
+
+
+
+# Reference data
+
+Small, cited tables that `fpc.Properties` loads with pandas, so the library
+reads reference values from data instead of hard-coding constants. They live
+inside the `fpc` package (not the repo root) so they ship when the package is
+installed elsewhere.
+
+> **Teaching data.** Values are typical/nominal. For real design work, use the
+> governing standard or your supplier's certified data.
+
+| File | Contents | Source |
+| --- | --- | --- |
+| `water_saturation.csv` | Saturated water vs temperature: pressure, liquid/vapor density, enthalpies | IAPWS-95 formulation, computed with [CoolProp](http://www.coolprop.org/) 8.0.0 by `generate_water_tables.py`. Matches the saturated-water tables in Çengel & Boles, *Thermodynamics*, Table A-4. |
+| `water_liquid_1atm.csv` | Liquid water at 101.325 kPa: density, viscosity, specific heat, conductivity | Same as above (IAPWS-95 / IAPWS 2008 viscosity / IAPWS 2011 conductivity via CoolProp). |
+| `materials.csv` | Density, E, yield & ultimate strength, CTE, thermal conductivity for common metals and one polymer | Typical room-temperature values from [MatWeb](https://www.matweb.com/) / ASM International data sheets. Strengths are typical, **not** minimum design allowables. |
+| `w_shapes.csv` | A selection of AISC wide-flange (W) beams: weight, area, depth, Ix, Sx (US units) | [AISC Shapes Database v16.0](https://www.aisc.org/publications/steel-construction-manual-resources/16th-ed-steel-construction-manual/aisc-shapes-database-v16.0/). Check `tests/test_properties.py`, which verifies Sx ≈ Ix/(d/2) and weight ≈ 3.4·A for every row. |
+
+To regenerate the water tables: `uv run --with CoolProp python fpc/data/generate_water_tables.py`
+(run it from `fpc/data/`).
diff --git a/fpc/data/generate_water_tables.py b/fpc/data/generate_water_tables.py
new file mode 100644
index 0000000..f23fb80
--- /dev/null
+++ b/fpc/data/generate_water_tables.py
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Regenerate the water property CSVs in this folder from CoolProp (IAPWS-95).
+
+Run from this folder: uv run --with CoolProp python generate_water_tables.py
+
+CoolProp is not a project dependency; it is only needed to rebuild the tables.
+Keeping the generator next to the data documents exactly where every number
+came from.
+"""
+
+import CoolProp
+from CoolProp.CoolProp import PropsSI
+
+print("CoolProp", CoolProp.__version__)
+
+# Saturated water: both phases coexist, so temperature alone fixes the state.
+temps = [0.01, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100, 120, 140, 160, 180, 200]
+with open("water_saturation.csv", "w") as f:
+ f.write(
+ "temperature_c,pressure_kpa,density_liquid_kg_m3,density_vapor_kg_m3,"
+ "enthalpy_liquid_kj_kg,enthalpy_vaporization_kj_kg,enthalpy_vapor_kj_kg\n"
+ )
+ for t in temps:
+ T = t + 273.15
+ # "Q" is vapor quality: 0 = saturated liquid, 1 = saturated vapor.
+ p = PropsSI("P", "T", T, "Q", 0, "Water") / 1e3
+ rl = PropsSI("D", "T", T, "Q", 0, "Water")
+ rv = PropsSI("D", "T", T, "Q", 1, "Water")
+ hl = PropsSI("H", "T", T, "Q", 0, "Water") / 1e3
+ hv = PropsSI("H", "T", T, "Q", 1, "Water") / 1e3
+ f.write(f"{t},{p:.4g},{rl:.2f},{rv:.5g},{hl:.2f},{hv - hl:.1f},{hv:.1f}\n")
+
+# Compressed liquid at 1 atm: what you need for everyday pipe-flow problems.
+with open("water_liquid_1atm.csv", "w") as f:
+ f.write(
+ "temperature_c,density_kg_m3,dynamic_viscosity_pa_s,"
+ "specific_heat_j_kg_k,thermal_conductivity_w_m_k\n"
+ )
+ for t in [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 99]:
+ T, P = t + 273.15, 101325
+ f.write(
+ f"{t},{PropsSI('D', 'T', T, 'P', P, 'Water'):.2f},"
+ f"{PropsSI('V', 'T', T, 'P', P, 'Water'):.4e},"
+ f"{PropsSI('C', 'T', T, 'P', P, 'Water'):.1f},"
+ f"{PropsSI('L', 'T', T, 'P', P, 'Water'):.4f}\n"
+ )
diff --git a/fpc/data/materials.csv b/fpc/data/materials.csv
new file mode 100644
index 0000000..051bc62
--- /dev/null
+++ b/fpc/data/materials.csv
@@ -0,0 +1,11 @@
+name,category,density_kg_m3,youngs_modulus_gpa,yield_strength_mpa,ultimate_strength_mpa,thermal_expansion_per_k,thermal_conductivity_w_m_k
+steel_a36,steel,7850,200,250,400,1.17e-05,51.9
+steel_1018_cold_drawn,steel,7870,205,370,440,1.19e-05,51.9
+stainless_304,stainless steel,8000,193,215,505,1.73e-05,16.2
+stainless_316,stainless steel,8000,193,205,515,1.60e-05,16.3
+aluminum_6061_t6,aluminum,2700,68.9,276,310,2.36e-05,167
+aluminum_7075_t6,aluminum,2810,71.7,503,572,2.36e-05,130
+titanium_ti6al4v,titanium,4430,113.8,880,950,8.6e-06,6.7
+copper_c11000_annealed,copper,8890,115,69,220,1.70e-05,388
+brass_c26000_annealed,copper alloy,8530,110,75,300,1.99e-05,120
+nylon_66,polymer,1140,2.9,82,82,8.0e-05,0.25
diff --git a/fpc/data/w_shapes.csv b/fpc/data/w_shapes.csv
new file mode 100644
index 0000000..3474488
--- /dev/null
+++ b/fpc/data/w_shapes.csv
@@ -0,0 +1,13 @@
+designation,weight_lb_ft,area_in2,depth_in,ix_in4,sx_in3
+W6x9,9,2.68,5.90,16.4,5.56
+W8x18,18,5.26,8.14,61.9,15.2
+W8x31,31,9.13,8.00,110,27.5
+W10x12,12,3.54,9.87,53.8,10.9
+W10x22,22,6.49,10.2,118,23.2
+W12x14,14,4.16,11.9,88.6,14.9
+W12x26,26,7.65,12.2,204,33.4
+W14x22,22,6.49,13.7,199,29.0
+W16x26,26,7.68,15.7,301,38.4
+W18x35,35,10.3,17.7,510,57.6
+W21x44,44,13.0,20.7,843,81.6
+W24x55,55,16.2,23.6,1350,114
diff --git a/fpc/data/water_liquid_1atm.csv b/fpc/data/water_liquid_1atm.csv
new file mode 100644
index 0000000..64dd910
--- /dev/null
+++ b/fpc/data/water_liquid_1atm.csv
@@ -0,0 +1,16 @@
+temperature_c,density_kg_m3,dynamic_viscosity_pa_s,specific_heat_j_kg_k,thermal_conductivity_w_m_k
+1,999.90,1.7310e-03,4216.1,0.5582
+5,999.97,1.5182e-03,4205.0,0.5678
+10,999.70,1.3059e-03,4195.2,0.5788
+15,999.10,1.1376e-03,4188.5,0.5888
+20,998.21,1.0016e-03,4184.1,0.5980
+25,997.05,8.9002e-04,4181.3,0.6065
+30,995.65,7.9722e-04,4179.8,0.6144
+35,994.03,7.1913e-04,4179.3,0.6217
+40,992.22,6.5273e-04,4179.4,0.6285
+50,988.04,5.4652e-04,4181.3,0.6406
+60,983.20,4.6604e-04,4185.0,0.6510
+70,977.76,4.0355e-04,4190.1,0.6598
+80,971.79,3.5405e-04,4196.8,0.6670
+90,965.31,3.1418e-04,4205.2,0.6728
+99,959.07,2.8457e-04,4214.5,0.6768
diff --git a/fpc/data/water_saturation.csv b/fpc/data/water_saturation.csv
new file mode 100644
index 0000000..fc7de45
--- /dev/null
+++ b/fpc/data/water_saturation.csv
@@ -0,0 +1,22 @@
+temperature_c,pressure_kpa,density_liquid_kg_m3,density_vapor_kg_m3,enthalpy_liquid_kj_kg,enthalpy_vaporization_kj_kg,enthalpy_vapor_kj_kg
+0.01,0.6117,999.79,0.0048546,0.00,2500.9,2500.9
+5,0.8726,999.92,0.0068022,21.02,2489.0,2510.1
+10,1.228,999.65,0.0094071,42.02,2477.2,2519.2
+15,1.706,999.06,0.012841,62.98,2465.4,2528.3
+20,2.339,998.16,0.017314,83.91,2453.5,2537.4
+25,3.17,997.00,0.023075,104.83,2441.7,2546.5
+30,4.247,995.61,0.030415,125.73,2429.8,2555.5
+35,5.629,993.99,0.039674,146.63,2417.9,2564.5
+40,7.385,992.18,0.051242,167.53,2406.0,2573.5
+45,9.595,990.17,0.065565,188.43,2394.0,2582.4
+50,12.35,988.00,0.083147,209.34,2381.9,2591.3
+60,19.95,983.16,0.13043,251.18,2357.7,2608.8
+70,31.2,977.73,0.19843,293.07,2333.0,2626.1
+80,47.41,971.77,0.29367,335.01,2308.0,2643.0
+90,70.18,965.30,0.4239,377.04,2282.5,2659.5
+100,101.4,958.35,0.59817,419.17,2256.4,2675.6
+120,198.7,943.11,1.1221,503.81,2202.1,2705.9
+140,361.5,926.13,1.9667,589.16,2144.3,2733.4
+160,618.2,907.45,3.2596,675.47,2082.0,2757.4
+180,1003,887.00,5.1588,763.05,2014.2,2777.2
+200,1555,864.66,7.861,852.27,1939.7,2792.0
diff --git a/fpc/design.py b/fpc/design.py
new file mode 100644
index 0000000..7322ee8
--- /dev/null
+++ b/fpc/design.py
@@ -0,0 +1,227 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""design.py - turn "analyze this part" into "find the part that works".
+
+Most library methods answer *analysis* questions: given a beam, how much does
+it deflect? Real engineering usually asks the reverse *design* question: what
+size beam deflects no more than 5 mm? This class shows the two SciPy tools
+that answer design questions:
+
+ * **Root finding** (``scipy.optimize.brentq``) - solve one equation for one
+ unknown: "find h so that deflection(h) - limit = 0".
+ * **Constrained optimization** (``scipy.optimize.minimize``) - choose
+ several variables to minimize a goal (mass) while satisfying limits
+ (stress, deflection) and staying inside bounds (sizes you can buy).
+
+Both work on any function you can write in Python, so the same pattern scales
+from a textbook beam to a full simulation.
+
+All inputs are plain SI floats (m, N, Pa, kg/m^3). The beam is simply supported
+with a central point load and a solid rectangular cross-section, matching
+``fpc.Beam``.
+"""
+
+import numpy as np
+from scipy.optimize import brentq, minimize
+
+
+class Design:
+ """Design-by-calculation helpers: size and optimize simple parts."""
+
+ # --- Formulas the design methods search over -------------------------
+ @staticmethod
+ def _deflection(*, load, length, modulus, width, height):
+ """Midspan deflection of a simply supported beam: P*L^3 / (48*E*I)."""
+ moment_of_inertia = width * height**3 / 12
+ return load * length**3 / (48 * modulus * moment_of_inertia)
+
+ @staticmethod
+ def _bending_stress(*, load, length, width, height):
+ """Peak bending stress: M*c/I with M = P*L/4, c = h/2 -> 1.5*P*L/(b*h^2)."""
+ return 1.5 * load * length / (width * height**2)
+
+ # --- Root finding -----------------------------------------------------
+ def rectangular_beam_height_for_deflection(
+ self,
+ *,
+ load,
+ length,
+ modulus,
+ width,
+ max_deflection,
+ min_height=1e-3,
+ max_height=10.0,
+ ):
+ """Find the beam height that gives exactly the allowed deflection.
+
+ We write the requirement as a function that is zero at the answer::
+
+ residual(h) = deflection(h) - max_deflection
+
+ A short beam deflects too much (residual > 0); a very tall one barely
+ deflects (residual < 0). Because the sign changes between
+ ``min_height`` and ``max_height``, ``brentq`` is guaranteed to find the
+ crossing. (This particular equation *can* be solved by hand, which is
+ exactly why it is a good first example - the test suite checks the
+ numerical answer against the algebra.)
+
+ :param load: central point load P [N]
+ :param length: span L [m]
+ :param modulus: Young's modulus E [Pa]
+ :param width: section width b [m]
+ :param max_deflection: allowed midspan deflection [m]
+ :param min_height: lower end of the search bracket [m]
+ :param max_height: upper end of the search bracket [m]
+ :returns: the minimum section height h [m]
+ :raises ValueError: if the answer is not inside the bracket
+ """
+
+ def residual(height):
+ return (
+ self._deflection(
+ load=load,
+ length=length,
+ modulus=modulus,
+ width=width,
+ height=height,
+ )
+ - max_deflection
+ )
+
+ if residual(min_height) * residual(max_height) > 0:
+ raise ValueError("no sign change between min_height and max_height; widen the bracket")
+ return float(brentq(residual, min_height, max_height, xtol=1e-12))
+
+ # --- Constrained optimization ----------------------------------------
+ def lightest_rectangular_beam(
+ self,
+ *,
+ load,
+ length,
+ modulus,
+ density,
+ allowable_stress,
+ max_deflection,
+ width_bounds=(0.01, 0.5),
+ height_bounds=(0.01, 1.0),
+ max_aspect_ratio=4.0,
+ ):
+ """Find the lightest rectangular beam that is strong and stiff enough.
+
+ Optimization problem (the vocabulary is worth learning):
+
+ * **Design variables** x = [width b, height h].
+ * **Objective** (minimize): mass = density * b * h * L.
+ * **Constraints** (must hold): bending stress <= allowable_stress,
+ deflection <= max_deflection, and height <= max_aspect_ratio * width.
+ SciPy's convention is that each
+ ``"ineq"`` constraint function must be **>= 0** when satisfied, so
+ we write them as ``limit - actual``.
+ * **Bounds**: the sizes you are willing to consider.
+
+ SLSQP (Sequential Least SQuares Programming) is the SciPy method that
+ handles bounds plus inequality constraints. We divide each constraint
+ by its limit so both are "fractions of the limit" - optimizers behave
+ far better when numbers are of similar size.
+
+ Why the aspect-ratio limit? Without it the "best" beam is an infinitely
+ thin, infinitely tall plank - mathematically optimal, physically
+ useless (it would buckle sideways). Optimizers exploit every gap in
+ your problem statement, so constraints must capture what you really
+ mean. Constraints that end up exactly at their limit are reported as
+ ``active``: those are the requirements that drive the design.
+
+ :param load: central point load P [N]
+ :param length: span L [m]
+ :param modulus: Young's modulus E [Pa]
+ :param density: material density [kg/m^3]
+ :param allowable_stress: maximum allowed bending stress [Pa]
+ :param max_deflection: maximum allowed midspan deflection [m]
+ :param width_bounds: (min, max) section width [m]
+ :param height_bounds: (min, max) section height [m]
+ :param max_aspect_ratio: largest allowed height / width, defaults to 4
+ (a common rule of thumb against lateral-torsional buckling)
+ :returns: dict with ``width``, ``height``, ``mass``, ``stress``,
+ ``deflection`` (SI units), and which constraints are ``active``
+ :raises RuntimeError: if the optimizer fails to find a feasible design
+ """
+
+ def mass(x):
+ width, height = x
+ return density * width * height * length
+
+ # Scale the objective to "about 1" by dividing by the starting mass.
+ # Gradient-based optimizers converge far more reliably on well-scaled
+ # numbers than on raw values that may be 1e-3 or 1e4.
+ start = [np.mean(width_bounds), np.mean(height_bounds)]
+ reference_mass = mass(start)
+
+ def scaled_mass(x):
+ return mass(x) / reference_mass
+
+ def stress_margin(x):
+ width, height = x
+ stress = self._bending_stress(load=load, length=length, width=width, height=height)
+ return 1 - stress / allowable_stress
+
+ def deflection_margin(x):
+ width, height = x
+ deflection = self._deflection(
+ load=load,
+ length=length,
+ modulus=modulus,
+ width=width,
+ height=height,
+ )
+ return 1 - deflection / max_deflection
+
+ def aspect_margin(x):
+ width, height = x
+ return 1 - height / (max_aspect_ratio * width)
+
+ # Start in the middle of the allowed box; the optimizer walks from here.
+ result = minimize(
+ scaled_mass,
+ x0=start,
+ method="SLSQP",
+ bounds=[width_bounds, height_bounds],
+ constraints=[
+ {"type": "ineq", "fun": stress_margin},
+ {"type": "ineq", "fun": deflection_margin},
+ {"type": "ineq", "fun": aspect_margin},
+ ],
+ options={"ftol": 1e-12, "maxiter": 500},
+ )
+ # Always check .success - a "result" from a failed solve is garbage.
+ if not result.success:
+ raise RuntimeError(f"optimization failed: {result.message}")
+
+ width, height = result.x
+ active = [
+ name
+ for name, margin in (
+ ("stress", stress_margin(result.x)),
+ ("deflection", deflection_margin(result.x)),
+ ("aspect_ratio", aspect_margin(result.x)),
+ )
+ if margin < 1e-4 # within 0.01% of the limit -> this limit governs
+ ]
+ return {
+ "width": float(width),
+ "height": float(height),
+ "mass": float(mass(result.x)),
+ "stress": float(
+ self._bending_stress(load=load, length=length, width=width, height=height)
+ ),
+ "deflection": float(
+ self._deflection(
+ load=load,
+ length=length,
+ modulus=modulus,
+ width=width,
+ height=height,
+ )
+ ),
+ "active": active,
+ }
diff --git a/library/dicom.py b/fpc/dicom.py
similarity index 100%
rename from library/dicom.py
rename to fpc/dicom.py
diff --git a/library/dynamics.py b/fpc/dynamics.py
similarity index 92%
rename from library/dynamics.py
rename to fpc/dynamics.py
index 705520a..6fb1d42 100644
--- a/library/dynamics.py
+++ b/fpc/dynamics.py
@@ -16,7 +16,6 @@
import math
-import numpy as np
from scipy.integrate import solve_ivp
@@ -44,11 +43,7 @@ def displacement_constant_acceleration(
:param time: elapsed time t [s]
:returns: position x at time t [m]
"""
- return (
- initial_position
- + initial_velocity * time
- + 0.5 * acceleration * time ** 2
- )
+ return initial_position + initial_velocity * time + 0.5 * acceleration * time**2
def final_velocity(self, *, initial_velocity, acceleration, time):
"""Velocity after constant acceleration: v = v0 + a*t.
@@ -71,7 +66,7 @@ def velocity_from_distance(self, *, initial_velocity, acceleration, distance):
:returns: final speed v [m/s] (always non-negative)
"""
# max(..., 0.0) guards against tiny negative values from rounding.
- return math.sqrt(max(initial_velocity ** 2 + 2 * acceleration * distance, 0.0))
+ return math.sqrt(max(initial_velocity**2 + 2 * acceleration * distance, 0.0))
def projectile_range(self, *, speed, angle_deg, gravity=GRAVITY):
"""Horizontal range of a projectile launched over flat ground.
@@ -85,7 +80,7 @@ def projectile_range(self, *, speed, angle_deg, gravity=GRAVITY):
"""
# Trig functions in ``math`` expect radians, so convert first.
angle_rad = math.radians(angle_deg)
- return speed ** 2 * math.sin(2 * angle_rad) / gravity
+ return speed**2 * math.sin(2 * angle_rad) / gravity
def projectile_time_of_flight(self, *, speed, angle_deg, gravity=GRAVITY):
"""Time a projectile spends in the air over flat ground.
@@ -116,7 +111,7 @@ def kinetic_energy(self, *, mass, velocity):
:param velocity: speed [m/s]
:returns: kinetic energy [J]
"""
- return 0.5 * mass * velocity ** 2
+ return 0.5 * mass * velocity**2
def momentum(self, *, mass, velocity):
"""Linear momentum: p = m*v.
@@ -134,11 +129,19 @@ def centripetal_acceleration(self, *, speed, radius):
:param radius: radius of the circular path [m]
:returns: centripetal acceleration [m/s^2]
"""
- return speed ** 2 / radius
+ return speed**2 / radius
def simulate_projectile_drag(
- self, *, speed, angle_deg, mass, drag_coefficient, frontal_area,
- air_density=1.225, gravity=GRAVITY, max_time=60.0,
+ self,
+ *,
+ speed,
+ angle_deg,
+ mass,
+ drag_coefficient,
+ frontal_area,
+ air_density=1.225,
+ gravity=GRAVITY,
+ max_time=60.0,
):
"""Simulate 2-D projectile flight WITH quadratic air drag.
@@ -193,7 +196,7 @@ def hit_ground(t, state):
t_span=(0.0, max_time),
y0=[0.0, 0.0, vx0, vy0],
events=hit_ground,
- max_step=0.01, # small steps keep the trajectory smooth
+ max_step=0.01, # small steps keep the trajectory smooth
dense_output=False,
)
diff --git a/library/electrical.py b/fpc/electrical.py
similarity index 100%
rename from library/electrical.py
rename to fpc/electrical.py
diff --git a/fpc/fluids.py b/fpc/fluids.py
new file mode 100644
index 0000000..b93d4b1
--- /dev/null
+++ b/fpc/fluids.py
@@ -0,0 +1,200 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""fluids.py - fluid dynamics calculations.
+
+Fluid mechanics governs anything that flows: pipes, pumps, wings, and weather.
+These helpers cover the core dimensionless groups and conservation laws -
+Reynolds number, the continuity equation, Bernoulli's equation, dynamic
+pressure, and pipe friction losses.
+
+``colebrook_friction_factor`` is also the project's root-finding lesson: the
+Colebrook equation cannot be rearranged for f, so we hand it to
+``scipy.optimize.brentq`` and let the computer find the f that satisfies it.
+"""
+
+import math
+
+from scipy.optimize import brentq
+
+
+class Fluids:
+ """Fluid dynamics helpers."""
+
+ # Acceleration due to gravity [m/s^2], used by head/pressure relations.
+ GRAVITY = 9.80665
+
+ def reynolds_number(self, *, density, velocity, length, viscosity):
+ """Reynolds number: Re = rho*v*L / mu.
+
+ Re is the ratio of inertial to viscous forces and tells you whether
+ flow is laminar (Re < ~2300 in a pipe) or turbulent (Re > ~4000).
+
+ :param density: fluid density rho [kg/m^3]
+ :param velocity: flow velocity v [m/s]
+ :param length: characteristic length L (e.g. pipe diameter) [m]
+ :param viscosity: dynamic viscosity mu [Pa*s]
+ :returns: Reynolds number [-]
+ """
+ return density * velocity * length / viscosity
+
+ def continuity_velocity(self, *, area_in, velocity_in, area_out):
+ """Outlet velocity from conservation of mass: A1*v1 = A2*v2.
+
+ For an incompressible fluid, narrowing the pipe speeds the flow up.
+
+ :param area_in: inlet cross-sectional area A1 [m^2]
+ :param velocity_in: inlet velocity v1 [m/s]
+ :param area_out: outlet cross-sectional area A2 [m^2]
+ :returns: outlet velocity v2 [m/s]
+ """
+ return area_in * velocity_in / area_out
+
+ def dynamic_pressure(self, *, density, velocity):
+ """Dynamic pressure of a moving fluid: q = 1/2 * rho * v^2.
+
+ :param density: fluid density rho [kg/m^3]
+ :param velocity: flow velocity v [m/s]
+ :returns: dynamic pressure [Pa]
+ """
+ return 0.5 * density * velocity**2
+
+ def bernoulli_pressure(
+ self,
+ *,
+ density,
+ velocity_1,
+ height_1,
+ pressure_1,
+ velocity_2,
+ height_2,
+ gravity=GRAVITY,
+ ):
+ """Downstream static pressure from Bernoulli's equation.
+
+ Along a streamline (no friction, incompressible):
+ P + 1/2*rho*v^2 + rho*g*h = constant.
+ Given state 1 fully and the velocity/height at state 2, solve for P2.
+
+ :param density: fluid density rho [kg/m^3]
+ :param velocity_1: velocity at point 1 [m/s]
+ :param height_1: elevation at point 1 [m]
+ :param pressure_1: static pressure at point 1 [Pa]
+ :param velocity_2: velocity at point 2 [m/s]
+ :param height_2: elevation at point 2 [m]
+ :param gravity: gravitational acceleration [m/s^2]
+ :returns: static pressure at point 2 [Pa]
+ """
+ total_head = pressure_1 + 0.5 * density * velocity_1**2 + density * gravity * height_1
+ return total_head - 0.5 * density * velocity_2**2 - density * gravity * height_2
+
+ def pipe_head_loss(self, *, friction_factor, length, diameter, velocity, gravity=GRAVITY):
+ """Major head loss in a pipe (Darcy-Weisbach equation).
+
+ h_f = f * (L/D) * v^2 / (2*g).
+
+ :param friction_factor: Darcy friction factor f [-]
+ :param length: pipe length L [m]
+ :param diameter: pipe inner diameter D [m]
+ :param velocity: average flow velocity v [m/s]
+ :param gravity: gravitational acceleration [m/s^2]
+ :returns: head loss [m of fluid]
+ """
+ return friction_factor * (length / diameter) * velocity**2 / (2 * gravity)
+
+ def swamee_jain_friction_factor(self, *, reynolds, relative_roughness):
+ """Explicit approximation to the Colebrook friction factor (Swamee-Jain).
+
+ f = 0.25 / [log10(eps/(3.7*D) + 5.74/Re^0.9)]^2, within about 1% of
+ Colebrook for 5e3 < Re < 1e8 and 1e-6 < eps/D < 1e-2. Handy as a quick
+ estimate, and as the starting guess for the exact solve below.
+
+ :param reynolds: Reynolds number Re [-] (turbulent flow)
+ :param relative_roughness: pipe roughness / diameter, eps/D [-]
+ :returns: Darcy friction factor f [-]
+ """
+ return 0.25 / math.log10(relative_roughness / 3.7 + 5.74 / reynolds**0.9) ** 2
+
+ def colebrook_friction_factor(self, *, reynolds, relative_roughness):
+ """Darcy friction factor from the Colebrook equation, solved numerically.
+
+ Colebrook (turbulent pipe flow) is *implicit* - f appears on both sides:
+
+ 1/sqrt(f) = -2 * log10( (eps/D)/3.7 + 2.51 / (Re * sqrt(f)) )
+
+ There is no algebra that isolates f, so we turn it into a root-finding
+ problem: define residual(f) = left side - right side and ask
+ ``scipy.optimize.brentq`` for the f where residual(f) = 0. Brent's
+ method needs a bracket [a, b] where the residual changes sign; every
+ turbulent pipe has 0.001 < f < 0.2, so that bracket always works.
+
+ For laminar flow (Re < 2300) the exact answer is simply f = 64/Re.
+
+ :param reynolds: Reynolds number Re [-]
+ :param relative_roughness: pipe roughness / diameter, eps/D [-]
+ (e.g. commercial steel eps = 0.045 mm)
+ :returns: Darcy friction factor f [-]
+ :raises ValueError: for non-positive Re or negative roughness
+ """
+ if reynolds <= 0:
+ raise ValueError("reynolds must be positive")
+ if relative_roughness < 0:
+ raise ValueError("relative_roughness cannot be negative")
+ if reynolds < 2300:
+ # Laminar: Hagen-Poiseuille gives an exact closed form.
+ return 64.0 / reynolds
+
+ def residual(f):
+ """Colebrook rearranged so the correct f makes this zero."""
+ return 1 / math.sqrt(f) + 2 * math.log10(
+ relative_roughness / 3.7 + 2.51 / (reynolds * math.sqrt(f))
+ )
+
+ # brentq is guaranteed to converge once the root is bracketed, which
+ # is why it is the default choice for "solve this one equation".
+ return brentq(residual, 0.001, 0.2, xtol=1e-12)
+
+ def pipe_flow(self, *, flow, diameter, length, roughness, density, viscosity):
+ """Everything about steady flow through one straight pipe, in one call.
+
+ Chains the methods above: velocity from continuity, Reynolds number,
+ Colebrook friction factor, Darcy-Weisbach head loss, and the pressure
+ drop dp = rho * g * h_f. Returning a dict (instead of a single number)
+ is handy when a report, CLI, or web app wants to show all of it.
+
+ :param flow: volumetric flow rate Q [m^3/s]
+ :param diameter: pipe inner diameter D [m]
+ :param length: pipe length L [m]
+ :param roughness: absolute pipe roughness eps [m]
+ (commercial steel ~0.045e-3, drawn tubing ~0.0015e-3)
+ :param density: fluid density rho [kg/m^3]
+ :param viscosity: dynamic viscosity mu [Pa*s]
+ :returns: dict with ``velocity`` [m/s], ``reynolds`` [-],
+ ``friction_factor`` [-], ``head_loss`` [m], ``pressure_drop`` [Pa]
+ """
+ if flow <= 0 or diameter <= 0 or length <= 0:
+ raise ValueError("flow, diameter, and length must be positive")
+ velocity = flow / (math.pi * diameter**2 / 4)
+ reynolds = self.reynolds_number(
+ density=density,
+ velocity=velocity,
+ length=diameter,
+ viscosity=viscosity,
+ )
+ friction_factor = self.colebrook_friction_factor(
+ reynolds=reynolds,
+ relative_roughness=roughness / diameter,
+ )
+ head_loss = self.pipe_head_loss(
+ friction_factor=friction_factor,
+ length=length,
+ diameter=diameter,
+ velocity=velocity,
+ )
+ return {
+ "velocity": velocity,
+ "reynolds": reynolds,
+ "friction_factor": friction_factor,
+ "head_loss": head_loss,
+ "pressure_drop": density * self.GRAVITY * head_loss,
+ }
diff --git a/library/gear.py b/fpc/gear.py
similarity index 99%
rename from library/gear.py
rename to fpc/gear.py
index b8b1a1f..b7be8d7 100644
--- a/library/gear.py
+++ b/fpc/gear.py
@@ -8,8 +8,6 @@
a ratio, and how to combine stages into an overall powertrain ratio.
"""
-import math
-
class Gear:
"""Gear and powertrain calculations."""
diff --git a/library/material.py b/fpc/material.py
similarity index 97%
rename from library/material.py
rename to fpc/material.py
index 1e9f16b..30c5252 100644
--- a/library/material.py
+++ b/fpc/material.py
@@ -86,4 +86,4 @@ def euler_buckling_load(self, *, modulus, moment_of_inertia, length, end_factor=
:returns: critical axial load [N]
"""
effective_length = end_factor * length
- return math.pi ** 2 * modulus * moment_of_inertia / effective_length ** 2
+ return math.pi**2 * modulus * moment_of_inertia / effective_length**2
diff --git a/library/mechatronics.py b/fpc/mechatronics.py
similarity index 93%
rename from library/mechatronics.py
rename to fpc/mechatronics.py
index a5def89..3f241b0 100644
--- a/library/mechatronics.py
+++ b/fpc/mechatronics.py
@@ -62,7 +62,7 @@ def reflected_inertia(self, *, load_inertia, gear_ratio):
:param gear_ratio: gear reduction ratio [-]
:returns: reflected inertia at the motor shaft [kg*m^2]
"""
- return load_inertia / gear_ratio ** 2
+ return load_inertia / gear_ratio**2
def acceleration_torque(self, *, inertia, angular_acceleration):
"""Torque to angularly accelerate an inertia: T = J * alpha.
@@ -75,7 +75,18 @@ def acceleration_torque(self, *, inertia, angular_acceleration):
"""
return inertia * angular_acceleration
- def calculate_inertia_ratio(self, *, j_motor, mass, pitch, teeth_motor_pulley, teeth_screw_pulley, j_screw, j_pulley_motor, j_pulley_screw):
+ def calculate_inertia_ratio(
+ self,
+ *,
+ j_motor,
+ mass,
+ pitch,
+ teeth_motor_pulley,
+ teeth_screw_pulley,
+ j_screw,
+ j_pulley_motor,
+ j_pulley_screw,
+ ):
"""
Calculates the inertia ratio of a servomotor driving a ballscrew via a timing belt.
@@ -115,7 +126,9 @@ def calculate_inertia_ratio(self, *, j_motor, mass, pitch, teeth_motor_pulley, t
"reduction_ratio": reduction_ratio,
}
- def _calculate_acceleration_torque(self, *, j_total, j_motor, linear_acceleration, pitch, reduction_ratio):
+ def _calculate_acceleration_torque(
+ self, *, j_total, j_motor, linear_acceleration, pitch, reduction_ratio
+ ):
"""
Uses linear acceleration to find the required motor acceleration torque.
diff --git a/library/modal.py b/fpc/modal.py
similarity index 93%
rename from library/modal.py
rename to fpc/modal.py
index 1918149..050fd14 100644
--- a/library/modal.py
+++ b/fpc/modal.py
@@ -63,7 +63,7 @@ def damped_natural_frequency(self, *, stiffness, mass, damping):
"""
omega_n, _ = self.natural_frequency(stiffness=stiffness, mass=mass)
zeta = self.damping_ratio(damping=damping, mass=mass, stiffness=stiffness)
- return omega_n * math.sqrt(1 - zeta ** 2)
+ return omega_n * math.sqrt(1 - zeta**2)
def mode_shapes(self, *, mass_matrix, stiffness_matrix):
"""Natural frequencies and mode shapes of a multi-DOF system.
@@ -93,8 +93,15 @@ def mode_shapes(self, *, mass_matrix, stiffness_matrix):
return frequencies_hz[order], eigenvectors[:, order]
def simulate_free_vibration(
- self, *, mass, stiffness, damping, initial_displacement,
- initial_velocity=0.0, duration, num_points=500,
+ self,
+ *,
+ mass,
+ stiffness,
+ damping,
+ initial_displacement,
+ initial_velocity=0.0,
+ duration,
+ num_points=500,
):
"""Simulate the free ring-down of an SDOF spring-mass-damper.
@@ -112,6 +119,7 @@ def simulate_free_vibration(
:param num_points: number of evenly spaced output samples
:returns: tuple of numpy arrays (time [s], displacement [m])
"""
+
def equations_of_motion(t, state):
"""d/dt of [x, v] for x' = v, v' = -(c*v + k*x)/m."""
x, v = state
@@ -152,15 +160,15 @@ def fit_damped_response(self, *, time, displacement, guess_frequency_hz=1.0):
displacement = np.asarray(displacement, dtype=float)
def model(t, amplitude, zeta, omega_n, phase):
- omega_d = omega_n * np.sqrt(1 - zeta ** 2)
+ omega_d = omega_n * np.sqrt(1 - zeta**2)
return amplitude * np.exp(-zeta * omega_n * t) * np.cos(omega_d * t + phase)
# A good initial guess (p0) is what makes nonlinear fitting converge.
initial_guess = [
- float(np.max(np.abs(displacement))), # amplitude
- 0.05, # lightly damped
- 2 * math.pi * guess_frequency_hz, # angular frequency
- 0.0, # phase
+ float(np.max(np.abs(displacement))), # amplitude
+ 0.05, # lightly damped
+ 2 * math.pi * guess_frequency_hz, # angular frequency
+ 0.0, # phase
]
params, _covariance = curve_fit(model, time, displacement, p0=initial_guess)
amplitude, zeta, omega_n, _phase = params
diff --git a/library/mri.py b/fpc/mri.py
similarity index 96%
rename from library/mri.py
rename to fpc/mri.py
index f78352a..49c2704 100644
--- a/library/mri.py
+++ b/fpc/mri.py
@@ -78,8 +78,16 @@ def t2_decay(self, *, time_ms, t2_ms, initial_magnetization=1.0):
"""
return initial_magnetization * math.exp(-time_ms / t2_ms)
- def spin_echo_signal(self, *, repetition_time_ms, echo_time_ms, t1_ms=None, t2_ms=None,
- proton_density=1.0, tissue=None):
+ def spin_echo_signal(
+ self,
+ *,
+ repetition_time_ms,
+ echo_time_ms,
+ t1_ms=None,
+ t2_ms=None,
+ proton_density=1.0,
+ tissue=None,
+ ):
"""Relative spin-echo signal S = PD * (1 - exp(-TR/T1)) * exp(-TE/T2).
TR controls how much T1 recovery happens between pulses; TE controls
diff --git a/fpc/properties.py b/fpc/properties.py
new file mode 100644
index 0000000..073ab9b
--- /dev/null
+++ b/fpc/properties.py
@@ -0,0 +1,195 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""properties.py - look up reference data from cited tables.
+
+Engineers spend a lot of time reading values off tables: steam tables,
+material data sheets, steel-section catalogues. This class loads those tables
+from the CSV files in ``fpc/data/`` with **pandas** and answers questions about
+them. It teaches three things:
+
+ * **pandas basics** - read a CSV into a DataFrame, select rows with a
+ boolean mask, sort, and turn a row into a plain dict.
+ * **Interpolation** - a table only lists some temperatures; to get a value
+ in between you interpolate. ``np.interp`` does straight-line (linear)
+ interpolation; ``scipy.interpolate.CubicSpline`` fits a smooth curve.
+ * **Data, not constants** - keeping numbers in cited data files (see
+ ``fpc/data/README.md``) instead of buried in code makes them easy to
+ check, update, and trust.
+
+Example::
+
+ from fpc import Properties
+
+ props = Properties()
+ props.water_liquid(temperature_c=37, property="dynamic_viscosity_pa_s")
+ props.material(name="aluminum_6061_t6")["yield_strength_mpa"] # 276
+ props.lightest_w_shape(required_sx_in3=30)["designation"] # 'W16x26'
+"""
+
+from functools import cache
+from importlib import resources
+
+import numpy as np
+import pandas as pd
+from scipy.interpolate import CubicSpline
+
+
+class Properties:
+ """Reference-table lookups (water, materials, steel sections) with interpolation."""
+
+ # The tables shipped in fpc/data/, keyed by a short name.
+ TABLES = {
+ "water_saturation": "water_saturation.csv",
+ "water_liquid_1atm": "water_liquid_1atm.csv",
+ "materials": "materials.csv",
+ "w_shapes": "w_shapes.csv",
+ }
+
+ def list_tables(self):
+ """Return the names of the available reference tables.
+
+ :returns: list of table names you can pass to :meth:`load_table`
+ """
+ return list(self.TABLES)
+
+ def load_table(self, *, name):
+ """Load one reference table as a pandas DataFrame.
+
+ A DataFrame is a spreadsheet-like table: named columns, numbered rows.
+ Try ``frame.head()``, ``frame.describe()``, or ``frame["column"]``.
+
+ :param name: a table name from :meth:`list_tables`
+ :returns: a fresh ``pandas.DataFrame`` (safe to modify)
+ :raises ValueError: if the table name is unknown
+ """
+ if name not in self.TABLES:
+ raise ValueError(f"unknown table {name!r}; choose from {self.list_tables()}")
+ # Return a copy so a caller editing their DataFrame cannot corrupt the
+ # cached original that every other call shares.
+ return _read_table(self.TABLES[name]).copy()
+
+ # --- Interpolation ----------------------------------------------------
+ def interpolate(self, *, table, x_column, y_column, x, method="linear"):
+ """Interpolate ``y_column`` at ``x`` using any loaded table.
+
+ This is the general tool the water lookups below are built on.
+
+ :param table: a table name (str) or a DataFrame you loaded yourself
+ :param x_column: the column to look up by (must be increasing)
+ :param y_column: the column whose value you want
+ :param x: the lookup value - a number or a NumPy array of numbers
+ :param method: ``"linear"`` (np.interp, like reading a printed table by
+ hand) or ``"cubic"`` (scipy CubicSpline, smoother for curved data)
+ :returns: the interpolated value(s), same shape as ``x``
+ :raises ValueError: if ``x`` is outside the table (we refuse to
+ extrapolate, because guessing beyond the data is a common source
+ of silently wrong answers)
+ """
+ frame = self.load_table(name=table) if isinstance(table, str) else table
+ xs = frame[x_column].to_numpy(dtype=float)
+ ys = frame[y_column].to_numpy(dtype=float)
+
+ # Guard against extrapolation: np.interp would quietly clamp to the
+ # end values, and a spline would happily invent numbers.
+ x_array = np.asarray(x, dtype=float)
+ if np.any(x_array < xs.min()) or np.any(x_array > xs.max()):
+ raise ValueError(
+ f"{x_column}={x} is outside the table range "
+ f"[{xs.min()}, {xs.max()}]; refusing to extrapolate"
+ )
+
+ if method == "linear":
+ result = np.interp(x_array, xs, ys)
+ elif method == "cubic":
+ result = CubicSpline(xs, ys)(x_array)
+ else:
+ raise ValueError("method must be 'linear' or 'cubic'")
+ # Hand back a plain float for a scalar input, an array otherwise.
+ return float(result) if result.ndim == 0 else result
+
+ def water_saturation(self, *, temperature_c, property, method="linear"):
+ """Saturated-water property at a temperature (a steam-table lookup).
+
+ Columns: ``pressure_kpa``, ``density_liquid_kg_m3``,
+ ``density_vapor_kg_m3``, ``enthalpy_liquid_kj_kg``,
+ ``enthalpy_vaporization_kj_kg``, ``enthalpy_vapor_kj_kg``.
+
+ :param temperature_c: temperature [degC], 0.01 to 200
+ :param property: one of the column names above
+ :param method: ``"linear"`` or ``"cubic"``
+ :returns: the property value in the units named by the column
+ """
+ return self.interpolate(
+ table="water_saturation",
+ x_column="temperature_c",
+ y_column=property,
+ x=temperature_c,
+ method=method,
+ )
+
+ def water_liquid(self, *, temperature_c, property, method="linear"):
+ """Liquid water property at 1 atm and a given temperature.
+
+ Columns: ``density_kg_m3``, ``dynamic_viscosity_pa_s``,
+ ``specific_heat_j_kg_k``, ``thermal_conductivity_w_m_k``.
+
+ :param temperature_c: temperature [degC], 1 to 99
+ :param property: one of the column names above
+ :param method: ``"linear"`` or ``"cubic"``
+ :returns: the property value in the units named by the column
+ """
+ return self.interpolate(
+ table="water_liquid_1atm",
+ x_column="temperature_c",
+ y_column=property,
+ x=temperature_c,
+ method=method,
+ )
+
+ # --- Row lookups ------------------------------------------------------
+ def material(self, *, name):
+ """Return one material's properties as a plain dictionary.
+
+ :param name: the ``name`` column value, e.g. ``"steel_a36"``
+ :returns: dict of column -> value (units are in the column names)
+ :raises ValueError: if no material has that name
+ """
+ frame = self.load_table(name="materials")
+ # A boolean mask: a True/False Series, one entry per row. Indexing the
+ # DataFrame with it keeps only the True rows.
+ match = frame[frame["name"] == name]
+ if match.empty:
+ raise ValueError(f"unknown material {name!r}; choose from {list(frame['name'])}")
+ return match.iloc[0].to_dict()
+
+ def lightest_w_shape(self, *, required_sx_in3):
+ """Pick the lightest AISC W-shape with enough section modulus.
+
+ Beam selection in one pandas chain: filter to sections that are strong
+ enough, sort by weight, take the first row.
+
+ :param required_sx_in3: minimum elastic section modulus Sx [in^3]
+ :returns: dict describing the chosen section
+ :raises ValueError: if nothing in the table is strong enough
+ """
+ shapes = self.load_table(name="w_shapes")
+ candidates = shapes[shapes["sx_in3"] >= required_sx_in3]
+ if candidates.empty:
+ raise ValueError(f"no W-shape in the table has Sx >= {required_sx_in3} in^3")
+ # Sort by weight, then by Sx (descending) so ties go to the stronger beam.
+ best = candidates.sort_values(["weight_lb_ft", "sx_in3"], ascending=[True, False]).iloc[0]
+ return best.to_dict()
+
+
+@cache
+def _read_table(file_name):
+ """Read a CSV from the package's data folder (cached after the first read).
+
+ ``importlib.resources`` finds files *inside an installed package*, so this
+ works whether fpc is run from this repo or installed into another project.
+ ``functools.cache`` remembers the result so each file is parsed only once.
+ """
+ path = resources.files("fpc").joinpath("data", file_name)
+ with path.open("r", encoding="utf-8") as handle:
+ return pd.read_csv(handle)
diff --git a/library/radiation.py b/fpc/radiation.py
similarity index 93%
rename from library/radiation.py
rename to fpc/radiation.py
index 552102a..04d20a5 100644
--- a/library/radiation.py
+++ b/fpc/radiation.py
@@ -25,10 +25,10 @@ class Radiation:
# missing on purpose: their w_R depends on energy, so they get a method of
# their own (see :meth:`neutron_weighting_factor`).
RADIATION_WEIGHTING_FACTORS = {
- "photon": 1.0, # x-rays and gamma rays
- "electron": 1.0, # beta particles, muons
+ "photon": 1.0, # x-rays and gamma rays
+ "electron": 1.0, # beta particles, muons
"proton": 2.0,
- "alpha": 20.0, # also fission fragments and heavy ions
+ "alpha": 20.0, # also fission fragments and heavy ions
}
# ICRP 103 tissue weighting factors w_T. They describe what fraction of the
@@ -39,7 +39,7 @@ class Radiation:
"lung": 0.12,
"stomach": 0.12,
"breast": 0.12,
- "remainder": 0.12, # 13 remaining tissues, averaged together
+ "remainder": 0.12, # 13 remaining tissues, averaged together
"gonads": 0.08,
"bladder": 0.04,
"oesophagus": 0.04,
@@ -207,10 +207,10 @@ def neutron_weighting_factor(self, *, energy_mev):
raise ValueError("neutron energy must be positive")
# Three branches straight from ICRP 103, eq. (4.3).
if energy_mev < 1.0:
- return 2.5 + 18.2 * math.exp(-math.log(energy_mev) ** 2 / 6)
+ return 2.5 + 18.2 * math.exp(-(math.log(energy_mev) ** 2) / 6)
if energy_mev <= 50.0:
- return 5.0 + 17.0 * math.exp(-math.log(2 * energy_mev) ** 2 / 6)
- return 2.5 + 3.25 * math.exp(-math.log(0.04 * energy_mev) ** 2 / 6)
+ return 5.0 + 17.0 * math.exp(-(math.log(2 * energy_mev) ** 2) / 6)
+ return 2.5 + 3.25 * math.exp(-(math.log(0.04 * energy_mev) ** 2) / 6)
def equivalent_dose(self, *, absorbed_dose, radiation_type):
"""Equivalent dose H_T = w_R * D_T.
@@ -227,7 +227,10 @@ def equivalent_dose(self, *, absorbed_dose, radiation_type):
weight = self.RADIATION_WEIGHTING_FACTORS[radiation_type]
except KeyError:
valid = ", ".join(self.RADIATION_WEIGHTING_FACTORS)
- raise ValueError(f"unknown radiation_type {radiation_type!r}; use one of: {valid}")
+ # "from None" hides the internal KeyError: the ValueError says it all.
+ raise ValueError(
+ f"unknown radiation_type {radiation_type!r}; use one of: {valid}"
+ ) from None
return weight * absorbed_dose
def effective_dose(self, *, organ_equivalent_doses):
diff --git a/library/radiography.py b/fpc/radiography.py
similarity index 97%
rename from library/radiography.py
rename to fpc/radiography.py
index bc28ff0..ad1a376 100644
--- a/library/radiography.py
+++ b/fpc/radiography.py
@@ -70,8 +70,8 @@ def fifteen_percent_rule(self, *, kvp, mas, steps=1):
:param steps: number of 15% adjustments (+ raises kVp, - lowers it)
:returns: tuple (new kVp, new mAs)
"""
- new_kvp = kvp * 1.15 ** steps
- new_mas = mas / 2 ** steps
+ new_kvp = kvp * 1.15**steps
+ new_mas = mas / 2**steps
return new_kvp, new_mas
def exposure_maintenance_mas(self, *, original_mas, original_sid, new_sid):
@@ -104,8 +104,9 @@ def grid_mas(self, *, original_mas, original_grid, new_grid):
raise ValueError(f"unknown grid {grid!r}; use one of: {', '.join(factors)}")
return original_mas * factors[new_grid] / factors[original_grid]
- def heat_units(self, *, kvp, tube_current_ma, exposure_time_s, generator="high_frequency",
- exposures=1):
+ def heat_units(
+ self, *, kvp, tube_current_ma, exposure_time_s, generator="high_frequency", exposures=1
+ ):
"""Anode heat load in heat units: HU = kVp * mA * s * C * exposures.
Used with the tube rating chart to check a series of exposures (e.g. a
diff --git a/library/statics.py b/fpc/statics.py
similarity index 97%
rename from library/statics.py
rename to fpc/statics.py
index 83c4f85..9933cfd 100644
--- a/library/statics.py
+++ b/fpc/statics.py
@@ -34,7 +34,7 @@ def resultant_2d(self, *, fx, fy):
:param fy: force component along y [N]
:returns: tuple (magnitude [N], angle [degrees] measured from +x axis)
"""
- magnitude = math.hypot(fx, fy) # sqrt(fx^2 + fy^2)
+ magnitude = math.hypot(fx, fy) # sqrt(fx^2 + fy^2)
angle_deg = math.degrees(math.atan2(fy, fx))
return magnitude, angle_deg
diff --git a/library/thermo.py b/fpc/thermo.py
similarity index 70%
rename from library/thermo.py
rename to fpc/thermo.py
index 125f133..0f2ea15 100644
--- a/library/thermo.py
+++ b/fpc/thermo.py
@@ -5,9 +5,12 @@
Thermodynamics tracks energy as it moves between heat and work. These helpers
cover the ideal gas law, heat transfer by conduction and convection, sensible
-heat, and the Carnot efficiency limit on heat engines.
+heat, the Carnot efficiency limit on heat engines, and the log-mean
+temperature difference used to size heat exchangers.
"""
+import math
+
class Thermo:
"""Thermodynamics helpers."""
@@ -81,3 +84,26 @@ def celsius_to_kelvin(self, *, celsius):
:returns: temperature [K]
"""
return celsius + 273.15
+
+ def log_mean_temperature_difference(self, *, delta_t_1, delta_t_2):
+ """Log-mean temperature difference (LMTD) for a heat exchanger.
+
+ LMTD = (dT1 - dT2) / ln(dT1 / dT2), where dT1 and dT2 are the hot-cold
+ temperature differences at the two ends of the exchanger. The duty is
+ then Q = U * A * LMTD. When dT1 == dT2 the formula is 0/0; its limit
+ is simply dT1, which we return directly.
+
+ For counterflow: dT1 = Th_in - Tc_out, dT2 = Th_out - Tc_in.
+ For parallel flow: dT1 = Th_in - Tc_in, dT2 = Th_out - Tc_out.
+
+ :param delta_t_1: temperature difference at one end [K]
+ :param delta_t_2: temperature difference at the other end [K]
+ :returns: log-mean temperature difference [K]
+ :raises ValueError: if either difference is not positive (that would
+ mean the streams cross, which a simple exchanger cannot do)
+ """
+ if delta_t_1 <= 0 or delta_t_2 <= 0:
+ raise ValueError("both end temperature differences must be positive")
+ if math.isclose(delta_t_1, delta_t_2):
+ return delta_t_1
+ return (delta_t_1 - delta_t_2) / math.log(delta_t_1 / delta_t_2)
diff --git a/library/ultrasound.py b/fpc/ultrasound.py
similarity index 96%
rename from library/ultrasound.py
rename to fpc/ultrasound.py
index 9e26eb1..c8b913f 100644
--- a/library/ultrasound.py
+++ b/fpc/ultrasound.py
@@ -126,8 +126,9 @@ def maximum_depth(self, *, pulse_repetition_frequency_hz, speed=SOFT_TISSUE_SPEE
"""
return speed / (2.0 * pulse_repetition_frequency_hz) * 100.0
- def doppler_shift(self, *, transmitted_frequency_mhz, velocity, angle_deg,
- speed=SOFT_TISSUE_SPEED):
+ def doppler_shift(
+ self, *, transmitted_frequency_mhz, velocity, angle_deg, speed=SOFT_TISSUE_SPEED
+ ):
"""Doppler frequency shift df = 2 * f0 * v * cos(theta) / c.
:param transmitted_frequency_mhz: transmitted frequency f0 [MHz]
@@ -139,8 +140,9 @@ def doppler_shift(self, *, transmitted_frequency_mhz, velocity, angle_deg,
f0_hz = transmitted_frequency_mhz * 1e6
return 2.0 * f0_hz * velocity * math.cos(math.radians(angle_deg)) / speed
- def doppler_velocity(self, *, transmitted_frequency_mhz, doppler_shift_hz, angle_deg,
- speed=SOFT_TISSUE_SPEED):
+ def doppler_velocity(
+ self, *, transmitted_frequency_mhz, doppler_shift_hz, angle_deg, speed=SOFT_TISSUE_SPEED
+ ):
"""Blood velocity from a measured Doppler shift (the inverse problem).
v = df * c / (2 * f0 * cos(theta)). Because cos(theta) sits in the
diff --git a/library/units.py b/fpc/units.py
similarity index 75%
rename from library/units.py
rename to fpc/units.py
index b94c380..ea8447c 100644
--- a/library/units.py
+++ b/fpc/units.py
@@ -10,7 +10,7 @@
IMPORTANT: Pint quantities can only be combined if they come from the SAME
registry. So the project defines exactly one here and everything imports it::
- from library.units import ureg, Q_
+ from fpc.units import ureg, Q_
length = 2 * ureg.meter # or: Q_(2, "meter")
force = Q_(1000, "newton")
@@ -19,8 +19,10 @@
from pint import UnitRegistry
-# The single registry shared across the project.
-ureg = UnitRegistry()
+# The single registry shared across the project. The annotation tells type
+# checkers (mypy, your editor) what this object is; UnitRegistry is generic,
+# so a bare ``UnitRegistry()`` leaves them guessing.
+ureg: UnitRegistry = UnitRegistry()
# ``Q_`` is the conventional short alias for building a quantity from a value
# and a unit string, e.g. ``Q_(200, "GPa")``.
diff --git a/library/utility.py b/fpc/utility.py
similarity index 63%
rename from library/utility.py
rename to fpc/utility.py
index 56cbc25..5232ef6 100644
--- a/library/utility.py
+++ b/fpc/utility.py
@@ -5,7 +5,8 @@
The Utility class collects the "plumbing" every engineering script needs but
that does not belong to any single physics domain: reading and writing JSON,
-exporting results to CSV (so you can open them in a spreadsheet), and popping up
+exporting results to CSV (so you can open them in a spreadsheet), reading and
+writing tables with pandas (CSV or Excel), and popping up
a native file-picker dialog with tkinter.
These methods demonstrate working with the file system, the ``json`` and
@@ -15,6 +16,7 @@
import csv
import json
+from pathlib import Path
class Utility:
@@ -27,7 +29,7 @@ def json_to_dict(self, *, file_path):
:returns: parsed data as a dict (or list, matching the JSON's top level)
"""
# ``with`` guarantees the file is closed even if parsing raises.
- with open(file_path, "r", encoding="utf-8") as handle:
+ with open(file_path, encoding="utf-8") as handle:
return json.load(handle)
def dict_to_json(self, *, data, file_path, indent=2):
@@ -58,6 +60,49 @@ def export_csv(self, *, rows, file_path, header=None):
writer.writerows(rows)
return file_path
+ def read_table(self, *, file_path, sheet_name=0):
+ """Read a CSV or Excel file into a pandas DataFrame.
+
+ pandas is the standard tool for tabular data (sensor logs, test
+ results, property tables). This picks the right reader from the file
+ extension so scripts can accept either format.
+
+ :param file_path: path to a ``.csv``, ``.xlsx``, or ``.xls`` file
+ :param sheet_name: Excel sheet name or 0-based index, defaults to the
+ first sheet (ignored for CSV)
+ :returns: a ``pandas.DataFrame``
+ :raises ValueError: for an unsupported file extension
+ """
+ # Import here so `from fpc import Utility` stays fast for scripts that
+ # never touch tables.
+ import pandas as pd
+
+ suffix = Path(file_path).suffix.lower()
+ if suffix == ".csv":
+ return pd.read_csv(file_path)
+ if suffix in (".xlsx", ".xls"):
+ # Excel support comes from the openpyxl package (a dependency).
+ return pd.read_excel(file_path, sheet_name=sheet_name)
+ raise ValueError(f"unsupported table format {suffix!r}; use .csv or .xlsx")
+
+ def write_table(self, *, frame, file_path):
+ """Write a pandas DataFrame to CSV or Excel, chosen by file extension.
+
+ :param frame: the ``pandas.DataFrame`` to save
+ :param file_path: destination ``.csv`` or ``.xlsx`` path
+ :returns: the file_path that was written
+ :raises ValueError: for an unsupported file extension
+ """
+ suffix = Path(file_path).suffix.lower()
+ # index=False: the row numbers are pandas bookkeeping, not data.
+ if suffix == ".csv":
+ frame.to_csv(file_path, index=False)
+ elif suffix == ".xlsx":
+ frame.to_excel(file_path, index=False)
+ else:
+ raise ValueError(f"unsupported table format {suffix!r}; use .csv or .xlsx")
+ return file_path
+
def pick_file(self, *, title="Select a file", file_types=(("All files", "*.*"),)):
"""Open a native "open file" dialog and return the chosen path.
diff --git a/gui.py b/gui.py
index a4a9b83..71066f4 100644
--- a/gui.py
+++ b/gui.py
@@ -14,10 +14,10 @@
* A "Workspace" table that records every expression result so you can see
your session at a glance - much like MATLAB's workspace pane.
* Buttons to open a .py file into the command box, run it, clear the log,
- and EXPORT the recorded results to CSV or JSON (via library.Utility).
+ and EXPORT the recorded results to CSV or JSON (via fpc.Utility).
The interpreter namespace comes preloaded with ``math``, ``numpy as np``, and an
-instance of every engineering class in ``library`` (lower-cased: ``dyn``,
+instance of every engineering class in ``fpc`` (lower-cased: ``dyn``,
``statics``, ``thermo``, ...), so you can start computing immediately, e.g.::
dyn.projectile_range(speed=20, angle_deg=45)
@@ -35,10 +35,26 @@
import numpy as np
# Pull in the whole engineering library plus the file/export helper.
-from library import (
- Dynamics, Statics, Modal, Thermo, Fluids, Material,
- Gear, Electrical, Mechatronics, Controls, Utility,
- Radiation, Radiography, CT, MRI, Ultrasound, Dicom,
+from fpc import (
+ CT,
+ MRI,
+ Controls,
+ Design,
+ Dicom,
+ Dynamics,
+ Electrical,
+ Fluids,
+ Gear,
+ Material,
+ Mechatronics,
+ Modal,
+ Properties,
+ Radiation,
+ Radiography,
+ Statics,
+ Thermo,
+ Ultrasound,
+ Utility,
)
@@ -67,6 +83,8 @@ def __init__(self):
"electrical": Electrical(),
"mechatronics": Mechatronics(),
"controls": Controls(),
+ "design": Design(),
+ "props": Properties(),
# Radiology / medical imaging.
"radiation": Radiation(),
"radiography": Radiography(),
@@ -92,8 +110,7 @@ def run(self, *, source):
"""
source = source.strip()
if not source:
- return {"label": None, "command": "", "value": None,
- "output": "", "error": None}
+ return {"label": None, "command": "", "value": None, "output": "", "error": None}
captured = io.StringIO()
value_text = None
@@ -136,7 +153,7 @@ def run(self, *, source):
return result
def export_csv(self, *, file_path):
- """Write the recorded results to a CSV file using library.Utility.
+ """Write the recorded results to a CSV file using fpc.Utility.
:param file_path: destination .csv path
:returns: the file path written
@@ -149,7 +166,7 @@ def export_csv(self, *, file_path):
)
def export_json(self, *, file_path):
- """Write the recorded results to a JSON file using library.Utility.
+ """Write the recorded results to a JSON file using fpc.Utility.
:param file_path: destination .json path
:returns: the file path written
@@ -168,6 +185,7 @@ def export_json(self, *, file_path):
# require a display.
# ---------------------------------------------------------------------------
+
def launch():
"""Build and run the GUI. Returns nothing; blocks until the window closes."""
import tkinter as tk
@@ -189,8 +207,7 @@ def __init__(self, root):
self._log(
"Ready. Preloaded: math, np, and dyn/statics/thermo/... "
"Type an expression and press Run (or Enter).\n"
- "Example: dyn.projectile_range(speed=20, angle_deg=45)\n"
- + "-" * 70 + "\n"
+ "Example: dyn.projectile_range(speed=20, angle_deg=45)\n" + "-" * 70 + "\n"
)
# --- UI construction ------------------------------------------------
@@ -213,8 +230,9 @@ def _build_log_and_workspace(self):
# Session log (read-only text with a scrollbar).
log_frame = ttk.LabelFrame(middle, text="Session log")
log_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
- self.log = tk.Text(log_frame, wrap=tk.WORD, height=20,
- font=("monospace", 10), state=tk.DISABLED)
+ self.log = tk.Text(
+ log_frame, wrap=tk.WORD, height=20, font=("monospace", 10), state=tk.DISABLED
+ )
log_scroll = ttk.Scrollbar(log_frame, command=self.log.yview)
self.log.configure(yscrollcommand=log_scroll.set)
log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
@@ -224,7 +242,10 @@ def _build_log_and_workspace(self):
ws_frame = ttk.LabelFrame(middle, text="Workspace")
ws_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(8, 0))
self.workspace = ttk.Treeview(
- ws_frame, columns=("label", "result"), show="headings", height=20,
+ ws_frame,
+ columns=("label", "result"),
+ show="headings",
+ height=20,
)
self.workspace.heading("label", text="Name")
self.workspace.heading("result", text="Result")
@@ -268,7 +289,7 @@ def open_script(self):
)
if not path:
return
- with open(path, "r", encoding="utf-8") as handle:
+ with open(path, encoding="utf-8") as handle:
text = handle.read()
self.command.delete(0, tk.END)
self.command.insert(0, text)
@@ -292,8 +313,9 @@ def clear_log(self):
def _export(self, *, kind):
"""Shared logic for the two export buttons."""
if not self.session.records:
- messagebox.showinfo("Nothing to export",
- "Run some expressions first - the workspace is empty.")
+ messagebox.showinfo(
+ "Nothing to export", "Run some expressions first - the workspace is empty."
+ )
return
extension = ".csv" if kind == "csv" else ".json"
path = filedialog.asksaveasfilename(
@@ -318,7 +340,9 @@ def _show_result(self, result):
elif result["value"] is not None:
self._log(f"{result['label']} = {result['value']}\n")
self.workspace.insert(
- "", tk.END, values=(result["label"], result["value"]),
+ "",
+ tk.END,
+ values=(result["label"], result["value"]),
)
def _log(self, text):
diff --git a/library/fluids.py b/library/fluids.py
deleted file mode 100644
index 0eebff4..0000000
--- a/library/fluids.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-# Copyright 2026 Evan Gress
-
-"""fluids.py - fluid dynamics calculations.
-
-Fluid mechanics governs anything that flows: pipes, pumps, wings, and weather.
-These helpers cover the core dimensionless groups and conservation laws -
-Reynolds number, the continuity equation, Bernoulli's equation, dynamic
-pressure, and pipe friction losses.
-"""
-
-import math
-
-
-class Fluids:
- """Fluid dynamics helpers."""
-
- # Acceleration due to gravity [m/s^2], used by head/pressure relations.
- GRAVITY = 9.80665
-
- def reynolds_number(self, *, density, velocity, length, viscosity):
- """Reynolds number: Re = rho*v*L / mu.
-
- Re is the ratio of inertial to viscous forces and tells you whether
- flow is laminar (Re < ~2300 in a pipe) or turbulent (Re > ~4000).
-
- :param density: fluid density rho [kg/m^3]
- :param velocity: flow velocity v [m/s]
- :param length: characteristic length L (e.g. pipe diameter) [m]
- :param viscosity: dynamic viscosity mu [Pa*s]
- :returns: Reynolds number [-]
- """
- return density * velocity * length / viscosity
-
- def continuity_velocity(self, *, area_in, velocity_in, area_out):
- """Outlet velocity from conservation of mass: A1*v1 = A2*v2.
-
- For an incompressible fluid, narrowing the pipe speeds the flow up.
-
- :param area_in: inlet cross-sectional area A1 [m^2]
- :param velocity_in: inlet velocity v1 [m/s]
- :param area_out: outlet cross-sectional area A2 [m^2]
- :returns: outlet velocity v2 [m/s]
- """
- return area_in * velocity_in / area_out
-
- def dynamic_pressure(self, *, density, velocity):
- """Dynamic pressure of a moving fluid: q = 1/2 * rho * v^2.
-
- :param density: fluid density rho [kg/m^3]
- :param velocity: flow velocity v [m/s]
- :returns: dynamic pressure [Pa]
- """
- return 0.5 * density * velocity ** 2
-
- def bernoulli_pressure(
- self, *, density, velocity_1, height_1, pressure_1, velocity_2, height_2,
- gravity=GRAVITY,
- ):
- """Downstream static pressure from Bernoulli's equation.
-
- Along a streamline (no friction, incompressible):
- P + 1/2*rho*v^2 + rho*g*h = constant.
- Given state 1 fully and the velocity/height at state 2, solve for P2.
-
- :param density: fluid density rho [kg/m^3]
- :param velocity_1: velocity at point 1 [m/s]
- :param height_1: elevation at point 1 [m]
- :param pressure_1: static pressure at point 1 [Pa]
- :param velocity_2: velocity at point 2 [m/s]
- :param height_2: elevation at point 2 [m]
- :param gravity: gravitational acceleration [m/s^2]
- :returns: static pressure at point 2 [Pa]
- """
- total_head = (
- pressure_1
- + 0.5 * density * velocity_1 ** 2
- + density * gravity * height_1
- )
- return total_head - 0.5 * density * velocity_2 ** 2 - density * gravity * height_2
-
- def pipe_head_loss(self, *, friction_factor, length, diameter, velocity, gravity=GRAVITY):
- """Major head loss in a pipe (Darcy-Weisbach equation).
-
- h_f = f * (L/D) * v^2 / (2*g).
-
- :param friction_factor: Darcy friction factor f [-]
- :param length: pipe length L [m]
- :param diameter: pipe inner diameter D [m]
- :param velocity: average flow velocity v [m/s]
- :param gravity: gravitational acceleration [m/s^2]
- :returns: head loss [m of fluid]
- """
- return friction_factor * (length / diameter) * velocity ** 2 / (2 * gravity)
diff --git a/main.py b/main.py
index 2767b2b..b6925e6 100644
--- a/main.py
+++ b/main.py
@@ -22,24 +22,22 @@
# --- Standard library -------------------------------------------------------
# ``math`` ships with Python and covers scalar math (sqrt, sin, pi, ...).
-import math
# --- Third-party scientific stack ------------------------------------------
# ``numpy`` gives us fast array math; ``scipy`` adds the heavy machinery -
# here we use its numerical integration routines. Both are installed via
# ``uv add numpy scipy`` and pinned in pyproject.toml.
-import numpy as np
-from scipy import integrate
-
# ``matplotlib`` turns result arrays into figures - the engineering deliverable.
# pyplot is its MATLAB-style interface; if no screen is available it quietly
# falls back to a file-only backend, so saving a PNG always works.
import matplotlib.pyplot as plt
+import numpy as np
+from scipy import integrate
# --- Project library --------------------------------------------------------
-# Our own dynamics helpers live in ./library. This is how you import a class
+# Our own dynamics helpers live in ./fpc. This is how you import a class
# you wrote yourself and reuse it across scripts.
-from library.dynamics import Dynamics
+from fpc.dynamics import Dynamics
def analytical_displacement(*, initial_position, initial_velocity, acceleration, time):
@@ -58,7 +56,7 @@ def analytical_displacement(*, initial_position, initial_velocity, acceleration,
:param time: elapsed time t [s]
:returns: position x at time t [m]
"""
- return initial_position + initial_velocity * time + 0.5 * acceleration * time ** 2
+ return initial_position + initial_velocity * time + 0.5 * acceleration * time**2
def plot_motion(*, time, acceleration, velocity, position, save_path="displacement.png"):
@@ -106,10 +104,10 @@ def main():
# Scenario: an object launched upward at 5 m/s, then pulled by gravity.
# We use the sign convention "down is positive" so gravity is +9.81.
# ----------------------------------------------------------------------
- x0 = 0.0 # initial position [m]
- v0 = 5.0 # initial velocity [m/s]
- g = 9.81 # gravitational acceleration [m/s^2]
- t_final = 3.0 # how long we observe [s]
+ x0 = 0.0 # initial position [m]
+ v0 = 5.0 # initial velocity [m/s]
+ g = 9.81 # gravitational acceleration [m/s^2]
+ t_final = 3.0 # how long we observe [s]
# === 1. Closed-form answer ===========================================
# When acceleration is constant, a formula exists. This is our "truth"
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..4fde3f9
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,80 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+#
+# Documentation site built from docs/ and the library's docstrings.
+# uv sync --group docs
+# uv run mkdocs serve # live preview at http://127.0.0.1:8000
+# uv run mkdocs build --strict
+# Published to GitHub Pages by .github/workflows/docs.yml.
+
+site_name: First Principles Code
+site_description: Engineering calculations in Python, written to be read and learned from.
+repo_url: https://github.com/kovirlabs/first-principles-code
+edit_uri: edit/main/docs/
+
+theme:
+ name: material
+ features:
+ - navigation.sections
+ - content.code.copy
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ toggle: {icon: material/brightness-7, name: Dark mode}
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ toggle: {icon: material/brightness-4, name: Light mode}
+
+plugins:
+ - search
+ - mkdocstrings:
+ handlers:
+ python:
+ options:
+ # The library's docstrings use Sphinx-style ":param x:" fields.
+ docstring_style: sphinx
+ show_source: true
+ show_root_heading: true
+ members_order: source
+ show_signature_annotations: true
+
+markdown_extensions:
+ - admonition
+ - tables
+ - pymdownx.highlight
+ - pymdownx.superfences
+ - pymdownx.arithmatex: {generic: true}
+
+nav:
+ - Home: index.md
+ - Guides:
+ - Units - floats or Pint?: units.md
+ - From calculation to tool: building-tools.md
+ - API reference:
+ - Mechanics:
+ - Dynamics: api/dynamics.md
+ - Statics: api/statics.md
+ - Modal (vibration): api/modal.md
+ - Material: api/material.md
+ - Beam (Pint): api/beam.md
+ - Gear: api/gear.md
+ - Thermal and fluids:
+ - Thermo: api/thermo.md
+ - Fluids: api/fluids.md
+ - Electrical and controls:
+ - Electrical: api/electrical.md
+ - Mechatronics: api/mechatronics.md
+ - Controls: api/controls.md
+ - Design and data:
+ - Design (root finding, optimization): api/design.md
+ - Properties (reference tables): api/properties.md
+ - Utility (files, tables, dialogs): api/utility.md
+ - Units registry: api/units.md
+ - fpc command line: api/cli.md
+ - Radiology:
+ - Radiation: api/radiation.md
+ - Radiography: api/radiography.md
+ - CT: api/ct.md
+ - MRI: api/mri.md
+ - Ultrasound: api/ultrasound.md
+ - Dicom: api/dicom.md
diff --git a/notebooks/01_getting_started.ipynb b/notebooks/01_getting_started.ipynb
new file mode 100644
index 0000000..46c7a32
--- /dev/null
+++ b/notebooks/01_getting_started.ipynb
@@ -0,0 +1,176 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "bad23c80",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "\n",
+ "# 01 · Getting started with `fpc`\n",
+ "\n",
+ "A notebook mixes code, results, plots, and explanation in one document — ideal for exploring a problem and showing your working.\n",
+ "\n",
+ "**Run a cell:** Shift+Enter. Cells share variables, so run them top to bottom.\n",
+ "\n",
+ "Start Jupyter from the repo root:\n",
+ "```bash\n",
+ "uv sync --group notebooks\n",
+ "uv run jupyter lab\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6ae1a997",
+ "metadata": {},
+ "source": [
+ "## 1. Import a class and call a method\n",
+ "Every method takes **keyword arguments**, so the call says what each number means."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e83e754d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from fpc import Dynamics, Thermo\n",
+ "\n",
+ "dyn = Dynamics()\n",
+ "dyn.displacement_constant_acceleration(\n",
+ " initial_position=0.0, initial_velocity=5.0, acceleration=9.81, time=3.0,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6eb9845a",
+ "metadata": {},
+ "source": [
+ "The last expression in a cell is displayed automatically. `help()` (or Shift+Tab in Jupyter) shows the docstring, including the units:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5eaac978",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "help(Thermo.carnot_efficiency)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f24408e8",
+ "metadata": {},
+ "source": [
+ "## 2. Many cases: lists of dicts and `**` unpacking"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "723449d2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "launches = [{\"speed\": 20, \"angle_deg\": a} for a in (15, 30, 45, 60, 75)]\n",
+ "for case in launches:\n",
+ " print(f\"{case['angle_deg']:>3} deg -> {dyn.projectile_range(**case):6.2f} m\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "944a1bd5",
+ "metadata": {},
+ "source": [
+ "## 3. Plot a sweep\n",
+ "A plot shows the whole trend — here, why 45° is the best launch angle without drag."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cc20a850",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "angles = np.linspace(1, 89, 89)\n",
+ "ranges = [dyn.projectile_range(speed=20, angle_deg=a) for a in angles]\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(6, 3.5))\n",
+ "ax.plot(angles, ranges)\n",
+ "ax.set_xlabel(\"Launch angle [deg]\")\n",
+ "ax.set_ylabel(\"Range [m]\")\n",
+ "ax.grid(alpha=0.3)\n",
+ "angles[int(np.argmax(ranges))]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4b3420fa",
+ "metadata": {},
+ "source": [
+ "## 4. Drag changes the answer\n",
+ "`simulate_projectile_drag` integrates the equations of motion with SciPy. Compare its range with the no-drag formula."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7bb5993a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# A tennis ball: 57 g, 6.7 cm diameter, Cd ~ 0.5\n",
+ "import math\n",
+ "\n",
+ "t, x, y = dyn.simulate_projectile_drag(\n",
+ " speed=20, angle_deg=45, mass=0.057, drag_coefficient=0.5,\n",
+ " frontal_area=math.pi * 0.067**2 / 4,\n",
+ ")\n",
+ "no_drag = dyn.projectile_range(speed=20, angle_deg=45)\n",
+ "print(f\"without drag: {no_drag:.1f} m with drag: {x[-1]:.1f} m\")\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(6, 3))\n",
+ "ax.plot(x, y, label=\"with drag\")\n",
+ "xs = np.linspace(0, no_drag, 100)\n",
+ "ax.plot(xs, xs - 9.81 * xs**2 / 20**2, \"--\", label=\"no drag\") # y = x*tan(45) - g*x^2/(2 v^2 cos^2 45)\n",
+ "ax.set_xlabel(\"x [m]\")\n",
+ "ax.set_ylabel(\"y [m]\")\n",
+ "ax.legend()\n",
+ "ax.grid(alpha=0.3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "af77ce50",
+ "metadata": {},
+ "source": [
+ "## Next\n",
+ "- `02_pipe_flow_and_data.ipynb` — pandas tables, interpolation, root finding\n",
+ "- `03_beam_design.ipynb` — units with Pint, sizing and optimization\n",
+ "- `examples/` — complete worked problems as scripts"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/02_pipe_flow_and_data.ipynb b/notebooks/02_pipe_flow_and_data.ipynb
new file mode 100644
index 0000000..37ef51d
--- /dev/null
+++ b/notebooks/02_pipe_flow_and_data.ipynb
@@ -0,0 +1,191 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d05ced16",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "\n",
+ "# 02 · Pipe flow, property tables, and root finding\n",
+ "\n",
+ "Three tools you will use constantly:\n",
+ "1. **pandas** to load and inspect reference tables\n",
+ "2. **interpolation** to read between table rows\n",
+ "3. **root finding** to solve equations that have no algebraic solution"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "aa6066aa",
+ "metadata": {},
+ "source": [
+ "## 1. Load a table with pandas"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0883be1e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from fpc import Fluids, Properties\n",
+ "\n",
+ "props = Properties()\n",
+ "props.list_tables()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "737934f4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "water = props.load_table(name=\"water_liquid_1atm\")\n",
+ "water.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b1600506",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "water.describe().T[[\"min\", \"max\"]]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b515f7ae",
+ "metadata": {},
+ "source": [
+ "Boolean masks select rows — *which temperatures have viscosity below 0.5 mPa·s?*"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "69d42448",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "water[water[\"dynamic_viscosity_pa_s\"] < 0.5e-3][[\"temperature_c\", \"dynamic_viscosity_pa_s\"]]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ca0d30a4",
+ "metadata": {},
+ "source": [
+ "## 2. Interpolate between rows\n",
+ "The table jumps in 5–10 °C steps. `water_liquid` interpolates; `method=\"cubic\"` uses a smooth spline instead of straight lines. Outside the table it raises an error rather than guessing."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "51905ff5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "props.water_liquid(temperature_c=37, property=\"dynamic_viscosity_pa_s\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3055a749",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "sat = props.load_table(name=\"water_saturation\")\n",
+ "t = np.linspace(0.01, 200, 400)\n",
+ "fig, ax = plt.subplots(figsize=(6, 3.5))\n",
+ "ax.plot(sat[\"temperature_c\"], sat[\"pressure_kpa\"], \"o\", label=\"table\")\n",
+ "ax.plot(t, props.water_saturation(temperature_c=t, property=\"pressure_kpa\"), label=\"linear\")\n",
+ "ax.plot(t, props.water_saturation(temperature_c=t, property=\"pressure_kpa\", method=\"cubic\"), \"--\", label=\"cubic\")\n",
+ "ax.set_xlabel(\"Temperature [degC]\")\n",
+ "ax.set_ylabel(\"Saturation pressure [kPa]\")\n",
+ "ax.legend()\n",
+ "ax.grid(alpha=0.3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "579e1ba4",
+ "metadata": {},
+ "source": [
+ "## 3. Root finding: the Colebrook equation\n",
+ "$$\\frac{1}{\\sqrt f} = -2\\log_{10}\\left(\\frac{\\varepsilon/D}{3.7} + \\frac{2.51}{Re\\sqrt f}\\right)$$\n",
+ "`f` is on both sides, so we can't rearrange it. `Fluids.colebrook_friction_factor` hands the equation to `scipy.optimize.brentq`. Repeating that solve over a grid draws a Moody chart:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "119b6b03",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fluids = Fluids()\n",
+ "reynolds = np.logspace(3, 8, 200)\n",
+ "fig, ax = plt.subplots(figsize=(6, 4))\n",
+ "for roughness in (0, 1e-5, 1e-4, 1e-3, 1e-2):\n",
+ " f = [fluids.colebrook_friction_factor(reynolds=re, relative_roughness=roughness) for re in reynolds]\n",
+ " ax.loglog(reynolds, f, label=f\"eps/D = {roughness:g}\")\n",
+ "ax.set_xlabel(\"Reynolds number\")\n",
+ "ax.set_ylabel(\"Darcy friction factor\")\n",
+ "ax.legend(fontsize=8)\n",
+ "ax.grid(which=\"both\", alpha=0.3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b541e6be",
+ "metadata": {},
+ "source": [
+ "## 4. Put it together\n",
+ "`Fluids.pipe_flow` chains all of this for one pipe:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "81a1a752",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "rho = props.water_liquid(temperature_c=20, property=\"density_kg_m3\")\n",
+ "mu = props.water_liquid(temperature_c=20, property=\"dynamic_viscosity_pa_s\")\n",
+ "fluids.pipe_flow(flow=0.012, diameter=0.1023, length=120, roughness=0.045e-3, density=rho, viscosity=mu)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0a9a56d2",
+ "metadata": {},
+ "source": [
+ "**Try it:** how much does the pressure drop fall if the water is at 80 °C instead of 20 °C? Why?"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/03_beam_design.ipynb b/notebooks/03_beam_design.ipynb
new file mode 100644
index 0000000..428a20e
--- /dev/null
+++ b/notebooks/03_beam_design.ipynb
@@ -0,0 +1,177 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "32dba79d",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "\n",
+ "# 03 · Beam design: units, sizing, and optimization\n",
+ "\n",
+ "Analysis asks *how much does this beam deflect?* Design asks *what beam deflects no more than 5 mm?* This notebook does both."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3cd7f6b7",
+ "metadata": {},
+ "source": [
+ "## 1. Units with Pint\n",
+ "`Q_(value, unit)` makes a quantity. Arithmetic carries the units and `.to()` converts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a744ea59",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from fpc import Q_, Beam\n",
+ "\n",
+ "beam = Beam(length=Q_(2.0, \"m\"), width=Q_(50, \"mm\"), height=Q_(100, \"mm\"))\n",
+ "beam.max_deflection_point_load(load=Q_(5, \"kN\")).to(\"mm\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3ce932ab",
+ "metadata": {},
+ "source": [
+ "Mixing up units fails loudly instead of giving a wrong number:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "46702338",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " Beam(length=Q_(2.0, \"s\"), width=Q_(50, \"mm\"), height=Q_(100, \"mm\"))\n",
+ "except ValueError as error:\n",
+ " print(\"Caught:\", error)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ba7251ed",
+ "metadata": {},
+ "source": [
+ "## 2. Sizing by root finding\n",
+ "`Design.rectangular_beam_height_for_deflection` solves *deflection(h) − limit = 0* with `brentq`. Plain SI floats here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2480b170",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from fpc import Design\n",
+ "\n",
+ "design = Design()\n",
+ "h = design.rectangular_beam_height_for_deflection(\n",
+ " load=5000, length=2.0, modulus=200e9, width=0.05, max_deflection=0.001,\n",
+ ")\n",
+ "f\"required height = {h * 1000:.1f} mm\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c390c306",
+ "metadata": {},
+ "source": [
+ "## 3. Optimization with constraints\n",
+ "Minimize mass subject to stress, deflection, and a height ≤ 4 × width rule. The constraints that end up at their limits are the ones that *drive* the design."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4b1ab986",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "design.lightest_rectangular_beam(\n",
+ " load=5000, length=2.0, modulus=200e9, density=7850,\n",
+ " allowable_stress=150e6, max_deflection=0.005,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e56db7b1",
+ "metadata": {},
+ "source": [
+ "Sweep the load to see when the governing requirement switches from deflection to stress:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21317801",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "rows = []\n",
+ "for load in (2e3, 5e3, 10e3, 20e3, 40e3):\n",
+ " r = design.lightest_rectangular_beam(load=load, length=2.0, modulus=200e9, density=7850,\n",
+ " allowable_stress=150e6, max_deflection=0.01)\n",
+ " rows.append({\"load_kN\": load / 1e3, \"mass_kg\": round(r[\"mass\"], 1),\n",
+ " \"governs\": \", \".join(r[\"active\"])})\n",
+ "pd.DataFrame(rows)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b775f03",
+ "metadata": {},
+ "source": [
+ "## 4. Catalogue selection\n",
+ "Real beams come from a catalogue. `Properties.lightest_w_shape` filters and sorts the AISC table with pandas; Pint converts the required section modulus to the catalogue's units."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d969afda",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from fpc import Properties\n",
+ "\n",
+ "moment = Q_(40, \"kN\") * Q_(6, \"m\") / 4\n",
+ "required_sx = (moment / (0.66 * Q_(345, \"MPa\"))).to(\"in**3\")\n",
+ "print(f\"required Sx = {required_sx:.1f~P}\")\n",
+ "Properties().lightest_w_shape(required_sx_in3=required_sx.magnitude)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0397e6e4",
+ "metadata": {},
+ "source": [
+ "See `examples/beam_selection.py` for the full version with a deflection check."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/README.md b/notebooks/README.md
new file mode 100644
index 0000000..3dd3f1f
--- /dev/null
+++ b/notebooks/README.md
@@ -0,0 +1,26 @@
+
+
+
+# Notebooks
+
+Interactive companions to the library. Each notebook mixes explanation, code,
+and plots, so it doubles as a lab handout.
+
+```bash
+uv sync --group notebooks # one-time: installs Jupyter
+uv run jupyter lab # opens in your browser
+```
+
+| Notebook | Covers |
+| --- | --- |
+| `01_getting_started.ipynb` | Calling library methods, `help()`, lists of dicts, plotting a sweep, a SciPy drag simulation |
+| `02_pipe_flow_and_data.ipynb` | pandas tables, interpolation (linear vs cubic), root finding, a Moody chart |
+| `03_beam_design.ipynb` | Pint units, sizing by root finding, constrained optimization, catalogue selection |
+
+The notebooks are stored **without outputs** so diffs stay readable; run them
+to see results. They are generated from `make_notebooks.py`, which is easier
+to review than notebook JSON. `tests/test_notebooks.py` executes every
+notebook in CI.
+
+Prefer plain-Python notebooks? [marimo](https://marimo.io/) stores notebooks
+as `.py` files and reruns dependent cells automatically: `uvx marimo edit`.
diff --git a/notebooks/make_notebooks.py b/notebooks/make_notebooks.py
new file mode 100644
index 0000000..b35a76f
--- /dev/null
+++ b/notebooks/make_notebooks.py
@@ -0,0 +1,151 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Regenerate the notebooks in this folder from code (run from the repo root).
+
+Notebook JSON is awkward to review in a pull request, so the cells live here
+as plain Python strings. Edit this file, then run:
+
+ uv run --group notebooks python notebooks/make_notebooks.py
+"""
+
+import nbformat as nbf
+from nbformat.v4 import new_code_cell as code
+from nbformat.v4 import new_markdown_cell as md
+from nbformat.v4 import new_notebook
+
+HDR = "\n\n\n"
+
+
+def save(name, cells):
+ nb = new_notebook(
+ cells=cells,
+ metadata={
+ "kernelspec": {"name": "python3", "display_name": "Python 3", "language": "python"},
+ "language_info": {"name": "python"},
+ },
+ )
+ nbf.write(nb, f"notebooks/{name}")
+
+
+save(
+ "01_getting_started.ipynb",
+ [
+ md(
+ HDR
+ + "# 01 · Getting started with `fpc`\n\nA notebook mixes code, results, plots, and explanation in one document — ideal for exploring a problem and showing your working.\n\n**Run a cell:** Shift+Enter. Cells share variables, so run them top to bottom.\n\nStart Jupyter from the repo root:\n```bash\nuv sync --group notebooks\nuv run jupyter lab\n```"
+ ),
+ md(
+ "## 1. Import a class and call a method\nEvery method takes **keyword arguments**, so the call says what each number means."
+ ),
+ code(
+ "from fpc import Dynamics, Thermo\n\ndyn = Dynamics()\ndyn.displacement_constant_acceleration(\n initial_position=0.0, initial_velocity=5.0, acceleration=9.81, time=3.0,\n)"
+ ),
+ md(
+ "The last expression in a cell is displayed automatically. `help()` (or Shift+Tab in Jupyter) shows the docstring, including the units:"
+ ),
+ code("help(Thermo.carnot_efficiency)"),
+ md("## 2. Many cases: lists of dicts and `**` unpacking"),
+ code(
+ 'launches = [{"speed": 20, "angle_deg": a} for a in (15, 30, 45, 60, 75)]\nfor case in launches:\n print(f"{case[\'angle_deg\']:>3} deg -> {dyn.projectile_range(**case):6.2f} m")'
+ ),
+ md(
+ "## 3. Plot a sweep\nA plot shows the whole trend — here, why 45° is the best launch angle without drag."
+ ),
+ code(
+ 'import numpy as np\nimport matplotlib.pyplot as plt\n\nangles = np.linspace(1, 89, 89)\nranges = [dyn.projectile_range(speed=20, angle_deg=a) for a in angles]\n\nfig, ax = plt.subplots(figsize=(6, 3.5))\nax.plot(angles, ranges)\nax.set_xlabel("Launch angle [deg]")\nax.set_ylabel("Range [m]")\nax.grid(alpha=0.3)\nangles[int(np.argmax(ranges))]'
+ ),
+ md(
+ "## 4. Drag changes the answer\n`simulate_projectile_drag` integrates the equations of motion with SciPy. Compare its range with the no-drag formula."
+ ),
+ code(
+ '# A tennis ball: 57 g, 6.7 cm diameter, Cd ~ 0.5\nimport math\nt, x, y = dyn.simulate_projectile_drag(\n speed=20, angle_deg=45, mass=0.057, drag_coefficient=0.5,\n frontal_area=math.pi * 0.067**2 / 4,\n)\nno_drag = dyn.projectile_range(speed=20, angle_deg=45)\nprint(f"without drag: {no_drag:.1f} m with drag: {x[-1]:.1f} m")\n\nfig, ax = plt.subplots(figsize=(6, 3))\nax.plot(x, y, label="with drag")\nxs = np.linspace(0, no_drag, 100)\nax.plot(xs, xs - 9.81 * xs**2 / 20**2, "--", label="no drag") # y = x*tan(45) - g*x^2/(2 v^2 cos^2 45)\nax.set_xlabel("x [m]")\nax.set_ylabel("y [m]")\nax.legend()\nax.grid(alpha=0.3)'
+ ),
+ md(
+ "## Next\n- `02_pipe_flow_and_data.ipynb` — pandas tables, interpolation, root finding\n- `03_beam_design.ipynb` — units with Pint, sizing and optimization\n- `examples/` — complete worked problems as scripts"
+ ),
+ ],
+)
+
+save(
+ "02_pipe_flow_and_data.ipynb",
+ [
+ md(
+ HDR
+ + "# 02 · Pipe flow, property tables, and root finding\n\nThree tools you will use constantly:\n1. **pandas** to load and inspect reference tables\n2. **interpolation** to read between table rows\n3. **root finding** to solve equations that have no algebraic solution"
+ ),
+ md("## 1. Load a table with pandas"),
+ code("from fpc import Properties, Fluids\n\nprops = Properties()\nprops.list_tables()"),
+ code('water = props.load_table(name="water_liquid_1atm")\nwater.head()'),
+ code('water.describe().T[["min", "max"]]'),
+ md("Boolean masks select rows — *which temperatures have viscosity below 0.5 mPa·s?*"),
+ code(
+ 'water[water["dynamic_viscosity_pa_s"] < 0.5e-3][["temperature_c", "dynamic_viscosity_pa_s"]]'
+ ),
+ md(
+ '## 2. Interpolate between rows\nThe table jumps in 5–10 °C steps. `water_liquid` interpolates; `method="cubic"` uses a smooth spline instead of straight lines. Outside the table it raises an error rather than guessing.'
+ ),
+ code('props.water_liquid(temperature_c=37, property="dynamic_viscosity_pa_s")'),
+ code(
+ 'import numpy as np\nimport matplotlib.pyplot as plt\n\nsat = props.load_table(name="water_saturation")\nt = np.linspace(0.01, 200, 400)\nfig, ax = plt.subplots(figsize=(6, 3.5))\nax.plot(sat["temperature_c"], sat["pressure_kpa"], "o", label="table")\nax.plot(t, props.water_saturation(temperature_c=t, property="pressure_kpa"), label="linear")\nax.plot(t, props.water_saturation(temperature_c=t, property="pressure_kpa", method="cubic"), "--", label="cubic")\nax.set_xlabel("Temperature [degC]")\nax.set_ylabel("Saturation pressure [kPa]")\nax.legend()\nax.grid(alpha=0.3)'
+ ),
+ md(
+ "## 3. Root finding: the Colebrook equation\n$$\\frac{1}{\\sqrt f} = -2\\log_{10}\\left(\\frac{\\varepsilon/D}{3.7} + \\frac{2.51}{Re\\sqrt f}\\right)$$\n`f` is on both sides, so we can't rearrange it. `Fluids.colebrook_friction_factor` hands the equation to `scipy.optimize.brentq`. Repeating that solve over a grid draws a Moody chart:"
+ ),
+ code(
+ 'fluids = Fluids()\nreynolds = np.logspace(3, 8, 200)\nfig, ax = plt.subplots(figsize=(6, 4))\nfor roughness in (0, 1e-5, 1e-4, 1e-3, 1e-2):\n f = [fluids.colebrook_friction_factor(reynolds=re, relative_roughness=roughness) for re in reynolds]\n ax.loglog(reynolds, f, label=f"eps/D = {roughness:g}")\nax.set_xlabel("Reynolds number")\nax.set_ylabel("Darcy friction factor")\nax.legend(fontsize=8)\nax.grid(which="both", alpha=0.3)'
+ ),
+ md("## 4. Put it together\n`Fluids.pipe_flow` chains all of this for one pipe:"),
+ code(
+ 'rho = props.water_liquid(temperature_c=20, property="density_kg_m3")\nmu = props.water_liquid(temperature_c=20, property="dynamic_viscosity_pa_s")\nfluids.pipe_flow(flow=0.012, diameter=0.1023, length=120, roughness=0.045e-3, density=rho, viscosity=mu)'
+ ),
+ md(
+ "**Try it:** how much does the pressure drop fall if the water is at 80 °C instead of 20 °C? Why?"
+ ),
+ ],
+)
+
+save(
+ "03_beam_design.ipynb",
+ [
+ md(
+ HDR
+ + "# 03 · Beam design: units, sizing, and optimization\n\nAnalysis asks *how much does this beam deflect?* Design asks *what beam deflects no more than 5 mm?* This notebook does both."
+ ),
+ md(
+ "## 1. Units with Pint\n`Q_(value, unit)` makes a quantity. Arithmetic carries the units and `.to()` converts."
+ ),
+ code(
+ 'from fpc import Beam, Q_\n\nbeam = Beam(length=Q_(2.0, "m"), width=Q_(50, "mm"), height=Q_(100, "mm"))\nbeam.max_deflection_point_load(load=Q_(5, "kN")).to("mm")'
+ ),
+ md("Mixing up units fails loudly instead of giving a wrong number:"),
+ code(
+ 'try:\n Beam(length=Q_(2.0, "s"), width=Q_(50, "mm"), height=Q_(100, "mm"))\nexcept ValueError as error:\n print("Caught:", error)'
+ ),
+ md(
+ "## 2. Sizing by root finding\n`Design.rectangular_beam_height_for_deflection` solves *deflection(h) − limit = 0* with `brentq`. Plain SI floats here."
+ ),
+ code(
+ 'from fpc import Design\n\ndesign = Design()\nh = design.rectangular_beam_height_for_deflection(\n load=5000, length=2.0, modulus=200e9, width=0.05, max_deflection=0.001,\n)\nf"required height = {h * 1000:.1f} mm"'
+ ),
+ md(
+ "## 3. Optimization with constraints\nMinimize mass subject to stress, deflection, and a height ≤ 4 × width rule. The constraints that end up at their limits are the ones that *drive* the design."
+ ),
+ code(
+ "design.lightest_rectangular_beam(\n load=5000, length=2.0, modulus=200e9, density=7850,\n allowable_stress=150e6, max_deflection=0.005,\n)"
+ ),
+ md(
+ "Sweep the load to see when the governing requirement switches from deflection to stress:"
+ ),
+ code(
+ 'import pandas as pd\n\nrows = []\nfor load in (2e3, 5e3, 10e3, 20e3, 40e3):\n r = design.lightest_rectangular_beam(load=load, length=2.0, modulus=200e9, density=7850,\n allowable_stress=150e6, max_deflection=0.01)\n rows.append({"load_kN": load / 1e3, "mass_kg": round(r["mass"], 1),\n "governs": ", ".join(r["active"])})\npd.DataFrame(rows)'
+ ),
+ md(
+ "## 4. Catalogue selection\nReal beams come from a catalogue. `Properties.lightest_w_shape` filters and sorts the AISC table with pandas; Pint converts the required section modulus to the catalogue's units."
+ ),
+ code(
+ 'from fpc import Properties\n\nmoment = Q_(40, "kN") * Q_(6, "m") / 4\nrequired_sx = (moment / (0.66 * Q_(345, "MPa"))).to("in**3")\nprint(f"required Sx = {required_sx:.1f~P}")\nProperties().lightest_w_shape(required_sx_in3=required_sx.magnitude)'
+ ),
+ md("See `examples/beam_selection.py` for the full version with a deflection check."),
+ ],
+)
diff --git a/pyproject.toml b/pyproject.toml
index d4a2789..150570c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,27 +4,104 @@
[project]
name = "first-principles-code"
version = "0.1.0"
+description = "A teaching library of first-principles engineering calculations in Python."
+readme = "README.md"
requires-python = ">=3.14"
license = "Apache-2.0"
license-files = ["LICENSE"]
dependencies = [
"matplotlib>=3.10.9",
"numpy>=2.4.6",
+ "openpyxl>=3.1.5",
+ "pandas>=3.1.1",
"pint>=0.25.3",
"pydicom>=3.0.2",
"scipy>=1.17.1",
"scipy-stubs~=1.17.1",
]
+# Installing the package also installs an `fpc` terminal command that runs
+# fpc/cli.py:main (see docs/building-tools.md, rung 5).
+[project.scripts]
+fpc = "fpc.cli:main"
+
+# The build system turns this repo into an installable package, so other
+# projects can `uv add git+https://github.com/kovirlabs/first-principles-code`
+# (or `pip install -e .`) and then `import fpc`. Hatchling is a small,
+# standards-based build backend; the distribution name (first-principles-code)
+# differs from the import name (fpc), so we point it at the package folder.
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["fpc"]
+
[dependency-groups]
+apps = [
+ "streamlit>=1.67.0",
+]
dev = [
+ "mypy>=2.4.1",
+ "pandas-stubs>=3.1.0.261029",
+ "pre-commit>=4.7.0",
"pytest>=9.0.3",
+ "ruff>=0.17.4",
+]
+docs = [
+ "mkdocs-material>=9.8.0",
+ "mkdocstrings[python]>=1.0.8",
+]
+notebooks = [
+ "ipykernel>=7.4.0",
+ "jupyter>=1.1.1",
+ "nbclient>=0.11.0",
]
# pytest configuration: where to find tests and how to report.
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v"
-# Put the project root on sys.path so tests can `import library` / `import gui`
+# Put the project root on sys.path so tests can `import fpc` / `import gui`
# without installing the project as a package.
pythonpath = ["."]
+
+# --- Ruff: linter + formatter (uv run ruff check . / uv run ruff format .) ---
+# An explicit rule list so results are the same on every machine, whatever
+# global ruff config a contributor has.
+[tool.ruff]
+line-length = 100
+target-version = "py314"
+
+[tool.ruff.format]
+# Notebooks are generated by notebooks/make_notebooks.py; format that file
+# instead, so the generator and its output never disagree.
+exclude = ["*.ipynb"]
+
+[tool.ruff.lint]
+select = [
+ "E", "W", # pycodestyle: basic style errors and warnings
+ "F", # pyflakes: unused imports/variables, undefined names
+ "I", # isort: consistent import ordering
+ "B", # flake8-bugbear: likely bugs (mutable defaults, etc.)
+ "UP", # pyupgrade: modern Python syntax
+ "SIM", # flake8-simplify: simpler equivalent code
+ "NPY", # NumPy-specific checks (deprecated APIs, legacy random)
+]
+
+[tool.ruff.lint.per-file-ignores]
+# Notebook prose and cell sources are long strings; wrapping them hurts more
+# than it helps.
+"notebooks/*" = ["E501"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["fpc", "gui"]
+
+# --- mypy: type checking of the package (uv run mypy fpc) ---
+# Starts non-strict: most of the library has no annotations yet, and mypy
+# skips unannotated function bodies. Tighten gradually as hints are added.
+[tool.mypy]
+python_version = "3.14"
+packages = ["fpc"]
+ignore_missing_imports = true
+warn_unused_ignores = true
diff --git a/templates/engineering_script.py b/templates/engineering_script.py
new file mode 100644
index 0000000..a4f7a38
--- /dev/null
+++ b/templates/engineering_script.py
@@ -0,0 +1,155 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""engineering_script.py - a copy-and-adapt starting point for your own calculation.
+
+HOW TO USE THIS TEMPLATE
+ 1. Copy this file somewhere and rename it after your problem
+ (e.g. ``conveyor_motor_check.py``).
+ 2. Edit the INPUTS dictionary - every number your calculation depends on,
+ with its units in the key name.
+ 3. Replace the body of ``calculate()`` with your engineering.
+ 4. Run it: uv run templates/engineering_script.py
+
+The script follows the same five steps every engineering calculation does:
+
+ INPUTS -> CALCULATE -> REPORT -> PLOT -> EXPORT
+
+The worked example inside (a cantilever beam's tip deflection along its
+length) is only a placeholder - it is there so the template runs out of the
+box and you can see each step produce output.
+
+Why this structure?
+ * All inputs in one dictionary: anyone reviewing the calc can check every
+ assumption in one place, and you can loop over a list of these
+ dictionaries to run a parameter study.
+ * The calculation is a function with keyword-only arguments: it can be
+ imported and tested, and call sites say what each number means.
+ * Results are written to files, because a number in a terminal is not a
+ deliverable - a CSV and a figure are.
+"""
+
+# --- Imports ----------------------------------------------------------------
+# Standard library first, then third-party, then this project. Delete the
+# ones you do not use.
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from fpc import Utility
+
+# --- 1. INPUTS --------------------------------------------------------------
+# Put units in every key. A bare "length" invites someone to type inches.
+INPUTS = {
+ "length_m": 1.5, # cantilever length
+ "tip_load_n": 800.0, # point load at the free end
+ "modulus_pa": 200e9, # Young's modulus (steel)
+ "width_m": 0.04, # rectangular section width
+ "height_m": 0.06, # rectangular section height
+ "allowable_deflection_m": 0.008,
+}
+
+# Where results go: a folder next to this script, whatever directory you run
+# it from. ``Path(__file__)`` is the path of this file.
+OUTPUT_DIR = Path(__file__).resolve().parent / "output"
+
+
+# --- 2. CALCULATE -----------------------------------------------------------
+def calculate(
+ *, length_m, tip_load_n, modulus_pa, width_m, height_m, allowable_deflection_m, points=50
+):
+ """Deflection along a cantilever with a tip load.
+
+ Replace this with your own calculation. Keep it *pure*: inputs in, results
+ out, no printing or file writing - that keeps it easy to test and reuse.
+
+ Deflection at distance x from the wall:
+ y(x) = P * x^2 * (3L - x) / (6 * E * I)
+
+ :param length_m: beam length L [m]
+ :param tip_load_n: tip load P [N]
+ :param modulus_pa: Young's modulus E [Pa]
+ :param width_m: section width b [m]
+ :param height_m: section height h [m]
+ :param allowable_deflection_m: limit on tip deflection [m]
+ :param points: number of points along the beam to evaluate
+ :returns: dict of results (scalars and arrays)
+ """
+ moment_of_inertia = width_m * height_m**3 / 12 # [m^4]
+ # NumPy evaluates the formula at every x at once (no loop needed).
+ x = np.linspace(0.0, length_m, points) # [m]
+ deflection = tip_load_n * x**2 * (3 * length_m - x) / (6 * modulus_pa * moment_of_inertia)
+
+ tip_deflection = deflection[-1] # last point = the tip
+ return {
+ "x_m": x,
+ "deflection_m": deflection,
+ "moment_of_inertia_m4": moment_of_inertia,
+ "tip_deflection_m": tip_deflection,
+ "utilization": tip_deflection / allowable_deflection_m,
+ "passes": tip_deflection <= allowable_deflection_m,
+ }
+
+
+# --- 3. REPORT --------------------------------------------------------------
+def report(*, inputs, results):
+ """Print a short, readable summary - inputs echoed, then key results."""
+ print("Inputs")
+ print("-" * 40)
+ for name, value in inputs.items():
+ # :<26 left-aligns the name in 26 characters; :g picks a tidy format.
+ print(f" {name:<26} {value:g}")
+
+ print("\nResults")
+ print("-" * 40)
+ print(f" {'moment_of_inertia_m4':<26} {results['moment_of_inertia_m4']:.4e}")
+ print(f" {'tip_deflection_mm':<26} {results['tip_deflection_m'] * 1000:.3f}")
+ print(f" {'utilization':<26} {results['utilization']:.0%}")
+ verdict = "PASS" if results["passes"] else "FAIL"
+ print(f"\n Deflection check: {verdict}")
+
+
+# --- 4. PLOT ----------------------------------------------------------------
+def plot(*, inputs, results, save_path):
+ """Save a figure of the result - usually the real deliverable."""
+ figure, axis = plt.subplots(figsize=(7, 4))
+ axis.plot(results["x_m"], results["deflection_m"] * 1000, label="deflection")
+ axis.axhline(
+ inputs["allowable_deflection_m"] * 1000, color="red", linestyle="--", label="allowable"
+ )
+ axis.set_xlabel("Distance from wall [m]")
+ axis.set_ylabel("Deflection [mm]")
+ axis.set_title("Cantilever deflection")
+ axis.grid(True, alpha=0.3)
+ axis.legend()
+ figure.tight_layout()
+ figure.savefig(save_path, dpi=120)
+ plt.close(figure) # free memory; matters when a loop makes many figures
+
+
+# --- 5. EXPORT --------------------------------------------------------------
+def export(*, inputs, results):
+ """Write the inputs (JSON) and the array results (CSV) to OUTPUT_DIR."""
+ OUTPUT_DIR.mkdir(exist_ok=True)
+ utility = Utility()
+ utility.dict_to_json(data=inputs, file_path=OUTPUT_DIR / "inputs.json")
+ # zip pairs the arrays up row by row: (x0, y0), (x1, y1), ...
+ # strict=True raises if the arrays differ in length instead of silently
+ # dropping the extra values.
+ rows = zip(results["x_m"], results["deflection_m"], strict=True)
+ utility.export_csv(
+ rows=rows, file_path=OUTPUT_DIR / "results.csv", header=["x_m", "deflection_m"]
+ )
+ plot(inputs=inputs, results=results, save_path=OUTPUT_DIR / "results.png")
+ print(f"\n Wrote inputs.json, results.csv, results.png to {OUTPUT_DIR}")
+
+
+# --- Run --------------------------------------------------------------------
+# This guard means "only run when executed as a script". Importing this file
+# (e.g. from a test or another script) gives you calculate() without
+# running the whole thing.
+if __name__ == "__main__":
+ results = calculate(**INPUTS) # ** unpacks the dict into keyword arguments
+ report(inputs=INPUTS, results=results)
+ export(inputs=INPUTS, results=results)
diff --git a/tests/test_beam.py b/tests/test_beam.py
index bbb426b..2683841 100644
--- a/tests/test_beam.py
+++ b/tests/test_beam.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
-"""Tests for the unit-aware Beam dataclass (library.beam.Beam).
+"""Tests for the unit-aware Beam dataclass (fpc.beam.Beam).
Shows how to test code that uses Pint: do the math in known units, then call
``.to(...).magnitude`` to compare the numeric value with ``pytest.approx``.
@@ -11,8 +11,8 @@
import pytest
from pint import DimensionalityError
-from library.units import Q_
-from library.beam import Beam
+from fpc.beam import Beam
+from fpc.units import Q_
def make_beam():
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..7733f3d
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Tests for the ``fpc`` command line (fpc.cli) and the argparse tool example.
+
+``main(argv=[...])`` runs the CLI in-process; ``capsys`` captures what it
+prints so we can check the output text.
+"""
+
+import importlib.util
+from pathlib import Path
+
+import pytest
+
+from fpc import cli
+
+
+def test_pipe_flow_command(capsys):
+ cli.main(["pipe-flow", "--flow-l-s", "12", "--diameter-mm", "102.3", "--length-m", "120"])
+ output = capsys.readouterr().out
+ assert "pressure drop" in output
+ assert "kPa" in output
+
+
+def test_water_command(capsys):
+ cli.main(["water", "--temperature-c", "20"])
+ assert "998.21" in capsys.readouterr().out
+
+
+def test_missing_subcommand_exits():
+ # argparse reports usage errors by raising SystemExit (exit code 2).
+ with pytest.raises(SystemExit):
+ cli.main([])
+
+
+def test_step2_cli_example(capsys):
+ """The rung-2 example lives outside the package, so load it by path."""
+ path = Path(__file__).resolve().parent.parent / "examples" / "tools" / "step2_cli.py"
+ spec = importlib.util.spec_from_file_location("step2_cli", path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ module.main(["--flow-l-s", "12", "--diameter-mm", "102.3", "--length-m", "120"])
+ assert "Reynolds" in capsys.readouterr().out
diff --git a/tests/test_console.py b/tests/test_console.py
index 66ad18f..7aba4dc 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -7,6 +7,8 @@
can test "run a line of code and record the result" with no display attached.
"""
+from pathlib import Path
+
from gui import Session
@@ -47,12 +49,12 @@ def test_runtime_error_is_reported_without_crashing():
def test_export_csv_and_json(tmp_path):
- """Recorded results export to both formats via library.Utility."""
+ """Recorded results export to both formats via fpc.Utility."""
session = Session()
session.run(source="dyn.kinetic_energy(mass=2, velocity=3)")
csv_path = session.export_csv(file_path=str(tmp_path / "out.csv"))
json_path = session.export_json(file_path=str(tmp_path / "out.json"))
- assert "label,command,result" in open(csv_path, encoding="utf-8").read()
- assert "out1" in open(json_path, encoding="utf-8").read()
+ assert "label,command,result" in Path(csv_path).read_text(encoding="utf-8")
+ assert "out1" in Path(json_path).read_text(encoding="utf-8")
diff --git a/tests/test_design_properties.py b/tests/test_design_properties.py
new file mode 100644
index 0000000..b57125e
--- /dev/null
+++ b/tests/test_design_properties.py
@@ -0,0 +1,256 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Tests for root finding, interpolation, optimization, and pandas helpers.
+
+Covers fpc.design.Design, fpc.properties.Properties, the Colebrook solver in
+fpc.fluids.Fluids, Thermo's LMTD, and Utility's table read/write.
+"""
+
+import math
+
+import numpy as np
+import pandas as pd
+import pytest
+
+from fpc import Design, Fluids, Properties, Thermo, Utility
+
+design = Design()
+props = Properties()
+fluids = Fluids()
+
+
+# --- Root finding ----------------------------------------------------------
+
+
+def test_beam_height_for_deflection_matches_algebra():
+ """brentq's answer equals the hand-solved h = (P*L^3*12 / (48*E*b*delta))^(1/3)."""
+ load, length, modulus, width, limit = 5000.0, 2.0, 200e9, 0.05, 0.001
+ exact = (load * length**3 * 12 / (48 * modulus * width * limit)) ** (1 / 3)
+ assert design.rectangular_beam_height_for_deflection(
+ load=load,
+ length=length,
+ modulus=modulus,
+ width=width,
+ max_deflection=limit,
+ ) == pytest.approx(exact, rel=1e-9)
+ assert exact == pytest.approx(0.1) # same beam as the README Beam example
+
+
+def test_beam_height_bad_bracket_raises():
+ with pytest.raises(ValueError):
+ design.rectangular_beam_height_for_deflection(
+ load=5000.0,
+ length=2.0,
+ modulus=200e9,
+ width=0.05,
+ max_deflection=0.001,
+ min_height=0.5,
+ max_height=1.0,
+ )
+
+
+@pytest.mark.parametrize(
+ "reynolds, roughness",
+ [
+ (1e4, 0.0),
+ (1e5, 1e-4),
+ (1e6, 1e-3),
+ (1e7, 1e-2),
+ ],
+)
+def test_colebrook_satisfies_its_own_equation(reynolds, roughness):
+ """Plug the solved f back into Colebrook: both sides must agree."""
+ f = fluids.colebrook_friction_factor(reynolds=reynolds, relative_roughness=roughness)
+ lhs = 1 / math.sqrt(f)
+ rhs = -2 * math.log10(roughness / 3.7 + 2.51 / (reynolds * math.sqrt(f)))
+ assert lhs == pytest.approx(rhs, rel=1e-9)
+ # Swamee-Jain is an explicit approximation within ~1-2% of Colebrook.
+ approx = fluids.swamee_jain_friction_factor(reynolds=reynolds, relative_roughness=roughness)
+ assert approx == pytest.approx(f, rel=0.02)
+
+
+def test_colebrook_known_value_and_laminar():
+ """Moody chart: Re = 1e5, eps/D = 1e-4 gives f ~= 0.0185."""
+ assert fluids.colebrook_friction_factor(
+ reynolds=1e5,
+ relative_roughness=1e-4,
+ ) == pytest.approx(0.0185, abs=2e-4)
+ assert fluids.colebrook_friction_factor(
+ reynolds=1000,
+ relative_roughness=1e-3,
+ ) == pytest.approx(0.064)
+
+
+# --- Optimization ----------------------------------------------------------
+
+
+def test_lightest_beam_is_feasible_and_on_its_limits():
+ limits = dict(allowable_stress=150e6, max_deflection=0.005)
+ result = design.lightest_rectangular_beam(
+ load=5000.0,
+ length=2.0,
+ modulus=200e9,
+ density=7850.0,
+ **limits,
+ )
+ assert result["stress"] <= limits["allowable_stress"] * (1 + 1e-6)
+ assert result["deflection"] <= limits["max_deflection"] * (1 + 1e-6)
+ assert result["height"] <= 4 * result["width"] * (1 + 1e-6)
+ # Hand solution: deflection + aspect ratio active -> b*h^3 = 1e-5 with h = 4b.
+ width = (1e-5 / 64) ** 0.25
+ assert result["width"] == pytest.approx(width, rel=1e-3)
+ assert result["height"] == pytest.approx(4 * width, rel=1e-3)
+ assert set(result["active"]) == {"deflection", "aspect_ratio"}
+
+
+def test_lightest_beam_stress_governed_case():
+ """A heavy load with a loose deflection limit makes stress govern instead."""
+ result = design.lightest_rectangular_beam(
+ load=20000.0,
+ length=2.0,
+ modulus=200e9,
+ density=7850.0,
+ allowable_stress=150e6,
+ max_deflection=0.05,
+ )
+ assert "stress" in result["active"]
+ assert "deflection" not in result["active"]
+
+
+# --- Properties: pandas + interpolation ------------------------------------
+
+
+def test_tables_load_as_dataframes():
+ for name in props.list_tables():
+ frame = props.load_table(name=name)
+ assert isinstance(frame, pd.DataFrame)
+ assert not frame.empty
+ with pytest.raises(ValueError):
+ props.load_table(name="unobtainium")
+
+
+def test_load_table_returns_independent_copy():
+ frame = props.load_table(name="materials")
+ frame.loc[0, "density_kg_m3"] = -1
+ assert props.load_table(name="materials").loc[0, "density_kg_m3"] > 0
+
+
+def test_water_saturation_at_table_points():
+ """At a tabulated temperature, interpolation returns the table value exactly."""
+ assert props.water_saturation(
+ temperature_c=100,
+ property="pressure_kpa",
+ ) == pytest.approx(101.4)
+ assert props.water_saturation(
+ temperature_c=100,
+ property="enthalpy_vaporization_kj_kg",
+ ) == pytest.approx(2256.4)
+
+
+def test_linear_interpolation_between_points():
+ """Halfway between 20 and 25 degC, linear interpolation gives the average."""
+ expected = (998.21 + 997.05) / 2
+ assert props.water_liquid(
+ temperature_c=22.5,
+ property="density_kg_m3",
+ ) == pytest.approx(expected)
+
+
+def test_cubic_beats_linear_on_curved_data():
+ """Saturation pressure is strongly curved: check against a tabulated point
+ we deliberately remove, and the cubic spline should land closer."""
+ table = props.load_table(name="water_saturation")
+ held_out = table[table["temperature_c"] == 90].iloc[0]["pressure_kpa"]
+ reduced = table[table["temperature_c"] != 90]
+ linear = props.interpolate(
+ table=reduced, x_column="temperature_c", y_column="pressure_kpa", x=90
+ )
+ cubic = props.interpolate(
+ table=reduced, x_column="temperature_c", y_column="pressure_kpa", x=90, method="cubic"
+ )
+ assert abs(cubic - held_out) < abs(linear - held_out)
+
+
+def test_interpolation_accepts_arrays_and_refuses_extrapolation():
+ values = props.water_liquid(temperature_c=np.array([10, 20]), property="density_kg_m3")
+ assert values == pytest.approx([999.70, 998.21])
+ with pytest.raises(ValueError):
+ props.water_liquid(temperature_c=150, property="density_kg_m3")
+
+
+def test_material_lookup():
+ al = props.material(name="aluminum_6061_t6")
+ assert al["yield_strength_mpa"] == 276
+ with pytest.raises(ValueError):
+ props.material(name="unobtainium")
+
+
+def test_lightest_w_shape():
+ assert props.lightest_w_shape(required_sx_in3=30)["designation"] == "W16x26"
+ assert props.lightest_w_shape(required_sx_in3=5)["designation"] == "W6x9"
+ with pytest.raises(ValueError):
+ props.lightest_w_shape(required_sx_in3=1e6)
+
+
+def test_w_shape_table_is_self_consistent():
+ """Catch typos in the steel table with physics: Sx = Ix / (d/2), and
+ weight [lb/ft] = area [in^2] * 490 lb/ft^3 / 144 in^2/ft^2 (~3.40 * A)."""
+ shapes = props.load_table(name="w_shapes")
+ sx_from_ix = shapes["ix_in4"] / (shapes["depth_in"] / 2)
+ assert np.allclose(shapes["sx_in3"], sx_from_ix, rtol=0.01)
+ assert np.allclose(shapes["weight_lb_ft"], shapes["area_in2"] * 490 / 144, rtol=0.02)
+
+
+# --- Thermo LMTD ------------------------------------------------------------
+
+
+def test_lmtd():
+ assert Thermo().log_mean_temperature_difference(
+ delta_t_1=60.0,
+ delta_t_2=30.0,
+ ) == pytest.approx(30 / math.log(2))
+ assert Thermo().log_mean_temperature_difference(
+ delta_t_1=20.0,
+ delta_t_2=20.0,
+ ) == pytest.approx(20.0)
+ with pytest.raises(ValueError):
+ Thermo().log_mean_temperature_difference(delta_t_1=-5.0, delta_t_2=20.0)
+
+
+# --- Utility: pandas read/write -------------------------------------------
+
+
+@pytest.mark.parametrize("suffix", [".csv", ".xlsx"])
+def test_table_round_trip(tmp_path, suffix):
+ utility = Utility()
+ frame = pd.DataFrame({"time_s": [0.0, 1.0], "pressure_kpa": [101.3, 99.8]})
+ path = utility.write_table(frame=frame, file_path=tmp_path / f"log{suffix}")
+ # check_dtype=False: Excel has no int/float distinction, so 0.0 and 1.0
+ # come back as integers. The values are identical; only the type differs.
+ pd.testing.assert_frame_equal(
+ utility.read_table(file_path=path),
+ frame,
+ check_dtype=False,
+ )
+
+
+def test_table_bad_extension(tmp_path):
+ with pytest.raises(ValueError):
+ Utility().read_table(file_path=tmp_path / "log.txt")
+
+
+def test_pipe_flow_chains_the_pieces():
+ result = fluids.pipe_flow(
+ flow=0.012,
+ diameter=0.1023,
+ length=120.0,
+ roughness=0.045e-3,
+ density=998.2,
+ viscosity=1.0016e-3,
+ )
+ assert result["velocity"] == pytest.approx(0.012 / (math.pi * 0.1023**2 / 4))
+ assert result["pressure_drop"] == pytest.approx(998.2 * fluids.GRAVITY * result["head_loss"])
+ assert 0.015 < result["friction_factor"] < 0.025
+ with pytest.raises(ValueError):
+ fluids.pipe_flow(flow=0, diameter=0.1, length=1, roughness=0, density=1000, viscosity=1e-3)
diff --git a/tests/test_dynamics.py b/tests/test_dynamics.py
index 88c65ce..f797ba9 100644
--- a/tests/test_dynamics.py
+++ b/tests/test_dynamics.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
-"""Tests for library.dynamics.Dynamics.
+"""Tests for fpc.dynamics.Dynamics.
These tests double as a pytest tutorial. Notice three ideas:
@@ -16,7 +16,7 @@
import pytest
-from library.dynamics import Dynamics
+from fpc.dynamics import Dynamics
# One shared instance is fine because the methods hold no state.
dyn = Dynamics()
@@ -25,8 +25,10 @@
def test_displacement_matches_kinematic_formula():
"""x = x0 + v0*t + 1/2*a*t^2 for the README's headline example."""
result = dyn.displacement_constant_acceleration(
- initial_position=0.0, initial_velocity=5.0,
- acceleration=9.81, time=3.0,
+ initial_position=0.0,
+ initial_velocity=5.0,
+ acceleration=9.81,
+ time=3.0,
)
assert result == pytest.approx(59.145)
@@ -34,23 +36,27 @@ def test_displacement_matches_kinematic_formula():
def test_final_velocity_is_linear_in_time():
"""v = v0 + a*t: starting from rest at 9.81 m/s^2 for 2 s gives 19.62 m/s."""
assert dyn.final_velocity(
- initial_velocity=0.0, acceleration=9.81, time=2.0,
+ initial_velocity=0.0,
+ acceleration=9.81,
+ time=2.0,
) == pytest.approx(19.62)
def test_velocity_from_distance():
"""v^2 = v0^2 + 2*a*d, so from rest with a=2 over d=4 gives v=4 m/s."""
assert dyn.velocity_from_distance(
- initial_velocity=0.0, acceleration=2.0, distance=4.0,
+ initial_velocity=0.0,
+ acceleration=2.0,
+ distance=4.0,
) == pytest.approx(4.0)
@pytest.mark.parametrize(
"angle_deg, expected_range",
[
- (45, 40.7886), # 45 degrees gives the maximum range
+ (45, 40.7886), # 45 degrees gives the maximum range
(30, 35.3240),
- (60, 35.3240), # complementary angles share the same range
+ (60, 35.3240), # complementary angles share the same range
],
)
def test_projectile_range_over_angles(angle_deg, expected_range):
@@ -70,8 +76,11 @@ def test_newtons_second_law_and_energy():
def test_projectile_drag_lands_and_falls_short_of_vacuum():
"""solve_ivp: air drag must shorten the range and the run ends at impact."""
time, x, y = dyn.simulate_projectile_drag(
- speed=40.0, angle_deg=45.0, mass=0.145,
- drag_coefficient=0.3, frontal_area=0.0042,
+ speed=40.0,
+ angle_deg=45.0,
+ mass=0.145,
+ drag_coefficient=0.3,
+ frontal_area=0.0042,
)
# The three arrays describe the same trajectory, so they share a length.
assert time.shape == x.shape == y.shape
diff --git a/tests/test_examples.py b/tests/test_examples.py
new file mode 100644
index 0000000..eb4908b
--- /dev/null
+++ b/tests/test_examples.py
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Smoke tests: every example script and the template run without errors.
+
+Examples are documentation, and documentation that crashes teaches the wrong
+lesson. Each script runs in a fresh Python process, exactly as a user would
+run it, with a non-interactive matplotlib backend so no windows open.
+"""
+
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parent.parent
+SCRIPTS = sorted((ROOT / "examples").glob("*.py")) + [
+ ROOT / "templates" / "engineering_script.py",
+]
+
+
+@pytest.mark.parametrize("script", SCRIPTS, ids=lambda path: path.name)
+def test_script_runs(script):
+ environment = {**os.environ, "MPLBACKEND": "Agg"}
+ completed = subprocess.run(
+ [sys.executable, str(script)],
+ cwd=ROOT,
+ env=environment,
+ capture_output=True,
+ text=True,
+ timeout=120,
+ )
+ assert completed.returncode == 0, completed.stderr
diff --git a/tests/test_gear_electrical_mechatronics.py b/tests/test_gear_electrical_mechatronics.py
index a37caf5..489c5b4 100644
--- a/tests/test_gear_electrical_mechatronics.py
+++ b/tests/test_gear_electrical_mechatronics.py
@@ -7,9 +7,9 @@
import pytest
-from library.gear import Gear
-from library.electrical import Electrical
-from library.mechatronics import Mechatronics
+from fpc.electrical import Electrical
+from fpc.gear import Gear
+from fpc.mechatronics import Mechatronics
gear = Gear()
electrical = Electrical()
@@ -18,6 +18,7 @@
# --- Gear ------------------------------------------------------------------
+
def test_gear_ratio_reduces_speed_and_multiplies_torque():
ratio = gear.gear_ratio(driver_teeth=10, driven_teeth=40)
assert ratio == pytest.approx(4.0)
@@ -25,7 +26,8 @@ def test_gear_ratio_reduces_speed_and_multiplies_torque():
assert gear.output_speed(input_speed=1000.0, gear_ratio=ratio) == pytest.approx(250.0)
# ...and (ideally) quadruples the torque.
assert gear.output_torque(
- input_torque=10.0, gear_ratio=ratio,
+ input_torque=10.0,
+ gear_ratio=ratio,
) == pytest.approx(40.0)
@@ -35,6 +37,7 @@ def test_powertrain_ratio_is_the_product_of_stages():
# --- Electrical ------------------------------------------------------------
+
def test_parallel_resistance_is_below_the_smallest_resistor():
total = electrical.parallel_resistance(resistances=[100.0, 100.0])
assert total == pytest.approx(50.0)
@@ -49,24 +52,28 @@ def test_series_resistance_adds():
def test_rc_time_constant_and_power():
assert electrical.rc_time_constant(
- resistance=1000.0, capacitance=1e-6,
+ resistance=1000.0,
+ capacitance=1e-6,
) == pytest.approx(1e-3)
assert electrical.power(voltage=10.0, current=2.0) == pytest.approx(20.0)
# --- Mechatronics ----------------------------------------------------------
+
def test_ballscrew_linear_speed():
"""600 rpm on a 5 mm lead -> 50 mm/s."""
assert mechatronics.ballscrew_linear_speed(
- motor_rpm=600.0, lead=5.0,
+ motor_rpm=600.0,
+ lead=5.0,
) == pytest.approx(50.0)
def test_reflected_inertia_falls_with_square_of_ratio():
"""A 2:1 reduction makes the load look 1/4 as heavy to the motor."""
assert mechatronics.reflected_inertia(
- load_inertia=0.04, gear_ratio=2.0,
+ load_inertia=0.04,
+ gear_ratio=2.0,
) == pytest.approx(0.01)
@@ -74,7 +81,8 @@ def test_motor_power_from_torque_and_speed():
"""P = T*omega, with omega = rpm*2*pi/60."""
expected = 2.0 * (3000.0 * 2 * math.pi / 60.0)
assert mechatronics.motor_power(
- torque=2.0, speed_rpm=3000.0,
+ torque=2.0,
+ speed_rpm=3000.0,
) == pytest.approx(expected)
diff --git a/tests/test_modal_controls.py b/tests/test_modal_controls.py
index 3b966c2..62e2fc0 100644
--- a/tests/test_modal_controls.py
+++ b/tests/test_modal_controls.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
-"""Tests for library.modal.Modal and library.controls.Controls.
+"""Tests for fpc.modal.Modal and fpc.controls.Controls.
Shows how to test code that returns numpy arrays (check the shape and a few
values) and how to test a simulation (check it converges to the right answer).
@@ -12,8 +12,8 @@
import numpy as np
import pytest
-from library.modal import Modal
-from library.controls import Controls
+from fpc.controls import Controls
+from fpc.modal import Modal
modal = Modal()
controls = Controls()
@@ -21,6 +21,7 @@
# --- Modal -----------------------------------------------------------------
+
def test_natural_frequency():
"""omega_n = sqrt(k/m); f_n = omega_n/(2*pi)."""
omega_n, f_n = modal.natural_frequency(stiffness=1000.0, mass=2.0)
@@ -39,9 +40,10 @@ def test_damped_natural_frequency():
test_modal = Modal()
- with patch.object(test_modal, "natural_frequency", return_value=(10.0, None)) as mock_nf, \
- patch.object(test_modal, "damping_ratio") as mock_dr:
-
+ with (
+ patch.object(test_modal, "natural_frequency", return_value=(10.0, None)) as mock_nf,
+ patch.object(test_modal, "damping_ratio") as mock_dr,
+ ):
# Underdamped case: zeta = 0.5
mock_dr.return_value = 0.5
omega_d = test_modal.damped_natural_frequency(stiffness=1000.0, mass=10.0, damping=5.0)
@@ -73,15 +75,21 @@ def test_simulate_then_fit_recovers_parameters():
true_zeta = modal.damping_ratio(damping=damping, mass=mass, stiffness=stiffness)
time, displacement = modal.simulate_free_vibration(
- mass=mass, stiffness=stiffness, damping=damping,
- initial_displacement=0.01, duration=5.0, num_points=600,
+ mass=mass,
+ stiffness=stiffness,
+ damping=damping,
+ initial_displacement=0.01,
+ duration=5.0,
+ num_points=600,
)
# The response should start at the release point and decay toward zero.
assert displacement[0] == pytest.approx(0.01)
assert abs(displacement[-1]) < abs(displacement[0])
fit = modal.fit_damped_response(
- time=time, displacement=displacement, guess_frequency_hz=3.0,
+ time=time,
+ displacement=displacement,
+ guess_frequency_hz=3.0,
)
assert fit["natural_frequency_hz"] == pytest.approx(true_f_n, rel=1e-2)
assert fit["damping_ratio"] == pytest.approx(true_zeta, rel=5e-2)
@@ -101,10 +109,16 @@ def test_mode_shapes_returns_sorted_frequencies():
# --- Controls --------------------------------------------------------------
+
def test_pid_output_sums_three_terms():
"""u = Kp*e + Ki*integral + Kd*derivative."""
u = controls.pid_output(
- error=2.0, integral=1.0, derivative=0.5, kp=3.0, ki=2.0, kd=1.0,
+ error=2.0,
+ integral=1.0,
+ derivative=0.5,
+ kp=3.0,
+ ki=2.0,
+ kd=1.0,
)
assert u == pytest.approx(3 * 2 + 2 * 1 + 1 * 0.5)
@@ -112,15 +126,19 @@ def test_pid_output_sums_three_terms():
def test_damping_from_overshoot_inverts_correctly():
"""Feeding the derived zeta back through the overshoot formula returns 5%."""
zeta = controls.damping_from_overshoot(percent_overshoot=5.0)
- overshoot = math.exp(-zeta * math.pi / math.sqrt(1 - zeta ** 2)) * 100
+ overshoot = math.exp(-zeta * math.pi / math.sqrt(1 - zeta**2)) * 100
assert overshoot == pytest.approx(5.0)
def test_pid_simulation_converges_to_setpoint():
"""A PI controller should drive a first-order plant to its target."""
time, output = controls.simulate_pid_first_order(
- setpoint=1.0, time_constant=0.5, gain=1.0,
- kp=4.0, ki=2.0, duration=15.0,
+ setpoint=1.0,
+ time_constant=0.5,
+ gain=1.0,
+ kp=4.0,
+ ki=2.0,
+ duration=15.0,
)
# The time and output arrays line up...
assert time.shape == output.shape
diff --git a/tests/test_notebooks.py b/tests/test_notebooks.py
new file mode 100644
index 0000000..53c70d1
--- /dev/null
+++ b/tests/test_notebooks.py
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2026 Evan Gress
+
+"""Execute every notebook top to bottom; any cell error fails the test.
+
+Needs the optional notebook tools (``uv sync --group notebooks``; CI installs
+all groups). Without them this module is skipped rather than failing.
+"""
+
+from pathlib import Path
+
+import pytest
+
+nbformat = pytest.importorskip("nbformat")
+nbclient = pytest.importorskip("nbclient")
+
+NOTEBOOKS = sorted((Path(__file__).resolve().parent.parent / "notebooks").glob("*.ipynb"))
+
+
+@pytest.mark.parametrize("path", NOTEBOOKS, ids=lambda p: p.name)
+def test_notebook_executes(path):
+ notebook = nbformat.read(path, as_version=4)
+ # Run with the notebooks folder as the working directory, like Jupyter does.
+ nbclient.NotebookClient(
+ notebook,
+ timeout=120,
+ kernel_name="python3",
+ resources={"metadata": {"path": str(path.parent)}},
+ ).execute()
diff --git a/tests/test_radiology.py b/tests/test_radiology.py
index a79048f..a1ab4e4 100644
--- a/tests/test_radiology.py
+++ b/tests/test_radiology.py
@@ -13,7 +13,7 @@
import pytest
from pydicom.data import get_testdata_file
-from library import CT, MRI, Dicom, Radiation, Radiography, Ultrasound
+from fpc import CT, MRI, Dicom, Radiation, Radiography, Ultrasound
radiation = Radiation()
radiography = Radiography()
@@ -25,9 +25,12 @@
# --- Radiation ---------------------------------------------------------------
+
def test_inverse_square_doubling_distance_quarters_intensity():
assert radiation.inverse_square_law(
- initial_intensity=100.0, initial_distance=1.0, new_distance=2.0,
+ initial_intensity=100.0,
+ initial_distance=1.0,
+ new_distance=2.0,
) == pytest.approx(25.0)
@@ -36,7 +39,9 @@ def test_one_hvl_halves_the_beam():
hvl = radiation.half_value_layer(attenuation_coefficient=mu)
assert hvl == pytest.approx(math.log(2) / mu)
assert radiation.transmitted_intensity(
- initial_intensity=1.0, attenuation_coefficient=mu, thickness=hvl,
+ initial_intensity=1.0,
+ attenuation_coefficient=mu,
+ thickness=hvl,
) == pytest.approx(0.5)
@@ -49,7 +54,9 @@ def test_tvl_is_about_3_32_hvls():
def test_shielding_thickness_inverts_attenuation():
x = radiation.shielding_thickness(attenuation_coefficient=0.5, transmission=0.01)
assert radiation.transmitted_intensity(
- initial_intensity=1.0, attenuation_coefficient=0.5, thickness=x,
+ initial_intensity=1.0,
+ attenuation_coefficient=0.5,
+ thickness=x,
) == pytest.approx(0.01)
@@ -60,7 +67,9 @@ def test_duane_hunt_minimum_wavelength_at_100_kvp():
def test_tc99m_after_two_half_lives():
"""Tc-99m (6.01 h): after 12.02 h a quarter of the activity remains."""
assert radiation.remaining_activity(
- initial_activity=800.0, half_life=6.01, elapsed_time=12.02,
+ initial_activity=800.0,
+ half_life=6.01,
+ elapsed_time=12.02,
) == pytest.approx(200.0)
@@ -69,12 +78,19 @@ def test_effective_half_life_is_shorter_than_both():
assert te == pytest.approx(4.8)
-@pytest.mark.parametrize("radiation_type, expected_sv", [
- ("photon", 2.0), ("electron", 2.0), ("proton", 4.0), ("alpha", 40.0),
-])
+@pytest.mark.parametrize(
+ "radiation_type, expected_sv",
+ [
+ ("photon", 2.0),
+ ("electron", 2.0),
+ ("proton", 4.0),
+ ("alpha", 40.0),
+ ],
+)
def test_equivalent_dose(radiation_type, expected_sv):
assert radiation.equivalent_dose(
- absorbed_dose=2.0, radiation_type=radiation_type,
+ absorbed_dose=2.0,
+ radiation_type=radiation_type,
) == pytest.approx(expected_sv)
@@ -105,6 +121,7 @@ def test_partial_body_effective_dose():
# --- Radiography -------------------------------------------------------------
+
def test_mas_reciprocity():
assert radiography.mas(tube_current_ma=200, exposure_time_s=0.1) == pytest.approx(
radiography.mas(tube_current_ma=400, exposure_time_s=0.05)
@@ -120,7 +137,9 @@ def test_fifteen_percent_rule_round_trip():
def test_exposure_maintenance_100_to_180_cm():
assert radiography.exposure_maintenance_mas(
- original_mas=10, original_sid=100, new_sid=180,
+ original_mas=10,
+ original_sid=100,
+ new_sid=180,
) == pytest.approx(32.4)
@@ -130,7 +149,10 @@ def test_grid_mas_adding_12_to_1_grid():
def test_heat_units_high_frequency():
assert radiography.heat_units(
- kvp=80, tube_current_ma=200, exposure_time_s=0.1, generator="high_frequency",
+ kvp=80,
+ tube_current_ma=200,
+ exposure_time_s=0.1,
+ generator="high_frequency",
) == pytest.approx(2320.0)
@@ -141,7 +163,9 @@ def test_magnification_and_true_size():
def test_geometric_unsharpness():
assert radiography.geometric_unsharpness(
- focal_spot_size=1.0, sid=100, oid=20,
+ focal_spot_size=1.0,
+ sid=100,
+ oid=20,
) == pytest.approx(0.25)
@@ -151,12 +175,14 @@ def test_nyquist_limit_for_100_um_detector():
def test_deviation_index_doubling_is_plus_three():
assert radiography.exposure_index_deviation(
- exposure_index=800, target_exposure_index=400,
+ exposure_index=800,
+ target_exposure_index=400,
) == pytest.approx(3.01, abs=0.01)
# --- CT ----------------------------------------------------------------------
+
def test_hounsfield_reference_points():
hu = ct.hounsfield_units(attenuation_coefficient=[0.0, 0.19, 0.38], water_attenuation=0.19)
np.testing.assert_allclose(hu, [-1000.0, 0.0, 1000.0])
@@ -182,11 +208,17 @@ def test_window_requires_width_and_level():
ct.window_bounds(window_width=400)
-@pytest.mark.parametrize("hu, label", [
- (-1000, "air"), (-700, "lung / aerated tissue"), (-100, "fat"),
- (0, "water / simple fluid"), (45, "soft tissue / blood"),
- (1000, "bone / calcification / metal"),
-])
+@pytest.mark.parametrize(
+ "hu, label",
+ [
+ (-1000, "air"),
+ (-700, "lung / aerated tissue"),
+ (-100, "fat"),
+ (0, "water / simple fluid"),
+ (45, "soft tissue / blood"),
+ (1000, "bone / calcification / metal"),
+ ],
+)
def test_classify_hounsfield(hu, label):
assert ct.classify_hounsfield(hounsfield=hu) == label
@@ -199,6 +231,7 @@ def test_head_ct_effective_dose():
# --- MRI ---------------------------------------------------------------------
+
def test_larmor_frequency_at_1_5_t():
assert mri.larmor_frequency(field_strength_t=1.5) == pytest.approx(63.87, abs=0.01)
@@ -222,16 +255,22 @@ def test_csf_brighter_than_white_matter_on_t2():
assert csf > wm
-@pytest.mark.parametrize("tr, te, weighting", [
- (500, 15, "T1-weighted"), (4000, 100, "T2-weighted"),
- (3000, 15, "PD-weighted"), (1500, 50, "mixed"),
-])
+@pytest.mark.parametrize(
+ "tr, te, weighting",
+ [
+ (500, 15, "T1-weighted"),
+ (4000, 100, "T2-weighted"),
+ (3000, 15, "PD-weighted"),
+ (1500, 50, "mixed"),
+ ],
+)
def test_sequence_weighting(tr, te, weighting):
assert mri.sequence_weighting(repetition_time_ms=tr, echo_time_ms=te) == weighting
# --- Ultrasound --------------------------------------------------------------
+
def test_wavelength_5_mhz_soft_tissue():
assert ultrasound.wavelength(frequency_mhz=5) == pytest.approx(0.308)
@@ -258,19 +297,21 @@ def test_doppler_shift_and_inverse_agree():
shift = ultrasound.doppler_shift(transmitted_frequency_mhz=5, velocity=0.5, angle_deg=60)
assert shift == pytest.approx(1623.4, rel=1e-3)
velocity = ultrasound.doppler_velocity(
- transmitted_frequency_mhz=5, doppler_shift_hz=shift, angle_deg=60,
+ transmitted_frequency_mhz=5,
+ doppler_shift_hz=shift,
+ angle_deg=60,
)
assert velocity == pytest.approx(0.5)
def test_doppler_at_90_degrees_raises():
with pytest.raises(ValueError):
- ultrasound.doppler_velocity(transmitted_frequency_mhz=5, doppler_shift_hz=100,
- angle_deg=90)
+ ultrasound.doppler_velocity(transmitted_frequency_mhz=5, doppler_shift_hz=100, angle_deg=90)
# --- DICOM (uses pydicom's bundled sample CT - no patient data) --------------
+
@pytest.fixture
def ct_dataset():
return dicom.read(path=get_testdata_file("CT_small.dcm"))
@@ -285,8 +326,9 @@ def test_dicom_summary_has_modality(ct_dataset):
def test_dicom_pixels_are_converted_to_hu(ct_dataset):
raw = ct_dataset.pixel_array.astype(float)
hu = dicom.pixel_array_in_units(dataset=ct_dataset)
- np.testing.assert_allclose(hu, raw * float(ct_dataset.RescaleSlope)
- + float(ct_dataset.RescaleIntercept))
+ np.testing.assert_allclose(
+ hu, raw * float(ct_dataset.RescaleSlope) + float(ct_dataset.RescaleIntercept)
+ )
def test_dicom_anonymize_copies_and_strips_phi(ct_dataset, tmp_path):
diff --git a/tests/test_statics_material.py b/tests/test_statics_material.py
index 2a4baae..ed71813 100644
--- a/tests/test_statics_material.py
+++ b/tests/test_statics_material.py
@@ -1,12 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
-"""Tests for library.statics.Statics and library.material.Material."""
+"""Tests for fpc.statics.Statics and fpc.material.Material."""
import pytest
-from library.statics import Statics
-from library.material import Material
+from fpc.material import Material
+from fpc.statics import Statics
statics = Statics()
material = Material()
@@ -14,10 +14,13 @@
# --- Statics ---------------------------------------------------------------
+
def test_beam_reactions_sum_to_the_load():
"""A simply supported beam's two reactions must carry the whole load."""
reaction_a, reaction_b = statics.beam_reactions_point_load(
- span=10.0, load=1000.0, load_position=3.0,
+ span=10.0,
+ load=1000.0,
+ load_position=3.0,
)
assert reaction_a == pytest.approx(700.0)
assert reaction_b == pytest.approx(300.0)
@@ -34,15 +37,18 @@ def test_resultant_of_3_4_5_triangle():
def test_factor_of_safety_and_friction():
assert statics.factor_of_safety(
- strength=250e6, applied_stress=125e6,
+ strength=250e6,
+ applied_stress=125e6,
) == pytest.approx(2.0)
assert statics.friction_force(
- coefficient=0.3, normal_force=100.0,
+ coefficient=0.3,
+ normal_force=100.0,
) == pytest.approx(30.0)
# --- Material --------------------------------------------------------------
+
def test_hookes_law_round_trip():
"""stress/strain should recover Young's modulus."""
stress = material.stress(force=1000.0, area=0.01)
@@ -55,7 +61,9 @@ def test_hookes_law_round_trip():
def test_euler_buckling_load():
"""Critical load of a pinned-pinned column matches pi^2*E*I/L^2."""
load = material.euler_buckling_load(
- modulus=200e9, moment_of_inertia=4e-8, length=2.0,
+ modulus=200e9,
+ moment_of_inertia=4e-8,
+ length=2.0,
)
assert load == pytest.approx(19739.2, rel=1e-4)
@@ -63,9 +71,13 @@ def test_euler_buckling_load():
def test_thermal_expansion_scales_with_temperature():
"""dL is proportional to the temperature change."""
small = material.thermal_expansion(
- coefficient=12e-6, original_length=1.0, delta_temperature=50.0,
+ coefficient=12e-6,
+ original_length=1.0,
+ delta_temperature=50.0,
)
big = material.thermal_expansion(
- coefficient=12e-6, original_length=1.0, delta_temperature=100.0,
+ coefficient=12e-6,
+ original_length=1.0,
+ delta_temperature=100.0,
)
assert big == pytest.approx(2 * small)
diff --git a/tests/test_thermo_fluids.py b/tests/test_thermo_fluids.py
index 2ad93b3..2db23eb 100644
--- a/tests/test_thermo_fluids.py
+++ b/tests/test_thermo_fluids.py
@@ -1,12 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
-"""Tests for library.thermo.Thermo and library.fluids.Fluids."""
+"""Tests for fpc.thermo.Thermo and fpc.fluids.Fluids."""
import pytest
-from library.thermo import Thermo
-from library.fluids import Fluids
+from fpc.fluids import Fluids
+from fpc.thermo import Thermo
thermo = Thermo()
fluids = Fluids()
@@ -14,10 +14,12 @@
# --- Thermo ----------------------------------------------------------------
+
def test_carnot_efficiency():
"""1 - Tc/Th for an 800 K / 300 K engine is 0.625."""
assert thermo.carnot_efficiency(
- hot_temperature=800.0, cold_temperature=300.0,
+ hot_temperature=800.0,
+ cold_temperature=300.0,
) == pytest.approx(0.625)
@@ -27,25 +29,36 @@ def test_celsius_to_kelvin():
def test_conduction_and_convection_rates():
assert thermo.conduction_heat_rate(
- conductivity=0.5, area=2.0, delta_temperature=20.0, thickness=0.1,
+ conductivity=0.5,
+ area=2.0,
+ delta_temperature=20.0,
+ thickness=0.1,
) == pytest.approx(200.0)
assert thermo.convection_heat_rate(
- coefficient=25.0, area=2.0, delta_temperature=20.0,
+ coefficient=25.0,
+ area=2.0,
+ delta_temperature=20.0,
) == pytest.approx(1000.0)
# --- Fluids ----------------------------------------------------------------
+
def test_reynolds_number():
assert fluids.reynolds_number(
- density=1000.0, velocity=2.0, length=0.05, viscosity=1e-3,
+ density=1000.0,
+ velocity=2.0,
+ length=0.05,
+ viscosity=1e-3,
) == pytest.approx(1e5)
def test_continuity_speeds_flow_in_a_narrower_pipe():
"""Halving the area doubles the velocity (A1*v1 = A2*v2)."""
v_out = fluids.continuity_velocity(
- area_in=0.1, velocity_in=2.0, area_out=0.05,
+ area_in=0.1,
+ velocity_in=2.0,
+ area_out=0.05,
)
assert v_out == pytest.approx(4.0)
@@ -54,8 +67,11 @@ def test_bernoulli_trades_velocity_for_pressure():
"""Speeding the fluid up (0 -> 10 m/s) must drop the static pressure."""
p2 = fluids.bernoulli_pressure(
density=1000.0,
- velocity_1=0.0, height_1=0.0, pressure_1=200000.0,
- velocity_2=10.0, height_2=0.0,
+ velocity_1=0.0,
+ height_1=0.0,
+ pressure_1=200000.0,
+ velocity_2=10.0,
+ height_2=0.0,
)
assert p2 == pytest.approx(150000.0)
assert p2 < 200000.0
diff --git a/tests/test_utility.py b/tests/test_utility.py
index 0e4a24e..fa45eba 100644
--- a/tests/test_utility.py
+++ b/tests/test_utility.py
@@ -1,14 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Evan Gress
-"""Tests for library.utility.Utility.
+"""Tests for fpc.utility.Utility.
Demonstrates pytest's ``tmp_path`` fixture: pytest hands each test a unique,
automatically-cleaned temporary directory, so file I/O can be tested without
leaving junk behind or touching the real project files.
"""
-from library.utility import Utility
+from fpc.utility import Utility
utility = Utility()
@@ -33,5 +33,5 @@ def test_export_csv_writes_header_and_rows(tmp_path):
lines = path.read_text(encoding="utf-8").strip().splitlines()
assert lines[0] == "material,E_GPa"
- assert len(lines) == 3 # 1 header + 2 data rows
+ assert len(lines) == 3 # 1 header + 2 data rows
assert "steel,200" in lines[1]
diff --git a/uv.lock b/uv.lock
index 422f09e..8aa8220 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,442 @@
version = 1
revision = 3
requires-python = ">=3.14"
+resolution-markers = [
+ "python_full_version >= '3.15' and sys_platform == 'win32'",
+ "python_full_version >= '3.15' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version < '3.15' and sys_platform == 'win32'",
+ "python_full_version < '3.15' and sys_platform == 'emscripten'",
+ "python_full_version < '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+
+[[package]]
+name = "altair"
+version = "6.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jinja2" },
+ { name = "jsonschema" },
+ { name = "narwhals" },
+ { name = "packaging" },
+ { name = "typing-extensions", marker = "python_full_version < '3.15'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e8/de/9731304fae332b5783fdc3ed18531dfaf4a3cc62a66aaaa74c047355b70f/altair-6.3.0.tar.gz", hash = "sha256:fa632121aca6d0fcb198a3d8a9f7d937fa5b7518ec947951cfffb44e736d97c8", size = 766362, upload-time = "2026-09-15T06:38:03.623Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/b9/10a8bb13a0462e0bc4e02c0b7b8237dc24ab590f5bb4be94bec5e8f1d532/altair-6.3.0-py3-none-any.whl", hash = "sha256:7defb6ca730676dfc99a299768e2769f51585fcb3dc960ea71aacc368929d65e", size = 797192, upload-time = "2026-09-15T06:38:01.791Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
+]
+
+[[package]]
+name = "appnope"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/f7/a82489c2b6ebe32d3e2831895ae19c77861f0eadf3bb16034484d965dbb2/appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49", size = 5454, upload-time = "2026-08-20T21:36:13.748Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/c7/6b687cb0f83d2a51017d47953f2ee430ffad6fec2a8ebc964e0718a33eb1/appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e", size = 4158, upload-time = "2026-08-20T21:36:12.444Z" },
+]
+
+[[package]]
+name = "argon2-cffi"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "argon2-cffi-bindings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" },
+]
+
+[[package]]
+name = "argon2-cffi-bindings"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/d2/0ae991f1b2181e5be49007c574710a800ad36c2978683addb3e67c474e55/argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", size = 25521, upload-time = "2026-08-20T07:32:43.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177, upload-time = "2026-08-20T07:32:44.158Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597, upload-time = "2026-08-20T07:32:45.172Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403, upload-time = "2026-08-20T07:32:46.13Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/81/b4a20d4902af7f796390bf9245ff83c5217dfa7367efa1d14986956c482b/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", size = 27132, upload-time = "2026-08-20T07:32:47.13Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/1b/c8de358af07b1c490e0fcb863ef98e46ddb486e45567aca5a60bd68d9daa/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", size = 27588, upload-time = "2026-08-20T07:32:48.087Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2f/7ee62a6e79f9309f9d9982d301b22a00010adb580c05c8109b94d7b33de0/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4", size = 26785, upload-time = "2026-08-20T07:32:48.977Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/10/960d0ee93d4897741bcaf4799c697dae2d81499f66fd1ed042a7dd54c1f4/argon2_cffi_bindings-26.1.0-cp310-abi3-win32.whl", hash = "sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb", size = 23898, upload-time = "2026-08-20T07:32:50.114Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/3a/0cc14a05810e6add9bce5e87693334baa2222de5f647fa31781885b6573f/argon2_cffi_bindings-26.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e", size = 25730, upload-time = "2026-08-20T07:32:51.091Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/db/d83cf2af140547f0b9cdaece05b2dc2dcbf991be4667331d073eff771435/argon2_cffi_bindings-26.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", size = 24478, upload-time = "2026-08-20T07:32:52.111Z" },
+ { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449, upload-time = "2026-08-20T07:32:54.075Z" },
+ { url = "https://files.pythonhosted.org/packages/91/0a/c25af768f6b75a5a71e31207f87c540656b2808c015260444a22763221ad/argon2_cffi_bindings-26.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", size = 25683, upload-time = "2026-08-20T07:32:55.05Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311, upload-time = "2026-08-20T07:32:56.166Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771, upload-time = "2026-08-20T07:32:57.206Z" },
+ { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568, upload-time = "2026-08-20T07:32:58.361Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/31/3a18e31406d8694b4d6a31573c3e572fff6bed318bb744453eb653766d22/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", size = 27280, upload-time = "2026-08-20T07:32:59.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/39/d4be4577e178b2397aa5b5575c8a309bf0da2afe05fe0c72c8f398662d63/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", size = 27776, upload-time = "2026-08-20T07:33:00.325Z" },
+ { url = "https://files.pythonhosted.org/packages/71/47/78f4dd96f7411339f723b96fe24039c1bd5835102b8a5ba71ac4ec712ac7/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", size = 26932, upload-time = "2026-08-20T07:33:01.272Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/cd/96bfd37434cc0a848a9066c291d84b28846c4c9ea289ed9866b1164d622b/argon2_cffi_bindings-26.1.0-cp314-cp314t-win32.whl", hash = "sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35", size = 24878, upload-time = "2026-08-20T07:33:02.189Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/42/d8b6810abd9b1bd2f47ebbccf460da59c9f32e94888bea4f7b137d998797/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8", size = 26656, upload-time = "2026-08-20T07:33:03.222Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/d1/095d95eaf2ed1d9f77268cf3291bde148c6cd56121f8db2c74c1ba618a0e/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1", size = 25378, upload-time = "2026-08-20T07:33:04.332Z" },
+ { url = "https://files.pythonhosted.org/packages/66/cb/214092c39c4dbcb72cf98b12234ddac2221f8fe2c0acf29c6a70fa83be53/argon2_cffi_bindings-26.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb", size = 25683, upload-time = "2026-08-20T07:33:05.337Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e5/02015b83e9b05ccb85ff2ced424cf6e83a12d3810bc7f66d679a92b69ffb/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", size = 27310, upload-time = "2026-08-20T07:33:06.344Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/4a/85e612787d0796878b3b4f6bd53dcd5484b6fe7b64cc6fc7b6e6a04cf835/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", size = 26771, upload-time = "2026-08-20T07:33:07.429Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/84/ccb003b6f9969820e87656398f4d49c857def71a85ca1588a0e809afd7ce/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", size = 27569, upload-time = "2026-08-20T07:33:08.598Z" },
+ { url = "https://files.pythonhosted.org/packages/88/07/c26b76debf0998ee08fbe947ab2058ac5de37d4b9d46b06c17abaa6c4ce9/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", size = 27279, upload-time = "2026-08-20T07:33:09.518Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/0d/ead6ddc029f91bc9b9390686dad3c808ab08100d348f6266b5f93f8970ee/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", size = 27774, upload-time = "2026-08-20T07:33:10.728Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/47/c108530d9eb86036b78d3af4de28b83b4a2d9a70512bd10ff8e59966aab4/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", size = 26933, upload-time = "2026-08-20T07:33:11.661Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/02/0bfc59e781c89acf64c31c388aade9d9d1c1ea38aa1ba1292fe07f607fe9/argon2_cffi_bindings-26.1.0-cp315-cp315t-win32.whl", hash = "sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210", size = 24875, upload-time = "2026-08-20T07:33:12.616Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c7/c3e46068cddffccecb8ad94d71135e9bf62bbc789589e7dfadc7c6f59214/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4", size = 26655, upload-time = "2026-08-20T07:33:13.521Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/ca/18b9c8c45fecf34b9100ec6d7946057f14a158f2eaa20ea123a3e82351cb/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440", size = 25376, upload-time = "2026-08-20T07:33:14.491Z" },
+]
+
+[[package]]
+name = "arrow"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" },
+]
+
+[[package]]
+name = "ast-serialize"
+version = "0.13.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/d5/5dc1ee8a80c3b20a0b63c61b43ba6bc53517faecd1def47bcd752d22268e/ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025", size = 955137, upload-time = "2026-10-17T00:04:17.183Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/a8/fbd136b9f70c59e75f59a8e974c84102976f1201828b4f886480ba956936/ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456", size = 897306, upload-time = "2026-10-17T00:02:52.479Z" },
+ { url = "https://files.pythonhosted.org/packages/50/3f/2ad51e9823a790e7a70fd781e9595ab3abeace27f3aefcaa1939457efd72/ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4", size = 1236082, upload-time = "2026-10-17T00:02:54.82Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/5c/8e959db5cc2237f9d4c560a8e2253c98f9746883fb4774651e4cda458901/ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad", size = 1221242, upload-time = "2026-10-17T00:02:56.395Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/05/ab1c56c3b7adcac88764bba609502d6b4989e449b2dd60eb478e1d20e369/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9", size = 1282105, upload-time = "2026-10-17T00:02:58.275Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f4/41981fcd2ddffe0760e5326be9dcbd2eda9478a8430a637e5123bd8fcb26/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331", size = 1287905, upload-time = "2026-10-17T00:03:00.068Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/43/b783addc5681153707121991b7b47892f7e9c8ba58a0d03b71a3b023ba47/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51", size = 1541806, upload-time = "2026-10-17T00:03:01.503Z" },
+ { url = "https://files.pythonhosted.org/packages/47/10/969db095038ee15270f65ad20d1129c5e4da605bf7e93b70ae640f1c8a9f/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372", size = 1309946, upload-time = "2026-10-17T00:03:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/06/dd/7c623d2fb0b868948952d1787e629a43b866f7d4caefd5445e20aefacec4/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd", size = 1302773, upload-time = "2026-10-17T00:03:04.378Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/c2/4d49469110a19a44fc6269f4fd59679bcf84df068f185f89b73bee4da19b/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82", size = 1299579, upload-time = "2026-10-17T00:03:05.88Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/9a/2d381f1f262dda15affc8ad643fba1619eb07e7d9f8bd904deffef183006/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769", size = 1355969, upload-time = "2026-10-17T00:03:07.321Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/9c/2266794e6c25fa9ce51ce04a9bf4b7e330e3ac9351ba3fa14e487678acdb/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8", size = 1460227, upload-time = "2026-10-17T00:03:08.733Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/d6/14deb28e5d8f328f626a0778ea2cf98fe908f2bbcccd804cb6f449ee1782/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96", size = 1562691, upload-time = "2026-10-17T00:03:10.25Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/19/7a28c0e5ba54f0d3e054f847a2a549b748cb179ab433321a8dc1b39e398d/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c", size = 1558669, upload-time = "2026-10-17T00:03:11.799Z" },
+ { url = "https://files.pythonhosted.org/packages/18/9a/f9e2690eab58217f1d395a964a78efa7eaf813a1e38be8dfd9a8d45d0840/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98", size = 1665071, upload-time = "2026-10-17T00:03:13.284Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/f3/ef5b159a0d7161b404f05e7ec21cf7735ee7edc10c000cd2cbbc7bb6b69a/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe", size = 1473364, upload-time = "2026-10-17T00:03:14.563Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/87/6e8ac4a865124a12cc4f915bf1bcfac3bba426cd5f58d3712a873ee61012/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa", size = 1500566, upload-time = "2026-10-17T00:03:15.917Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/1e/29155f1621433008013ad5c940a03d41a676be0680c41527040472d37583/ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47", size = 1128674, upload-time = "2026-10-17T00:03:17.481Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/64/2506da65143ea138de5822e9173e0b72320a2f70e23bd723f044033175f1/ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593", size = 1163306, upload-time = "2026-10-17T00:03:19.046Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/8f/c7cb22638899b0cf15908809c270560e46395e65604bc59e4b623ab9f405/ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6", size = 1133775, upload-time = "2026-10-17T00:03:20.377Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/e2/b0c140fe50fa39dd249e382f7733af6f816c90021fe7959a0b026f86237f/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5", size = 1236160, upload-time = "2026-10-17T00:03:21.945Z" },
+ { url = "https://files.pythonhosted.org/packages/23/c4/a8a2ca21e3943555b48221f8fba546877a7ad39efaf6505506524967d7c4/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8", size = 1222214, upload-time = "2026-10-17T00:03:23.462Z" },
+ { url = "https://files.pythonhosted.org/packages/15/19/2f58f2ba64e4799b39de85c140aef09a03aa6ea53be75dd292a9d9b9d8ae/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede", size = 1282979, upload-time = "2026-10-17T00:03:24.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/af/ca6087fe84210c1ccd43e3182917bb70649fa1e69745a90c1d59ba08fea8/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5", size = 1288596, upload-time = "2026-10-17T00:03:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/2c/4ef964c763329a0347125e745ec0451ba4fbaceec9efe2ac729b7cd55f17/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49", size = 1545540, upload-time = "2026-10-17T00:03:27.52Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/85/aa961dbb75813cfa94de0ce9a16a9510c226b95f604c405421e727f44bb0/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a", size = 1309374, upload-time = "2026-10-17T00:03:29.091Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/e6/f2b67c327d9f9533e41791804403ccc2dc457821195a2e51f0b587a90b90/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605", size = 1303466, upload-time = "2026-10-17T00:03:30.451Z" },
+ { url = "https://files.pythonhosted.org/packages/73/1d/33f18b81d1bb1f5d682943ea92fda8130a6593627f12f6201668d40824fd/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c", size = 1300576, upload-time = "2026-10-17T00:03:32.102Z" },
+ { url = "https://files.pythonhosted.org/packages/25/cb/64914144925f1a4abdd9217e717b122255c38fc5ebe4342f92052f3bacbc/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a", size = 1355960, upload-time = "2026-10-17T00:03:33.676Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/80/bd29379fab2bce19adc0bf9177002b11b23dfe8d8fecf409998b458b4624/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8", size = 1459779, upload-time = "2026-10-17T00:03:35.09Z" },
+ { url = "https://files.pythonhosted.org/packages/83/89/012dbe733de05ffdf8e40ca0f9fa5d417cb16ec93a74463c57c001914198/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611", size = 1563399, upload-time = "2026-10-17T00:03:37.285Z" },
+ { url = "https://files.pythonhosted.org/packages/69/37/ee4bcb0358bee38744eb0f18560446572d095fe3bf6b63d6f739d08a839c/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7", size = 1558808, upload-time = "2026-10-17T00:03:38.711Z" },
+ { url = "https://files.pythonhosted.org/packages/54/1c/ff44c5355251d61ef22934c4430bb31a61de4781d25a992154aac9b8c82a/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd", size = 1666324, upload-time = "2026-10-17T00:03:40.376Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/bb/6da4bfc8aa3403e6447607131bf3463acaf954d990c6c8b6787c1902ddfe/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0", size = 1474089, upload-time = "2026-10-17T00:03:41.715Z" },
+ { url = "https://files.pythonhosted.org/packages/21/97/897e33bef2eefd880e6350e375cc66bc5bcd00a139be480fd655f5b3f095/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd", size = 1501542, upload-time = "2026-10-17T00:03:43.08Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/4c/821211ac83870c27e4412381b7c7838336456b40575bf840e4680b2018d9/ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4", size = 1126118, upload-time = "2026-10-17T00:03:44.616Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/2b2d5e6c4fa352874a80618e742ffd32d249dee6c119f4233b51afc6b8f4/ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8", size = 1163766, upload-time = "2026-10-17T00:03:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a3/d610cfcdabaa44fd62a54ca1fc57a7dba120643798b089a14914973562de/ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3", size = 1134043, upload-time = "2026-10-17T00:03:48.453Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/89/0e30f9eddf7d44ccf9d25979030754fd0457af61fa4279acb8c293a04f97/ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5", size = 897546, upload-time = "2026-10-17T00:03:50.093Z" },
+ { url = "https://files.pythonhosted.org/packages/81/92/d705be88ff452b9ba0088b554a0cbd308fd01879da092b33fd2ad7d1aa60/ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489", size = 1242416, upload-time = "2026-10-17T00:03:51.521Z" },
+ { url = "https://files.pythonhosted.org/packages/60/c2/98370845a7f35cb8a25782e78284b48e01bf3128339881d48d613f304f8f/ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a", size = 1229729, upload-time = "2026-10-17T00:03:52.825Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/8b/2d7ffed76addd9d3fcf37fb171400942735c8912264c763561a85407f6a8/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8", size = 1292255, upload-time = "2026-10-17T00:03:54.148Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/2a/bb9e0e719d89cc950fadfcfcc9cb4bc7640419c4228c852f031ab8abf440/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011", size = 1298501, upload-time = "2026-10-17T00:03:55.559Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/32/13ce07dce48226f5bd9cfd8f708ec101a8be05aa430cda4918ce31ce8c82/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117", size = 1553758, upload-time = "2026-10-17T00:03:56.952Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/fb/9b51d186db0c20ca28cbaaefd50f03b573c89327b9a2627820cbce22196b/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef", size = 1318258, upload-time = "2026-10-17T00:03:58.418Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fa/da311559dd3dae9af5825cf21660775a929b970f72e3786af3f2cd667944/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226", size = 1313260, upload-time = "2026-10-17T00:04:00.194Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/11/97a4b9e5b4776d72f47fcd9a09a497bbf68ec8c1ae074307a2c51e410d85/ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e", size = 1309992, upload-time = "2026-10-17T00:04:01.575Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/c4/62f85121a80cd451ef04f550cf0f7ea0ad9ff46cc4e9a5b9ae439b6527b1/ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6", size = 1362715, upload-time = "2026-10-17T00:04:02.881Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/c5f4a545f72a7681e3295bab6e3600ee0862ec4391bed961fe9f735b67e0/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d", size = 1469931, upload-time = "2026-10-17T00:04:04.181Z" },
+ { url = "https://files.pythonhosted.org/packages/38/a9/65aed4f8fa7a001a7cfdc03e68e844517ab041ba6fb614b7d78a1d6ac056/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568", size = 1570945, upload-time = "2026-10-17T00:04:05.778Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/8c/44b2da991cede79bfbf52089f33a5f0168945466b3d9cd38b5dcf08e7130/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b", size = 1569978, upload-time = "2026-10-17T00:04:07.347Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e4/109b2a5beca700237767d5add920c2f8e1ba9bd5799b0faf143d0e9cf4ef/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6", size = 1676082, upload-time = "2026-10-17T00:04:08.7Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e0/7df70f6dbb11b5762da5416318f08316a1c51348e0d69ec79ca5f98f836a/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037", size = 1483207, upload-time = "2026-10-17T00:04:10.162Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/bc/523aa1c08bd69b5654621beb3dc1a9035715a4a63762fb8d219d6fb3eb81/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6", size = 1512140, upload-time = "2026-10-17T00:04:11.589Z" },
+ { url = "https://files.pythonhosted.org/packages/66/83/6607060659998f00afdea0d8375c1e6e2b7d515ab905090d9a5a02f48851/ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a", size = 1131889, upload-time = "2026-10-17T00:04:12.953Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/6f/bafd6490fe7b355627fabe95f4417e3d02da93102c8552550eb9a0774e1e/ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7", size = 1171153, upload-time = "2026-10-17T00:04:14.362Z" },
+ { url = "https://files.pythonhosted.org/packages/11/25/4235f4b375a67a37ac595bf478a5407a744eaf7b64113c1c3a5432b8d954/ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43", size = 1139035, upload-time = "2026-10-17T00:04:15.767Z" },
+]
+
+[[package]]
+name = "asttokens"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" },
+]
+
+[[package]]
+name = "async-lru"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8f/43/9d22679e8dd0ed965182cca990d32c68b150730cce8046ad61b3179b5b7a/async_lru-2.4.0.tar.gz", hash = "sha256:b0b0077d9b7596615b3c12d0aad4f6aeccaaa0ac75ceae472608ede89e764c08", size = 22927, upload-time = "2026-10-12T18:12:54.276Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/0b/6a817103fb78faca4970c686a61065047da7226aba0d7cae6d4299b20c59/async_lru-2.4.0-py3-none-any.whl", hash = "sha256:1b46a96419ef91fe11431d58db98d792c9b4250001427191a40faf1031934212", size = 10045, upload-time = "2026-10-12T18:12:53.091Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "babel"
+version = "2.18.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
+]
+
+[[package]]
+name = "backrefs"
+version = "8.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/dd/fa26313573cd0880799ce627f45b68d1a9d4e98ddd7987b1ee6d06c1db55/backrefs-8.2.tar.gz", hash = "sha256:3d464a90603d682006ad6fc4694bee8eeeea1506805e3783b3e6f4462cfe9d84", size = 7003472, upload-time = "2026-11-03T14:25:57.73Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/42/88bef2b432b7677e117ee8bc38966d59e3ff22a522cced27b0c235961c3b/backrefs-8.2-py310-none-any.whl", hash = "sha256:51d925937bfd5a019312dc6d242a8f3f6abdf2cd7aab53eb6304d5490ca13022", size = 379939, upload-time = "2026-11-03T14:25:49.152Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/0f/3b134134749cac841fc5da988c821a4b7a60e1d6b169c2abff5531bef8db/backrefs-8.2-py311-none-any.whl", hash = "sha256:a49a2b014bb24aa256e9486b0b2e59909452bd1106ee1343032349f3fb847385", size = 391924, upload-time = "2026-11-03T14:25:50.814Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/7e/77f7fca3992f14754b44c39effa5b3e0b9a269462c754f2ce3bc8c9b717c/backrefs-8.2-py312-none-any.whl", hash = "sha256:e64a50f31e8eee2837009ca7744999ba59125acfd50996ba2349d8f18061e608", size = 397740, upload-time = "2026-11-03T14:25:52.055Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6b/fe1b6162d8293bd2fa3ec92f0b16e4926db33baea7b20a47bc972d750c59/backrefs-8.2-py313-none-any.whl", hash = "sha256:c229c7dbf5140f369afd87a61549122c0be631df34171920006f289dcd082715", size = 399642, upload-time = "2026-11-03T14:25:53.395Z" },
+ { url = "https://files.pythonhosted.org/packages/43/2a/017b27795de641b549d6d7eec6e5755d8151568c2d728e2cc27e199347af/backrefs-8.2-py314-none-any.whl", hash = "sha256:4374f7a590d0cadf646a5bbdd71c2eebc1a093564d3c0b105aa8958fc1a46413", size = 411524, upload-time = "2026-11-03T14:25:54.81Z" },
+ { url = "https://files.pythonhosted.org/packages/62/b9/dcfb422e33138ca8b2af8c54f8fc4d507e6dd466a2cf8d771d08d7dcf1fb/backrefs-8.2-py315-none-any.whl", hash = "sha256:50362d9133634aff2c5225f43e6786ad056d47259cdb2d625126cb7c73ef7a45", size = 417004, upload-time = "2026-11-03T14:25:56.268Z" },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
+]
+
+[[package]]
+name = "bleach"
+version = "6.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "webencodings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" },
+]
+
+[package.optional-dependencies]
+css = [
+ { name = "tinycss2" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.10.23"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
+ { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
+ { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
+ { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
+ { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
+ { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
+ { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" },
+ { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" },
+ { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" },
+ { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" },
+ { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" },
+ { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" },
+ { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" },
+ { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" },
+ { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" },
+ { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" },
+ { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" },
+ { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" },
+ { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" },
+ { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" },
+ { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" },
+ { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" },
+ { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" },
+ { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" },
+ { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" },
+ { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" },
+ { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" },
+ { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" },
+ { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" },
+ { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" },
+ { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" },
+ { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" },
+ { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" },
+ { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" },
+ { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" },
+ { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" },
+ { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" },
+ { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/37/edbf1b0707709013c0cf2e8317e03d97ed29bf4cac709fe328904ae92a38/click-8.5.1.tar.gz", hash = "sha256:9bbfada76703fd1d784b83eea012e3e8c6a001a876543ef104fa56aaaeb072b4", size = 399151, upload-time = "2026-11-06T17:37:01.021Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/7d/164b7ffc8624fe9efb4221bf9af485d5c1e41b9ae118045d2b6dc5457d8b/click-8.5.1-py3-none-any.whl", hash = "sha256:8bb5376d2006950386f8c6427613334a2acadc5f559cb80e638d00b4df957e37", size = 126163, upload-time = "2026-11-06T17:36:59.287Z" },
+]
[[package]]
name = "colorama"
@@ -11,6 +447,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
+[[package]]
+name = "comm"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
+]
+
[[package]]
name = "contourpy"
version = "1.3.3"
@@ -53,13 +498,86 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
+[[package]]
+name = "debugpy"
+version = "1.8.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/9d/3cb6693342acf96802dba89934a5b8da43c201d764ebd1f2c0514a3d42bd/debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e", size = 1710877, upload-time = "2026-09-15T21:44:02.578Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/95/d97240bb68b86db8d0a07602be068aae9e50f17063d696a4c6f619975311/debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d", size = 2493148, upload-time = "2026-09-15T21:44:32.518Z" },
+ { url = "https://files.pythonhosted.org/packages/70/9e/994fe1fb23dd4a5a8d059a0dc95da4b0821907200a61c2d5a4fdc21b6cf8/debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9", size = 3961025, upload-time = "2026-09-15T21:44:34.387Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e0/66319918883cf6dd6dea3805210c3cafd9e65dfaa6f8b852482cff10a026/debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294", size = 5359408, upload-time = "2026-09-15T21:44:36.128Z" },
+ { url = "https://files.pythonhosted.org/packages/94/53/5b4d91f7fffb9393ac907133201d1da0c070d57e463ee8e2ec1db3cb63b6/debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e", size = 5397429, upload-time = "2026-09-15T21:44:38.429Z" },
+ { url = "https://files.pythonhosted.org/packages/24/71/580df4aed96c9b23c9cbb049907d8ac1347050afdb8f450df20649d51578/debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07", size = 2493380, upload-time = "2026-09-15T21:44:40.074Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/10/a323945b0966e560c40fe48470d2847bfc7692a8f550d7086e9a28f5b623/debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5", size = 3975456, upload-time = "2026-09-15T21:44:41.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/f9/4383635281999c3eaa44bf327de523ba42240b35eb87c1130adac4a829c6/debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0", size = 5359469, upload-time = "2026-09-15T21:44:43.234Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/14/4fe9f46a9c825c8cbc6dc4aebc2a885b0bceb37ce2a37699c170482e7a40/debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a", size = 5397869, upload-time = "2026-09-15T21:44:44.783Z" },
+ { url = "https://files.pythonhosted.org/packages/56/3d/c7dc9f35bc9e22cdb53679f844bee40fd5cff871862f60595869a433400d/debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947", size = 5374857, upload-time = "2026-09-15T21:44:54.454Z" },
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" },
+]
+
+[[package]]
+name = "et-xmlfile"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
+]
+
+[[package]]
+name = "executing"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/92/8a611141e42930c0f194f7504063ab87dfe970cf1a5af28e7703df448734/executing-2.3.0.tar.gz", hash = "sha256:15919cb5d667e5cb4e099511971d00d659573fff2dd5c4e6cd8b71636c7858d2", size = 1338003, upload-time = "2026-10-10T14:06:04.64Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/dd/8bc67e7d5ffc1d88aa5af511189dfb76dc4e1e5b808fab186146ef2663e5/executing-2.3.0-py3-none-any.whl", hash = "sha256:736e859c9f8701f11fcf516856f26f562e04776387824b43a35a1dfe21c84122", size = 29393, upload-time = "2026-10-10T14:06:02.777Z" },
+]
+
+[[package]]
+name = "fastjsonschema"
+version = "2.22.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/80/cc3c531dd70c8ffa1045aecf33c97c5581cc8fd0e824b3ecb2c1881550c1/fastjsonschema-2.22.3.tar.gz", hash = "sha256:8330c91b027dcf941efcb7b1f0998515ff95d177ea15bbabdfe8671965620901", size = 388854, upload-time = "2026-11-01T21:11:34.389Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/0d/6caab33488dc9d26440c24221fa610c7c8b83c30715ab45d31f0e004308e/fastjsonschema-2.22.3-py3-none-any.whl", hash = "sha256:f670b443830b75aca3f00b75949efa20f4b2a80302ec7a21505c28dd54cffd43", size = 28442, upload-time = "2026-11-01T21:11:29.306Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "4.2.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/1d/d730c664fcb4f1d264a722722a3134cf0881854f73c1fd58abf87572f232/filelock-4.2.9.tar.gz", hash = "sha256:96530b7c538d4ea08493ab067247aab7948c54d8cd622cce711c97cccc5d9896", size = 663116, upload-time = "2026-11-04T18:37:10.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/05/76184fe08339723d643ab47887a8e4de8f2921ca4e008617ab7490af7857/filelock-4.2.9-py3-none-any.whl", hash = "sha256:a379c50a729e5b68c8519a4d26489f755d9769960a236ecd838190e2a78db2e9", size = 164734, upload-time = "2026-11-04T18:37:08.273Z" },
+]
+
[[package]]
name = "first-principles-code"
version = "0.1.0"
-source = { virtual = "." }
+source = { editable = "." }
dependencies = [
{ name = "matplotlib" },
{ name = "numpy" },
+ { name = "openpyxl" },
+ { name = "pandas" },
{ name = "pint" },
{ name = "pydicom" },
{ name = "scipy" },
@@ -67,14 +585,32 @@ dependencies = [
]
[package.dev-dependencies]
+apps = [
+ { name = "streamlit" },
+]
dev = [
+ { name = "mypy" },
+ { name = "pandas-stubs" },
+ { name = "pre-commit" },
{ name = "pytest" },
+ { name = "ruff" },
+]
+docs = [
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extra = ["python"] },
+]
+notebooks = [
+ { name = "ipykernel" },
+ { name = "jupyter" },
+ { name = "nbclient" },
]
[package.metadata]
requires-dist = [
{ name = "matplotlib", specifier = ">=3.10.9" },
{ name = "numpy", specifier = ">=2.4.6" },
+ { name = "openpyxl", specifier = ">=3.1.5" },
+ { name = "pandas", specifier = ">=3.1.1" },
{ name = "pint", specifier = ">=0.25.3" },
{ name = "pydicom", specifier = ">=3.0.2" },
{ name = "scipy", specifier = ">=1.17.1" },
@@ -82,7 +618,23 @@ requires-dist = [
]
[package.metadata.requires-dev]
-dev = [{ name = "pytest", specifier = ">=9.0.3" }]
+apps = [{ name = "streamlit", specifier = ">=1.67.0" }]
+dev = [
+ { name = "mypy", specifier = ">=2.4.1" },
+ { name = "pandas-stubs", specifier = ">=3.1.0.261029" },
+ { name = "pre-commit", specifier = ">=4.7.0" },
+ { name = "pytest", specifier = ">=9.0.3" },
+ { name = "ruff", specifier = ">=0.17.4" },
+]
+docs = [
+ { name = "mkdocs-material", specifier = ">=9.8.0" },
+ { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.8" },
+]
+notebooks = [
+ { name = "ipykernel", specifier = ">=7.4.0" },
+ { name = "jupyter", specifier = ">=1.1.1" },
+ { name = "nbclient", specifier = ">=0.11.0" },
+]
[[package]]
name = "flexcache"
@@ -134,97 +686,1046 @@ wheels = [
]
[[package]]
-name = "iniconfig"
-version = "2.3.0"
+name = "fqdn"
+version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/01/248d44fbda9e78fa1a7b5a475194bef4c4a3e285db286419f3bb42f2efdf/fqdn-1.6.0.tar.gz", hash = "sha256:e39bf62e2a9481aa2cb780554fdfd944dd8ee5f230eb831684d539cbafed8de2", size = 130737, upload-time = "2026-09-28T05:43:35.482Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d5/a8f9149b97cced9e1f4bff92ff0b6206120e2c853ae774d7020d6ecd8fa8/fqdn-1.6.0-py3-none-any.whl", hash = "sha256:6c793c312ccfa29981e1135b3ef5e1277ebe639731a6b0f7350d90e96e63f0b4", size = 12569, upload-time = "2026-09-28T05:43:33.951Z" },
]
[[package]]
-name = "kiwisolver"
-version = "1.5.0"
+name = "ghp-import"
+version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+dependencies = [
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
- { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
- { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
- { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
- { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
- { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
- { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
- { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
- { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
- { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
- { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
- { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
- { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
- { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
- { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
- { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
- { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
- { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
- { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
- { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
- { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
- { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
- { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
- { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
- { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
- { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
- { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
]
[[package]]
-name = "matplotlib"
-version = "3.10.9"
+name = "griffelib"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/e2/0f5d8ed08766bc6dcd3eebf5a6623d83cbca33a6e4c90aef57156ce745ad/griffelib-2.4.1.tar.gz", hash = "sha256:40c028dd2e623f464423f0e11e3c553158f1cf4f1368239a370534d73e1407ff", size = 245153, upload-time = "2026-10-22T11:20:03.705Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/02/1156d5f5307f573b901b14373848b84e69ca6cab09169b8cc97d16a181f9/griffelib-2.4.1-py3-none-any.whl", hash = "sha256:76bc562f936ad1ce198d2cf6d0731bd5c92a7d934b69ef828ef22ee0ffdb62ce", size = 172983, upload-time = "2026-10-22T11:19:59.843Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "contourpy" },
- { name = "cycler" },
- { name = "fonttools" },
- { name = "kiwisolver" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "pillow" },
- { name = "pyparsing" },
- { name = "python-dateutil" },
+ { name = "certifi" },
+ { name = "h11" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" },
- { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" },
- { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" },
- { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" },
- { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" },
- { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" },
- { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" },
- { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" },
- { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" },
- { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" },
- { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" },
- { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" },
- { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" },
- { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" },
+ { 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 = "numpy"
-version = "2.4.6"
+name = "httpx"
+version = "0.28.1"
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" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
- { 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/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "ipykernel"
+version = "7.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "appnope", marker = "sys_platform == 'darwin'" },
+ { name = "comm" },
+ { name = "debugpy" },
+ { name = "ipython" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "matplotlib-inline" },
+ { name = "nest-asyncio2" },
+ { name = "packaging" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7a/05/e5499c8b762387d83926bf0389e8fff888c9bf6e7af30566a8527bc12f4a/ipykernel-7.4.0.tar.gz", hash = "sha256:4330114d22b9b33575b2c7c68753fc8faabbbcfbc60b0cdff9f14dd4ce63742f", size = 192731, upload-time = "2026-09-29T16:33:11.468Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/f8/8b5c10c7f36ab5a3b5c4703669e5b14e44e98782b58e535dd30670e8799a/ipykernel-7.4.0-py3-none-any.whl", hash = "sha256:a6757f790ddc5a6006b813d82da1a9dbb65b3a00f2b6607df776418c6566ec1c", size = 123184, upload-time = "2026-09-29T16:33:09.612Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.18.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "ipython-pygments-lexers" },
+ { name = "jedi" },
+ { name = "matplotlib-inline" },
+ { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit" },
+ { name = "psutil", marker = "sys_platform != 'android' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" },
+ { name = "pygments" },
+ { name = "stack-data" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/04/0d945c72d5b77527273ecb4c66aa2436de7a3b3bbe3da345bf053232b886/ipython-9.18.1.tar.gz", hash = "sha256:6319c0f5372c196397899479e6db1783dd7937abae06815d1317e8d5d51aaaaf", size = 4437314, upload-time = "2026-11-02T09:19:55.55Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/1c/91da4a1c541b75e97eb99d09c6a9b0534de55bb4a466669d1170e39b915c/ipython-9.18.1-py3-none-any.whl", hash = "sha256:997ce9140d374531a61069c70c0f9ef299d6dd3559ea0f94b8af589726e3c1d5", size = 649819, upload-time = "2026-11-02T09:19:53.32Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "ipywidgets"
+version = "8.1.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "comm" },
+ { name = "ipython" },
+ { name = "jupyterlab-widgets" },
+ { name = "traitlets" },
+ { name = "widgetsnbextension" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/7c/6db60eddf38547353b06d57941f5eee22a990640ce30479fd71a810507f2/ipywidgets-8.1.9.tar.gz", hash = "sha256:bcccba38a6ec3253f7a39c943cea5b9ad01999ce071396171adbc51c6a6a8613", size = 117252, upload-time = "2026-08-18T08:54:24.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl", hash = "sha256:f2b8cbcaae10252b809fbe4d7470db75c09b769a32cbf816d20e5ca6d3c5a79d", size = 140101, upload-time = "2026-08-18T08:54:22.339Z" },
+]
+
+[[package]]
+name = "isoduration"
+version = "20.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "arrow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.20.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/a4/4692ecd7fa700ac1066b1c1f6cadef60aa2e01e5e364710235137d823e0a/jedi-0.20.1.tar.gz", hash = "sha256:2f71208c3f9c1bca057c0e90d3f272aba44ace88fc4067d7587e9e069331b7e5", size = 3121738, upload-time = "2026-10-12T17:42:24.361Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/c7/71eb0d51cdcbb63b857c931aa4e9218b078b5d9f8ecd383b234f28fe7b0f/jedi-0.20.1-py2.py3-none-any.whl", hash = "sha256:0fb16d86c4a4c73c37ba518c77419975e30fcc620658a8d14fbb5720cdd34142", size = 4885980, upload-time = "2026-10-12T17:42:21.585Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "json5"
+version = "0.17.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/1f/58a4d7ee466f81bc3c7337036380fc6d49de3afb7855d74f8121b1e89b32/json5-0.17.3.tar.gz", hash = "sha256:8d0278ad34ebaa9c3af76d9519274811830224a2d1cd5af156dbd067f461b8a4", size = 54234, upload-time = "2026-10-09T21:19:28.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/39/e689b616c4279a821d5842a8a85c1874540f8d58e35028df93f2182120c2/json5-0.17.3-py3-none-any.whl", hash = "sha256:2c8b22a893c35cd6a3c5ccbf1dd1c7d02c25dc1b5897fea658f9bf08c2e05f9a", size = 34809, upload-time = "2026-10-09T21:19:27.357Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/93/f6/d1109cbb2c7cb692b432071802028052ba8d275c613c1a1cce427bc62c97/jsonpointer-3.10.tar.gz", hash = "sha256:69349c0e738c93e9efa501397091f49ad302ae686293effd05d7755c06f88ef0", size = 19950, upload-time = "2026-11-06T11:39:07.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/66/25cbb926e981a849d1cfcd36817fba6bfa5e9b6a2944c41230fe83ff25d2/jsonpointer-3.10-py3-none-any.whl", hash = "sha256:3b08b9e5239f1b683e31c9561c350d75ce3fa8cdd407130ca8491fdb0c9e56f3", size = 10376, upload-time = "2026-11-06T11:39:06.069Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[package.optional-dependencies]
+format-nongpl = [
+ { name = "fqdn" },
+ { name = "idna" },
+ { name = "isoduration" },
+ { name = "jsonpointer" },
+ { name = "rfc3339-validator" },
+ { name = "rfc3986-validator" },
+ { name = "rfc3987-syntax" },
+ { name = "uri-template" },
+ { name = "webcolors" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "jupyter"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ipykernel" },
+ { name = "ipywidgets" },
+ { name = "jupyter-console" },
+ { name = "jupyterlab" },
+ { name = "nbconvert" },
+ { name = "notebook" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" },
+]
+
+[[package]]
+name = "jupyter-builder"
+version = "1.2.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-core" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/4d/5681a9a083becca3e52d53c21bc16ee7ccdea485a4b15f4ad5c97b57e4a6/jupyter_builder-1.2.4.tar.gz", hash = "sha256:da5b46fc49faac39e977259e489519ee1a92917c31d90d1d258832d8f4d288a3", size = 1026105, upload-time = "2026-10-27T16:16:07.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/78/bfa339893d0fb06f5debb5a6e8948ecf0c0070b4e6bb65c157a8389d55d3/jupyter_builder-1.2.4-py3-none-any.whl", hash = "sha256:74fbafc8a7bedcc88415fab7da05727a7668f7dbdc069676f47790158e255f5a", size = 947863, upload-time = "2026-10-27T16:16:05.291Z" },
+]
+
+[[package]]
+name = "jupyter-client"
+version = "8.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-core" },
+ { name = "python-dateutil" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184, upload-time = "2026-08-28T12:17:09.028Z" },
+]
+
+[[package]]
+name = "jupyter-console"
+version = "6.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ipykernel" },
+ { name = "ipython" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "prompt-toolkit" },
+ { name = "pygments" },
+ { name = "pyzmq" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" },
+]
+
+[[package]]
+name = "jupyter-core"
+version = "5.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "platformdirs" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
+]
+
+[[package]]
+name = "jupyter-events"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema", extra = ["format-nongpl"] },
+ { name = "packaging" },
+ { name = "python-json-logger" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+ { name = "rfc3339-validator" },
+ { name = "rfc3986-validator" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" },
+]
+
+[[package]]
+name = "jupyter-lsp"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-server" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" },
+]
+
+[[package]]
+name = "jupyter-server"
+version = "2.21.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "argon2-cffi" },
+ { name = "jinja2" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "jupyter-events" },
+ { name = "jupyter-server-terminals" },
+ { name = "nbconvert" },
+ { name = "nbformat" },
+ { name = "packaging" },
+ { name = "prometheus-client" },
+ { name = "pywinpty", marker = "os_name == 'nt'" },
+ { name = "pyzmq" },
+ { name = "send2trash" },
+ { name = "terminado" },
+ { name = "tornado" },
+ { name = "traitlets" },
+ { name = "websocket-client" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/65/3d/3c9f8bce5d5448107bd285cd80185e76c9c871d076b580dfd90997e52904/jupyter_server-2.21.1.tar.gz", hash = "sha256:a8960aa29263f6041283e97d4756b099fb49b767baab6371891ecb1bd40a63df", size = 761651, upload-time = "2026-09-15T16:00:07.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/39/91bc08650cc8e3efeb7a83c25e20ebddcc9b70d28b4eb6f51a4fb55da7a4/jupyter_server-2.21.1-py3-none-any.whl", hash = "sha256:2a6467606af7dbae2e7e31640030025969e15db6a649eae334af90415dc71dca", size = 394641, upload-time = "2026-09-15T16:00:05.526Z" },
+]
+
+[[package]]
+name = "jupyter-server-terminals"
+version = "0.5.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pywinpty", marker = "os_name == 'nt'" },
+ { name = "terminado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" },
+]
+
+[[package]]
+name = "jupyterlab"
+version = "4.6.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "async-lru" },
+ { name = "httpx" },
+ { name = "ipykernel" },
+ { name = "jinja2" },
+ { name = "jupyter-builder" },
+ { name = "jupyter-core" },
+ { name = "jupyter-lsp" },
+ { name = "jupyter-server" },
+ { name = "jupyterlab-server" },
+ { name = "notebook-shim" },
+ { name = "packaging" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/43/490a500e421a46900c8afb0c912c1bf3c9977c85831a3815e2c2139309a9/jupyterlab-4.6.6.tar.gz", hash = "sha256:5f6fbb409158167aeefb5781d6f0b228e24b493cb6e6f1320fb5bc8897acc223", size = 28406802, upload-time = "2026-11-06T14:02:39.228Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/f0/842ff0c6ed2e9a4d027d32576813ef92c014f72743a3e745e0a9ce9df24b/jupyterlab-4.6.6-py3-none-any.whl", hash = "sha256:4261910ccf1ecad6e0b3ca0debe96d4fde9735bfc9707509c125aab850e06b81", size = 17334944, upload-time = "2026-11-06T14:02:30.924Z" },
+]
+
+[[package]]
+name = "jupyterlab-pygments"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" },
+]
+
+[[package]]
+name = "jupyterlab-server"
+version = "2.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "jinja2" },
+ { name = "json5" },
+ { name = "jsonschema" },
+ { name = "jupyter-server" },
+ { name = "packaging" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/16/c5/08946a10b81bd64408d8b78cb352e17fe52d3ee11c2aef00a0f06e506938/jupyterlab_server-2.28.1.tar.gz", hash = "sha256:0c3c2418d51021ce280916e63dfe4cba8386b4e2787be5c25433c98f560ddb31", size = 78141, upload-time = "2026-09-15T19:22:09.935Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/d3/996332fe24f3c2458b9c353b998570dfa2631763e9a8674ba3f94c0877d0/jupyterlab_server-2.28.1-py3-none-any.whl", hash = "sha256:4bd36c7c11d872e15cefa4951e12380a47e6901687165332dd50a007cb367ad9", size = 60197, upload-time = "2026-09-15T19:22:08.201Z" },
+]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/8b/e739cf9066ad5037a2d4b0a403f06da374fdccb9748221661c8b492d3dbc/jupyterlab_widgets-3.0.17.tar.gz", hash = "sha256:6e61fe21ca8a66039180a5cc52a433e07279d2fee79c8be963e00d55193f17a8", size = 213919, upload-time = "2026-08-18T08:52:17.511Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl", hash = "sha256:40ac1e9955acf116c4d995d9bfa082d86ad9ec6d91c4f134827cf5e0a5eb75e0", size = 217292, upload-time = "2026-08-18T08:52:15.47Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
+ { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
+ { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
+ { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
+]
+
+[[package]]
+name = "lark"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" },
+ { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" },
+ { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" },
+ { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" },
+ { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" },
+ { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" },
+ { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" },
+ { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" },
+ { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" },
+ { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" },
+ { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" },
+ { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" },
+ { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" },
+ { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" },
+ { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" },
+ { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" },
+ { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" },
+]
+
+[[package]]
+name = "markdown"
+version = "3.11.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/eb/dc0fd6eff604bfb870b7fb68b90985a32caaf3c76f67736bbf7f438acd59/markdown-3.11.3.tar.gz", hash = "sha256:81eb91631dc97ad1efbc76019ef0f80fe90ca49ddf81546752d79989c3bfff3e", size = 498810, upload-time = "2026-11-05T20:30:52.926Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/6f/1fa8970e960e5c832874399e6efa564870ade8206d57fef5d5e296b834f5/markdown-3.11.3-py3-none-any.whl", hash = "sha256:e1a31a112232504d753e5c01bb8c2de90d3f0ba986b063e10770cf7695b09e73", size = 116758, upload-time = "2026-11-05T20:30:51.775Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" },
+ { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" },
+ { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" },
+ { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" },
+ { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" },
+ { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" },
+ { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" },
+ { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" },
+ { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" },
+ { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" },
+ { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" },
+ { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" },
+ { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" },
+ { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" },
+ { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" },
+ { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" },
+ { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" },
+ { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" },
+ { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" },
+ { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" },
+ { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" },
+ { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" },
+ { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" },
+ { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" },
+ { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" },
+ { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" },
+ { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" },
+]
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" },
+]
+
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
+]
+
+[[package]]
+name = "mistune"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/eb/749a4e69bf7396e751ae37444abe997eff2254e695a8095b84d1c89c1181/mistune-3.4.0.tar.gz", hash = "sha256:2eaf7952af928ca8d33b779762effc1da517b62c6ad85ec8ca957e3b07b9782a", size = 136772, upload-time = "2026-10-19T10:44:28.172Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/f7/021d590d52c1ac28e3290311776854c6a1c927fa3fb4b25ce74caf937a3d/mistune-3.4.0-py3-none-any.whl", hash = "sha256:d03cec4c8620f21517a996d82d14f3b59c30d79bda2cc6c56bd32b839f18ce6b", size = 70398, upload-time = "2026-10-19T10:44:26.751Z" },
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "ghp-import" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mergedeep" },
+ { name = "mkdocs-get-deps" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pyyaml" },
+ { name = "pyyaml-env-tag" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
+]
+
+[[package]]
+name = "mkdocs-autorefs"
+version = "1.4.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e0/e0/1e8ed4e8dabdcba8cba5dbf4b8ce49bb3b51e750177155247abb5345d48a/mkdocs_autorefs-1.4.5.tar.gz", hash = "sha256:856f75b327b7717923ce06267094650689a510a748c48657f5333965950c2788", size = 59736, upload-time = "2026-10-22T15:03:55.402Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/cf/3dfc07ec910553bfb1e5c9009c1854bd9d2efa7f3dda03f007a77db0e63a/mkdocs_autorefs-1.4.5-py3-none-any.whl", hash = "sha256:68ee484e0800f3ba50cda1bc4a94293adbdfcc010243c322b3f5d66e15d06770", size = 29633, upload-time = "2026-10-22T15:03:54.091Z" },
+]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mergedeep" },
+ { name = "platformdirs" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" },
+]
+
+[[package]]
+name = "mkdocs-material"
+version = "9.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "backrefs" },
+ { name = "colorama" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "mkdocs" },
+ { name = "mkdocs-material-extensions" },
+ { name = "paginate" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/02/3c0df248819286810d8479b3a72d9531db0cc36c042fe1ab3985f137c664/mkdocs_material-9.8.0.tar.gz", hash = "sha256:1ed4c5edaf405a3fb62e55288b8fdff592971729361548b1ffa757bf9849fb81", size = 4094495, upload-time = "2026-10-24T09:50:16.181Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/16/360f44c7702f27ff4670a647a16c72432291e311371d72b4c34a57e990e0/mkdocs_material-9.8.0-py3-none-any.whl", hash = "sha256:19304a7263c5700428227122b3b2699aa73f432c9de8816876adaec7d7293ce3", size = 9302245, upload-time = "2026-10-24T09:50:12.636Z" },
+]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
+]
+
+[[package]]
+name = "mkdocstrings"
+version = "1.0.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "pymdown-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/27/cc/e3ed8b16851f5d664dffcf8f55fc60d0d3f736a3edca21d1508105d1255a/mkdocstrings-1.0.8.tar.gz", hash = "sha256:b50fc326230906425c82ee9ec59b80e5db00892b2b593d4d0b841a0d4acf537b", size = 107444, upload-time = "2026-10-19T15:43:02.157Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/5e/4df7a2290e97c60a3ba410084966e5ab84aa89d15c8c98c195acbaefa8f4/mkdocstrings-1.0.8-py3-none-any.whl", hash = "sha256:4b03ed2b863009f1153f0d04aacb22a85b92afc28a0636d85717c92eecca638c", size = 42209, upload-time = "2026-10-19T15:43:00.65Z" },
+]
+
+[package.optional-dependencies]
+python = [
+ { name = "mkdocstrings-python" },
+]
+
+[[package]]
+name = "mkdocstrings-python"
+version = "2.0.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffelib" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/43/4783d5606de4f5e4bea10388c8741b0c40d03e7cb9527b94ad35966351f5/mkdocstrings_python-2.0.10.tar.gz", hash = "sha256:5e34ae56b2f20a57807ee479d603ddc5ec7cc6d2252c4c177754627ef2d3b833", size = 207682, upload-time = "2026-10-19T15:58:07.934Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/1b/fc924a97a237adda47f15c1b0cc271639e6b181ab3f95500fbf060c79a69/mkdocstrings_python-2.0.10-py3-none-any.whl", hash = "sha256:4a3aaeebb25ba5ae57a8f9e75432fe4571984889602f2b2f7d672faa760e9062", size = 105538, upload-time = "2026-10-19T15:58:06.287Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ast-serialize" },
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/3c/78ecece71ee4b5445389a4ccd11627cfb9c69e4de554d11060171f376305/mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623", size = 4066728, upload-time = "2026-10-28T19:23:13.286Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/75/282d43d6fea7be984ac63e75ffd8eee146ba2f3b077bb1ca5bf3f973e0c4/mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456", size = 14233251, upload-time = "2026-10-28T19:22:44.977Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/a8/9021263cc7e761451c7d8131bdfab3c0a5c2d67aa2d694670019e1ab86a7/mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c", size = 14394953, upload-time = "2026-10-28T19:21:29.492Z" },
+ { url = "https://files.pythonhosted.org/packages/16/10/4192a5cea09ebbc41c534f57bd0d8f0fb42a8e810632cda6e37f072c59fd/mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453", size = 15442824, upload-time = "2026-10-28T19:22:42.068Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e6/d3b0a7474aa4f696594a47a218b6796d9c4346fc81ed288904278cddd9bd/mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a", size = 15717227, upload-time = "2026-10-28T19:22:54.026Z" },
+ { url = "https://files.pythonhosted.org/packages/48/6a/2ec530dacd4f77ca50d0fa05b854098f2d75e5d18c587d2612b44f10d68c/mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a", size = 7898637, upload-time = "2026-10-28T19:21:41.709Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/56/de9d7bad12e3868d5f1f4279aed20b1b568b2c5e166f2d3a05b211dca0b1/mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a", size = 11576656, upload-time = "2026-10-28T19:22:13.232Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/12/4fa9883b9b59e5c96ff62845ad019dc47786031c45d67eaccf452cffffd8/mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6", size = 11174403, upload-time = "2026-10-28T19:23:01.346Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/f2/827e356fc4f74c60b8da42663336e035b49d948a3c11224b0fc7543086b1/mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93", size = 15713427, upload-time = "2026-10-28T19:21:26.757Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/fa/e58a4fcae1d238189eac758036c0921992a86e3e6209190efd96d61fc5d4/mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276", size = 15938481, upload-time = "2026-10-28T19:22:34.371Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/54/a20f881801b88900923f2be1541a4b438ad0f0883e03836641fdc3d7adf4/mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373", size = 17243006, upload-time = "2026-10-28T19:22:39.425Z" },
+ { url = "https://files.pythonhosted.org/packages/de/1c/ff84a6133a6e89c390bff6b25d9bad6598e7891b205f4b30b496906d636d/mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6", size = 17557728, upload-time = "2026-10-28T19:21:43.607Z" },
+ { url = "https://files.pythonhosted.org/packages/15/4a/effa47d39b8fadf679c08e7c78559e523f9769a42a75662d2d15b2b9cdd6/mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca", size = 12370826, upload-time = "2026-10-28T19:22:29.779Z" },
+ { url = "https://files.pythonhosted.org/packages/46/9f/b214ececb1a97fbb61ab3dfd48ae3cffd92ae2c148af4c9dc94c6c56772b/mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971", size = 11851083, upload-time = "2026-10-28T19:23:11.288Z" },
+ { url = "https://files.pythonhosted.org/packages/57/5b/e44a2edb0b25a9f1429de770e1d0238af23302671dd40c77ac5efeb0c559/mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c", size = 14243144, upload-time = "2026-10-28T19:21:10.975Z" },
+ { url = "https://files.pythonhosted.org/packages/59/d4/5c99dc3a28e11d2f0fcbb02df63d2d3e574dedc0f9f69d50901df305a7a6/mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7", size = 14409316, upload-time = "2026-10-28T19:22:15.515Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c9/3108b38f4c30eabd80194f5d9771a87e27196c5e0ddb5bf4b99c6ed1e97e/mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2", size = 15445573, upload-time = "2026-10-28T19:22:05.635Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/12/84a53eceaf11c46dfd7931fb6b8902d01fb33787d3a991e81c461458150b/mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb", size = 15732989, upload-time = "2026-10-28T19:23:06.512Z" },
+ { url = "https://files.pythonhosted.org/packages/65/ba/bf017dc2abdf83b2f74e29c43834b7af67dc1f1b20202f02dae60b4781a0/mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692", size = 7898932, upload-time = "2026-10-28T19:21:59.366Z" },
+ { url = "https://files.pythonhosted.org/packages/76/05/0c22b1f0390120a8592d31209cb6b9f932b6a0b426fe643e550de15139e3/mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100", size = 11577577, upload-time = "2026-10-28T19:22:01.302Z" },
+ { url = "https://files.pythonhosted.org/packages/73/fb/ac01e173629697ddac2f11e51efd10347d673f9a7311b03f4aba710ebccd/mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704", size = 11173540, upload-time = "2026-10-28T19:21:46.444Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e4/b9b9c8eb09e70927ee65b1456cfed35710b6e4ff0b07f364ceaf2202307c/mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1", size = 15706623, upload-time = "2026-10-28T19:22:20.528Z" },
+ { url = "https://files.pythonhosted.org/packages/26/74/1ceb1ca884add0565e618d013dd24333039100dc0fcc42661561f2e1cae5/mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca", size = 15944095, upload-time = "2026-10-28T19:21:04.779Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/68/27bcf9b4efa22733b6050485ae37845b94c9b8851abe4fc60632d905c32d/mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1", size = 17223065, upload-time = "2026-10-28T19:23:03.724Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/a3/ed5724b317bf585c29f3c8be2792e97d45925d65a9c8af40ceeb267072e6/mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9", size = 17560589, upload-time = "2026-10-28T19:21:34.539Z" },
+ { url = "https://files.pythonhosted.org/packages/42/2f/005185e6ab4752fd6cc2d1a6d1421fe21979770afe2490c00c555f34a913/mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1", size = 12353324, upload-time = "2026-10-28T19:22:51.481Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/d6/20420cc9c780087139662534d128c2f3274361f8bb7e9cf0d5495a846ed5/mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f", size = 11845599, upload-time = "2026-10-28T19:22:23.136Z" },
+ { url = "https://files.pythonhosted.org/packages/21/5c/6e8010f8f2c4ad817364592d4b3883633417269b913fc9d5174fdeb7e565/mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999", size = 2801286, upload-time = "2026-10-28T19:21:19.516Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/6f/43edf3be2748d0e40974a5ee046a8bb4c9961a4bdd3e7d5c6dcbf32d1af6/mypy_extensions-1.2.0.tar.gz", hash = "sha256:c6e275a07f1c2fc096b62940452d0eaa3f3af7de45d704787866240a7c6d5a51", size = 6566, upload-time = "2026-11-03T16:26:33.692Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/99/7b1adac7e97ce7bbda34792dc6e9b2f5cc5cffd1875883f78a818b2a2687/mypy_extensions-1.2.0-py3-none-any.whl", hash = "sha256:993e7a20aa5671eb41a7132cf555af30062da99729601601b9a8382b11cae768", size = 5114, upload-time = "2026-11-03T16:26:32.731Z" },
+]
+
+[[package]]
+name = "narwhals"
+version = "2.28.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/43/e11d75a7fb635c6f63dd6e176cb3e050ec2fee35f2f2267b332a6ad351ec/narwhals-2.28.0.tar.gz", hash = "sha256:2d9860c80d6610820826e94c6e42ce4b1d92d89cdd479586e652a8d211dab345", size = 754977, upload-time = "2026-10-27T12:19:33.777Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/82/8ef9c7e0f50493307a49c09cfa7f00c5a778b6d1921f7b2dabe1d3a5809e/narwhals-2.28.0-py3-none-any.whl", hash = "sha256:1f69d6385ec30e2fa6e1fdeb37c562ecabd0f713f6f3e3b58a668c5c9df9567f", size = 490591, upload-time = "2026-10-27T12:19:31.649Z" },
+]
+
+[[package]]
+name = "nbclient"
+version = "0.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "nbformat" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" },
+]
+
+[[package]]
+name = "nbconvert"
+version = "7.17.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "bleach", extra = ["css"] },
+ { name = "defusedxml" },
+ { name = "jinja2" },
+ { name = "jupyter-core" },
+ { name = "jupyterlab-pygments" },
+ { name = "markupsafe" },
+ { name = "mistune" },
+ { name = "nbclient" },
+ { name = "nbformat" },
+ { name = "packaging" },
+ { name = "pandocfilters" },
+ { name = "pygments" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/53/6a/252c7620305a9661205aed7a2a4b3592f5207bfca56ad11b7dea2c5ec2f6/nbconvert-7.17.2.tar.gz", hash = "sha256:344a218e8595117ce37396ed025d994f146d076226ebac59255a2fa085ab24d8", size = 865989, upload-time = "2026-10-12T09:11:19.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/fd/a8d457af68848a8e5c12dd1d84ff35c415db471af78280e0703db4efbb12/nbconvert-7.17.2-py3-none-any.whl", hash = "sha256:6a143c11c00bf831604da4b1be4347d36ea52367057eb920135d69bc2862fed2", size = 262094, upload-time = "2026-10-12T09:11:17.215Z" },
+]
+
+[[package]]
+name = "nbformat"
+version = "5.11.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "fastjsonschema" },
+ { name = "jsonschema" },
+ { name = "jupyter-core" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849, upload-time = "2026-08-17T08:10:50.18Z" },
+]
+
+[[package]]
+name = "nest-asyncio2"
+version = "1.7.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/32/2c/70e3b6bb2189cfd260280b7e1042bd622949a258663f95e887fada4ac1e6/nest_asyncio2-1.7.4.tar.gz", hash = "sha256:995b1409d926ec55e0876ea39a69bffdd8852cf1db3652c177e8c8ffdbcaf3cc", size = 16891, upload-time = "2026-10-07T22:31:50.507Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/ed/559d2915dce85794bc0c36064043da170da50cd98746dcbddd85d5932017/nest_asyncio2-1.7.4-py3-none-any.whl", hash = "sha256:86e77b52c3d1496b294af3c944b44213b9b82a12a5ee590c0acc4ca4692ef6c7", size = 8543, upload-time = "2026-10-07T22:31:49.245Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/99/d27cfc9518f49e4cbc25c683ec7cf8704a9d31311341c291c0e78ddf791b/nodeenv-1.12.0.tar.gz", hash = "sha256:4ea7f88339310afebc3253f2c7a91b94bacf97d73a36a5c108962abdfaca501e", size = 131292, upload-time = "2026-10-31T19:03:18.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/6a/8a9ac7d5e2f7cf53c459bfef90ebc17a2f8cd54b665af96799d4c534840e/nodeenv-1.12.0-py3-none-any.whl", hash = "sha256:80644d65c10cc2be12e3e4b54b3367ff288c7ad24ae75c2dcc65454fc7449d4d", size = 39442, upload-time = "2026-10-31T19:03:17.427Z" },
+]
+
+[[package]]
+name = "notebook"
+version = "7.6.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-builder" },
+ { name = "jupyter-server" },
+ { name = "jupyterlab" },
+ { name = "jupyterlab-server" },
+ { name = "notebook-shim" },
+ { name = "tornado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/88/737faf4a8700326a3ee12802f79041471764699cd40b4770a6c66497738f/notebook-7.6.5.tar.gz", hash = "sha256:5e3445616fb2c590015434045f5b297566c2e09c636d1837efc62bef85892c9f", size = 5581297, upload-time = "2026-11-06T15:15:12.121Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/fe/3388224bbb7d9e68ad0325af0164d05c3ae20a8cc78d5d91c218581911d9/notebook-7.6.5-py3-none-any.whl", hash = "sha256:b27b4af6e410e7456cfffc6fb5a764dcfb7a331408907d483024d6ddb5ec05f2", size = 5629845, upload-time = "2026-11-06T15:15:09.229Z" },
+]
+
+[[package]]
+name = "notebook-shim"
+version = "0.2.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-server" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" },
+]
+
+[[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/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" },
@@ -247,118 +1748,385 @@ name = "numpy-typing-compat"
version = "20251206.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "numpy" },
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/5f/29fd5f29b0a5d96e2def96ecba3112fc330ecd16e8c97c2b332563c5e201/numpy_typing_compat-20251206.2.4.tar.gz", hash = "sha256:59882d23aaff054a2536da80564012cdce33487657be4d79c5925bb8705fcabc", size = 5011, upload-time = "2025-12-06T20:02:04.942Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/7c/5c2892e6bc0628a2ccf4e938e1e2db22794657ccb374672d66e20d73839e/numpy_typing_compat-20251206.2.4-py3-none-any.whl", hash = "sha256:a82e723bd20efaa4cf2886709d4264c144f1f2b609bda83d1545113b7e47a5b5", size = 6300, upload-time = "2025-12-06T20:01:57.578Z" },
+]
+
+[[package]]
+name = "openpyxl"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "et-xmlfile" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
+]
+
+[[package]]
+name = "optype"
+version = "0.17.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/86/e6f1f6f3487492dfcf3b7a2d4e2534d27af6ac05b364b276706906c34865/optype-0.17.1.tar.gz", hash = "sha256:07bfa32b795dea28fba8605a6288d36370d072f25183fb9c29b5a90f4b6f5638", size = 53572, upload-time = "2026-05-17T22:13:28.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/d4/c6a2b043e33f0dd012486dcebe0593585588d400175d22aad42049c88321/optype-0.17.1-py3-none-any.whl", hash = "sha256:82f2508ca31cb21e53a41648482d890fe1f5c6cb153720551af41161555adaf1", size = 65954, upload-time = "2026-05-17T22:13:27.549Z" },
+]
+
+[package.optional-dependencies]
+numpy = [
+ { name = "numpy" },
+ { name = "numpy-typing-compat" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "paginate"
+version = "0.5.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/ca/24c6bc92aca3d366146358e47f1d374c0e1a50763ef226fc71eff20a63fb/pandas-3.1.1.tar.gz", hash = "sha256:05fed3e3b7c99b226f160e29d7d6cb7ef39638c758cbfe42b3f24ba34f04b859", size = 5936163, upload-time = "2026-11-04T15:08:01.994Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/0f/d9931577227ee434184eaa122b768bbc6fa03f07e181eecadd2ee9efdd95/pandas-3.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5f29016e40682637b16acb96bd3327120c05d673c4f2d1961481728e6b0a4c4", size = 11676136, upload-time = "2026-11-04T15:06:35.089Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/da/a1077d57b531ab1123119d4117ddb78b6e039119b2ff7658cc53c6196636/pandas-3.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:862f6fbef3f05005d78df13ffd9c44606fdcb76af8748ab14c901f6181bf3789", size = 11268452, upload-time = "2026-11-04T15:06:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/13/becef2d3d8544ca4ad1d910414d24df87664566082db3529ffb65e7082ec/pandas-3.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0cf825769b60f9a01a8ff77ed82cfca9ccf2609170814f8f00810f5eaeae71", size = 11547130, upload-time = "2026-11-04T15:06:40.763Z" },
+ { url = "https://files.pythonhosted.org/packages/23/c3/dcf098d687f1f31c97b9c3869809fa2cdb6598039299167f6dfe0cf0c3a5/pandas-3.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:326459d8a9be11b5f48c7fc1d63e38ba7cfeaebe5d41bd19e4c5f4f11dc9ac91", size = 12029794, upload-time = "2026-11-04T15:06:43.68Z" },
+ { url = "https://files.pythonhosted.org/packages/59/70/0d0f9ebddae0a449af531858674a897d0902c12d95d154a6865bb3e42b68/pandas-3.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9c69786754f997cb401b98328678cfd9c7ac64bd111a27e305b44624fcaad5c2", size = 12576463, upload-time = "2026-11-04T15:06:46.31Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/ec/94125072c82e75ebd1c5a9fcb0bb0a778d3ac1cbc7f44b93bddacc0bfae0/pandas-3.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e95649e97b925ac63204075233e395250d38ea4051d0540f32df9e08e557654", size = 13104015, upload-time = "2026-11-04T15:06:49.085Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ea/c002d6c9ec0ab90b82eb4ff3d23816dc4e0ccf77821abb11bb78b9d9df1c/pandas-3.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:fdd87cf4d59a3c45f17af6ec965d0bd10b9f15e1b5082d2da11a577439551bcd", size = 10888164, upload-time = "2026-11-04T15:06:52.025Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/59/c5e10c034421175579e2375bcc43222a8d06d5440b1f5b09c80bf88ddd2d/pandas-3.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:42283f38afc154980f7a9c7964161ecc94de8c1bdebaf42528a18d40b556debd", size = 10287699, upload-time = "2026-11-04T15:06:54.519Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/e9/70433ed8a4ed227a35c41e68fd7ae0c516b5623eb003dbe13a6cf2138fed/pandas-3.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e3b7740decda762970df4b32edae1450630c77c10c5efb4c73a4437727dc7a5c", size = 12136868, upload-time = "2026-11-04T15:06:57.897Z" },
+ { url = "https://files.pythonhosted.org/packages/08/d1/03a5eb23b0418c5724fd1659d225503ca0305da5683a080ab7eb089d692e/pandas-3.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbe426189f238bee1d05a5ebe11919db8302d397e31d61b10ca0f9a19c75f70e", size = 11735549, upload-time = "2026-11-04T15:07:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/43/c93e7487f34001aaaee1fdc9e20e3bc24adc017c72bd4e67503ec9c45799/pandas-3.1.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55eb3ed76e7f707786468c1cbf7bdf26f4cbed077487f09f0af474a302b9b079", size = 11414807, upload-time = "2026-11-04T15:07:03.377Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c3/05a7b023e31c22137f3807a044c21da7255f5e233b32199842fb580a72c3/pandas-3.1.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211aabaf3f6aec76d87c69522a3e7b26f9e33cb9dc2336359a92b209de582899", size = 11869658, upload-time = "2026-11-04T15:07:05.963Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/b3/3cb6e73af148937766b7a68c93512707560058d817cc77f5931ce2995e0c/pandas-3.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:34e62b61a1e16e125239300f3503f54c4419eabe7df8a364f88cbda2ee289201", size = 12429463, upload-time = "2026-11-04T15:07:08.495Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/36/890746861b103d9da38b8b01317ad215f1495fe9bc2c7611f27d93917c46/pandas-3.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42160e7f78b8a74df8c00e4f3a6010a47bba567fc514dc5029b5a722c2f37ef5", size = 12951459, upload-time = "2026-11-04T15:07:11.194Z" },
+ { url = "https://files.pythonhosted.org/packages/70/b6/b3e0c6308d5ab92aa6a0c7f96020e8244bd77d87659343366c2c0e0ab84d/pandas-3.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:78978d0bf2fdb23f2eab1791abd685f03eb580f9247ab1583be8b74e4e270dbb", size = 11303679, upload-time = "2026-11-04T15:07:13.937Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b3/2906768f0a40eccd37610b8bd6a4abd6938a89f01939bb2e22855085c4bb/pandas-3.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f6c63d287fc2eddf698ebd3bd82089aba2dd4f520f6e4f341d28e125644a6137", size = 10634727, upload-time = "2026-11-04T15:07:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/53/f9/ec5d115b70790acbe1623d72c0d0e50210efa415c27e3bfeb2f3d6067690/pandas-3.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:b80ff163f9d8af866c102976ef727e10a93b1ff04134eb356d3547bd42392857", size = 11668344, upload-time = "2026-11-04T15:07:19.401Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/63/084626245db789e30c816c19919c9f1fe69772effe86b24a2e8153632de2/pandas-3.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:1e13eb2650f438ee4acda9c50ab7692ea2bc2c5fd5959d1284c83bc7eb287e5e", size = 11260058, upload-time = "2026-11-04T15:07:22.095Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9b/bdb3cbf0c4a8155f4f221752af2b8ea7fdd28d1f66f29c61f79eb57c7d02/pandas-3.1.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b71ab4607da4c1665f3db3bba62dfe7222f66a0ca353de0e2d9d934480dc19bf", size = 11541410, upload-time = "2026-11-04T15:07:24.74Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c1/85fbd576bfe7096260f4e2bd6883485a8e23a3783284712bed679c886731/pandas-3.1.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b6f34c1fd341128317fd880aee0365020fd14be573c375fb4c72e63567c6ba4", size = 12028228, upload-time = "2026-11-04T15:07:27.541Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/ae/fcdb22a703b614d524ec7d002fb0631f5c44baae2d195a40f574d10a7aa0/pandas-3.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8603f29efe58c21729e57d2feb3e86f4c19378242e1e2aeece5f8c3807c421c6", size = 12564972, upload-time = "2026-11-04T15:07:30.486Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/6b/707e906369750d8b785ff188d93a897bf3bbd60db3617074d548054ceed0/pandas-3.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9c6737e125607799df55584c682ff05db4ae4db09180e300448781b7e4448bca", size = 13097497, upload-time = "2026-11-04T15:07:33.198Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/fc/13d84a0b63f6e1c1374d3b455e5f54fae8a4ba69b873829d72c84de96ec4/pandas-3.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:07998b563a4e1ff2d6062ead5a7b145bb7f70096bbd428b7dc556672970ff0ed", size = 10880612, upload-time = "2026-11-04T15:07:36.083Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/df/eb01eb04fc58681afbfeeb51179d03c1ef14544e5257e107cb3312b9a98a/pandas-3.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:1983c990935a389b0d0d84f278a60ddfe5fcc5a9dc8a10e11a1e26b5d1220552", size = 10280721, upload-time = "2026-11-04T15:07:38.417Z" },
+ { url = "https://files.pythonhosted.org/packages/75/8b/74ec73f8cca62692835d16ee80648d937cdf204427bf5b1a57fca991ea30/pandas-3.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:77c2f479cfd0bc76a6c2aeee74d5d8814864ebbee9d351dc5e3a1864e95edbc0", size = 12111709, upload-time = "2026-11-04T15:07:40.919Z" },
+ { url = "https://files.pythonhosted.org/packages/66/35/5a21b5ae27f8dac664938ba41ca506db46988df8fc98c6f3f27534bff431/pandas-3.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:560614c776ea303182708c5ad6ad7e5350645b90ca96f9b955773453622885e0", size = 11710870, upload-time = "2026-11-04T15:07:43.715Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e7/47cbfb78ff93f5fb5dfe8d930059749f7e1df0d40d12ee7d30c9006e4c89/pandas-3.1.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:408067fdb3fcc794836bbcb320d4f15d546f39638156e00b5a2dd6616ee0f388", size = 11382425, upload-time = "2026-11-04T15:07:46.479Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/10/a6e8b8f7b76b144857fe3c1c8afabcfb261c682e43b8d39c15d1dcdb46a5/pandas-3.1.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b826e9052502570cd4449aeb0209bcc01e4c25baf774678ddeca225a92302359", size = 11854999, upload-time = "2026-11-04T15:07:49.158Z" },
+ { url = "https://files.pythonhosted.org/packages/52/73/e48611971651d165154595ce4008090863293ed6e5d8b6068881d672757e/pandas-3.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:198a8ee3276344a99bb2b05e0a92380b1d5f51469a193b8dcac7cfd1b34af23e", size = 12395695, upload-time = "2026-11-04T15:07:52.134Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/1a/01d622913e6a65d447eb9a42d1ed71d2a50c4244de24091128aaab023875/pandas-3.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:daf86c882e376ae2a8a459b2f769258d2b6d75e09f6327440cf6eb8d425c5ceb", size = 12913291, upload-time = "2026-11-04T15:07:54.651Z" },
+ { url = "https://files.pythonhosted.org/packages/69/73/76b46776b10c6ff7c2037e3748eeb713df8391a34b7768efa81a08c2a77a/pandas-3.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:f89fb8e88cc5791995cc076c526eac605adc157b6ac6cc619a33ddd0b8bf5871", size = 11273592, upload-time = "2026-11-04T15:07:57.141Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ab/cce173d73200410f06306a89dbbb5f2125bd64f66ca5eff393d53d813288/pandas-3.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:4e6f9a3036fc6a40c5ba00efe4092a45844212bdb6a63eda4ad5a5a4c8da45eb", size = 10604158, upload-time = "2026-11-04T15:07:59.733Z" },
+]
+
+[[package]]
+name = "pandas-stubs"
+version = "3.1.0.261029"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/f8/0346629264426a6693affbc9a0fb52143acfa38112d569f68da514ea503a/pandas_stubs-3.1.0.261029.tar.gz", hash = "sha256:dcfb5b14d399beae72cce558db6d2a0d89c10301275557f1c00d5393e9cb190e", size = 119466, upload-time = "2026-10-29T17:09:31.352Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/2c/ecbc04b2a8d05d737351a54398f843223cb66edf42ec5c0fcc6198f17d3a/pandas_stubs-3.1.0.261029-py3-none-any.whl", hash = "sha256:70f631f2d0781c4500b1cd5f84cab7d7279a3d114674d69655536a4a99a3a67f", size = 179851, upload-time = "2026-10-29T17:09:30.112Z" },
+]
+
+[[package]]
+name = "pandocfilters"
+version = "1.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" },
+]
+
+[[package]]
+name = "parso"
+version = "0.8.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "13.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/45/fbad9c7f5e2f1745c373a434d46072bc84eddfefaa28b852425ac957fda6/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", size = 4190721, upload-time = "2026-10-16T22:16:27.524Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/bf/2074902a259a90692c5dbf132885d21671644cbbd3f690047516243a55db/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", size = 4290236, upload-time = "2026-10-16T22:16:29.562Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a5/3d1bb93b8ea38d15d21356c1b8e9f50971ac592aaa248f7fb71739094c0e/pillow-13.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", size = 3723093, upload-time = "2026-10-16T22:16:31.249Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a1/be060135d85187b25fa8b2573786c0e45c62ae27527fff5257675eb97f4a/pillow-13.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4", size = 5572253, upload-time = "2026-10-16T22:16:33.103Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f4/0bf6f1ac9ba7ee2ffe9914aeb410a1035a0c1a3c42f122388d7fc950bc4b/pillow-13.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d", size = 4852269, upload-time = "2026-10-16T22:16:35.102Z" },
+ { url = "https://files.pythonhosted.org/packages/83/16/b603274617b3e6728da3a0133d4fba1ea8d7fbe0c4ea5fef9cbffa398b8b/pillow-13.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", size = 6516058, upload-time = "2026-10-16T22:16:37.228Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f2/1371707ce438ba9a43f9d24089828494ec6f9396e28cd7903aa704dd6306/pillow-13.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", size = 7142574, upload-time = "2026-10-16T22:16:39.172Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/a9/62cf68587f910bc87c5951bcb6e9430c7643a65076adf5f18b4bf350e2bd/pillow-13.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", size = 6614295, upload-time = "2026-10-16T22:16:41.117Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/c9/9e9b48178649730299be0bafbf5f810a5b6977538f5a92d47e4ba5f5d608/pillow-13.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", size = 7224841, upload-time = "2026-10-16T22:16:43.118Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/48/c44e91384b1b6c072a714a63d00608acc16a6a968735e478e4be4384e5ac/pillow-13.0.0-cp314-cp314-win32.whl", hash = "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", size = 6508694, upload-time = "2026-10-16T22:16:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e2/7ad17747517e4c813dca78257d6bb593466f8231baf1384bc98d92745edb/pillow-13.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", size = 7281830, upload-time = "2026-10-16T22:16:47.725Z" },
+ { url = "https://files.pythonhosted.org/packages/41/9f/e5b51f2ce8ef4563215334035403ce61a244f93a2e65c24a940131618f87/pillow-13.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", size = 2695468, upload-time = "2026-10-16T22:16:49.515Z" },
+ { url = "https://files.pythonhosted.org/packages/97/73/a53502f731f6c84805a4ce87a8881e86d07a65aee64728b900d112dc703e/pillow-13.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa", size = 5578196, upload-time = "2026-10-16T22:16:51.291Z" },
+ { url = "https://files.pythonhosted.org/packages/21/50/7e949959a56b5559c78ef172b70fc6025706acb289f3f648a15329e4fa6f/pillow-13.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f", size = 4857276, upload-time = "2026-10-16T22:16:53.399Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/52/43ded0ac08e77e39d17bd6d894162572201b5e8fdc4a022b14c55a1960a4/pillow-13.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", size = 6607008, upload-time = "2026-10-16T22:16:55.525Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/2e/4a57ed216d5bf877387cba4d44c02aa87abe51ad2e06404aa6b597c4da39/pillow-13.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", size = 7224281, upload-time = "2026-10-16T22:16:57.738Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/3d/2929b248e135752d63aaeabf53f6de797e905b7b6263f9c1e2adade20aba/pillow-13.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", size = 6695600, upload-time = "2026-10-16T22:16:59.842Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/33/fb0da992cf084ed1ee6ee05e96b6191e35c351eb707ca2df874feb24ce47/pillow-13.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", size = 7300589, upload-time = "2026-10-16T22:17:01.686Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/af/c30dd9ef6a3dce84ecb1b2393cac629047a32e66279056c6cb46c3ffd65c/pillow-13.0.0-cp314-cp314t-win32.whl", hash = "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", size = 6517204, upload-time = "2026-10-16T22:17:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/d5/ef3a8ae7609c8ab26570abf18194882534b480d245a9148b895a2262c004/pillow-13.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", size = 7286695, upload-time = "2026-10-16T22:17:05.543Z" },
+ { url = "https://files.pythonhosted.org/packages/84/3c/0861db0c69a03689ebe3e7599d35668a414deb5aaafd8255215e4e62e3d3/pillow-13.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", size = 2698098, upload-time = "2026-10-16T22:17:07.674Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/eb/e86a41962a3a31ad495a3d6a912d9002b01bba4a6fa71c28703abdca7f69/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521", size = 4191213, upload-time = "2026-10-16T22:17:09.449Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/04/7e7445e706afec84ef06dc25b2c77f9cecff242c23730c62928ad1089d4a/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc", size = 4290650, upload-time = "2026-10-16T22:17:11.196Z" },
+ { url = "https://files.pythonhosted.org/packages/46/85/d534089068ac40c0f61f650389a296c812eab745a8a733f81feec39ae955/pillow-13.0.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac", size = 3723078, upload-time = "2026-10-16T22:17:12.772Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/2b/00b5c3b401df538b5dbe311d3899730fae5ba1e21cb8b80a128887ac6747/pillow-13.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d", size = 5572292, upload-time = "2026-10-16T22:17:14.539Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f5/a6b0610f946e9b54ae888ba7ad29d6e2f8d9b11f69e212d70416b7186c09/pillow-13.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782", size = 4852222, upload-time = "2026-10-16T22:17:16.521Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/d0/c0ff25bb0427ec70f32aac995453321fc3c4494a46c7292ff02d9d260b04/pillow-13.0.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071", size = 6527349, upload-time = "2026-10-16T22:17:18.534Z" },
+ { url = "https://files.pythonhosted.org/packages/85/11/e46fa0bcc0fc117f1eacc206b3d51f04d583fab328b4b740672d0438d83f/pillow-13.0.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf", size = 7152873, upload-time = "2026-10-16T22:17:20.415Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/53/2bdeca705f3a58679fa9201233193c86a30238340ca29b3a152c3fcc719b/pillow-13.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3", size = 6626871, upload-time = "2026-10-16T22:17:22.361Z" },
+ { url = "https://files.pythonhosted.org/packages/14/de/3829030f209722ade3753bb456b805a0e7f885cf885b483407365c82e9ca/pillow-13.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932", size = 7236439, upload-time = "2026-10-16T22:17:24.352Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b1/67fc37388382bc036a74f04da5bb6fcbf90ec3ac1f45893e06176c373fd3/pillow-13.0.0-cp315-cp315-win32.whl", hash = "sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81", size = 6508688, upload-time = "2026-10-16T22:17:26.314Z" },
+ { url = "https://files.pythonhosted.org/packages/44/05/fdb6a79aee10861165526b41b7b440e36ee1ff58613b2fbefe3acbf06d4f/pillow-13.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6", size = 7281107, upload-time = "2026-10-16T22:17:28.198Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9c/6c6f7dc2a566467baf121394e83de25ac287cffe34cc030726beb4be0224/pillow-13.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0", size = 2695513, upload-time = "2026-10-16T22:17:30.013Z" },
+ { url = "https://files.pythonhosted.org/packages/23/16/e9120defdb8bff693b582d035312acc6f530cdc2bc5238a87f410efccd82/pillow-13.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f", size = 5578129, upload-time = "2026-10-16T22:17:31.642Z" },
+ { url = "https://files.pythonhosted.org/packages/47/22/76faf2bb2448f48a84cf894c1e2bb1090d7d2b1aa521ece131cb2f05d631/pillow-13.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88", size = 4857428, upload-time = "2026-10-16T22:17:33.43Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/64/0861ea4e03c50b14af9ed9dd8b9f388ab42f445c623a9fcbab19aef7aa55/pillow-13.0.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb", size = 6612582, upload-time = "2026-10-16T22:17:35.415Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/20/cd1e9b3f60a9f3d5ebe46da36955c26004092a501f7f2c42f9fcc26611bc/pillow-13.0.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe", size = 7230176, upload-time = "2026-10-16T22:17:37.362Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ef/ffb0a2fc85690714fb7ac662956949645d57f15bf4426f520da361e7b6e3/pillow-13.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22", size = 6703640, upload-time = "2026-10-16T22:17:39.313Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/c2/6402ee3ccb3363aeed8fdb3f9fe56703ac5377a539f0b81b53999d7e121f/pillow-13.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630", size = 7309231, upload-time = "2026-10-16T22:17:41.021Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/96/d6b82e8b8c06ad143b66b12e0b31051e4bf489b4fabca1f390a14fe1ebde/pillow-13.0.0-cp315-cp315t-win32.whl", hash = "sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2", size = 6517200, upload-time = "2026-10-16T22:17:42.886Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d4/0062e6005e9b270b165015aafd343364ac378ec5a685431ea42baad33357/pillow-13.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43", size = 7287488, upload-time = "2026-10-16T22:17:45.56Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/81/b8eb349746be7bce47dd95337c6634e84910af91b59aaa9159c034c614fa/pillow-13.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8", size = 2698073, upload-time = "2026-10-16T22:17:47.556Z" },
+]
+
+[[package]]
+name = "pint"
+version = "0.25.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flexcache" },
+ { name = "flexparser" },
+ { name = "platformdirs" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.9.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" },
+]
+
+[[package]]
+name = "prometheus-client"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.53"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/42/5f/29fd5f29b0a5d96e2def96ecba3112fc330ecd16e8c97c2b332563c5e201/numpy_typing_compat-20251206.2.4.tar.gz", hash = "sha256:59882d23aaff054a2536da80564012cdce33487657be4d79c5925bb8705fcabc", size = 5011, upload-time = "2025-12-06T20:02:04.942Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/7c/5c2892e6bc0628a2ccf4e938e1e2db22794657ccb374672d66e20d73839e/numpy_typing_compat-20251206.2.4-py3-none-any.whl", hash = "sha256:a82e723bd20efaa4cf2886709d4264c144f1f2b609bda83d1545113b7e47a5b5", size = 6300, upload-time = "2025-12-06T20:01:57.578Z" },
+ { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" },
]
[[package]]
-name = "optype"
-version = "0.17.1"
+name = "protobuf"
+version = "7.36.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9b/86/e6f1f6f3487492dfcf3b7a2d4e2534d27af6ac05b364b276706906c34865/optype-0.17.1.tar.gz", hash = "sha256:07bfa32b795dea28fba8605a6288d36370d072f25183fb9c29b5a90f4b6f5638", size = 53572, upload-time = "2026-05-17T22:13:28.725Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/89/5b8517baa72f84a67b8a307ba953c91057af618bf40bf676f3c03551f8f0/protobuf-7.36.2.tar.gz", hash = "sha256:497d0463ff3316681da6c0b9e8d06cb465d61abce00b613ab42226175644d1bb", size = 512737, upload-time = "2026-09-17T20:07:59.326Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/d4/c6a2b043e33f0dd012486dcebe0593585588d400175d22aad42049c88321/optype-0.17.1-py3-none-any.whl", hash = "sha256:82f2508ca31cb21e53a41648482d890fe1f5c6cb153720551af41161555adaf1", size = 65954, upload-time = "2026-05-17T22:13:27.549Z" },
+ { url = "https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:cbc70b17ee27e28894c7fee8bb04be1abead49e936bc70eb60052531eee2079e", size = 456039, upload-time = "2026-09-17T20:07:51.542Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ea/91fdf7c2b8bbd49cde056f00a9df6773532987e1c00fe2830b895af95c7e/protobuf-7.36.2-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:e11e1f0180583a2af89db6a2ecd9e8dc40aa6d2988ca175bfd0e6d12ea72d74e", size = 344219, upload-time = "2026-09-17T20:07:52.914Z" },
+ { url = "https://files.pythonhosted.org/packages/17/ab/5fd5f8ece73fad885c5a09aa849b32d70472f954ba3a92d3bb5974ea953b/protobuf-7.36.2-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:f4fee11ec330d238b34a05c9b675f693c20415d1c5bd7d5320cc2f8a798eb9cf", size = 357223, upload-time = "2026-09-17T20:07:53.985Z" },
+ { url = "https://files.pythonhosted.org/packages/db/f3/3996583dd2906297a637af12114deddf7658af6e683fedb83be061983fb5/protobuf-7.36.2-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:89f23aa53c24553a2416fd4fd1ec06f74fa42b14b546d8883128813f775bbfd2", size = 343223, upload-time = "2026-09-17T20:07:54.931Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/1b/dcc64f358fcb51811b58ae40b3d28f820725f116d86487cc20bd4b130701/protobuf-7.36.2-cp310-abi3-win32.whl", hash = "sha256:912c1221170e16c08d1f086762f563dd61ff83c18b5fa6652952dfaded66f728", size = 442998, upload-time = "2026-09-17T20:07:55.826Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/55/b77bda4e5e5f5971fb51b07663694690e9afdb9402136c16a522bd621cad/protobuf-7.36.2-cp310-abi3-win_amd64.whl", hash = "sha256:a300819d441e078a5608c0d3c709796bb548136058fda017ae51d425b44fd353", size = 456514, upload-time = "2026-09-17T20:07:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/04/d52c7016b04b6c5108f26691f9d33ec82a9b65d041f1a9c771137693d618/protobuf-7.36.2-py3-none-any.whl", hash = "sha256:bdb3a345d48db958e6ce1f18e508beb0cc981d64f24088427549c866cd039f1e", size = 179806, upload-time = "2026-09-17T20:07:58.211Z" },
]
-[package.optional-dependencies]
-numpy = [
- { name = "numpy" },
- { name = "numpy-typing-compat" },
+[[package]]
+name = "psutil"
+version = "7.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
]
[[package]]
-name = "packaging"
-version = "26.2"
+name = "ptyprocess"
+version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
]
[[package]]
-name = "pillow"
-version = "12.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
- { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
- { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
- { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
- { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
- { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
- { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
- { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
- { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
- { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
- { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
- { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
- { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
- { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
- { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
- { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
- { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
- { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
- { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
- { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
- { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
- { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
- { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
- { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
- { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
- { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
- { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
- { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
- { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
- { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
- { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
- { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
- { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
- { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
- { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
- { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
- { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
- { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
- { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
- { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
+name = "pure-eval"
+version = "0.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/9f/abfd2959e9261dd5217ca8551d4de211ca6ab26fe9b72cf44731ff6c4442/pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1", size = 20563, upload-time = "2026-09-10T21:41:22.836Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/18/83376915176eb058cb86470eb7396388a13350b09a8f233a79303bbcbc5b/pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a", size = 11893, upload-time = "2026-09-10T21:41:21.532Z" },
]
[[package]]
-name = "pint"
-version = "0.25.3"
+name = "pyarrow"
+version = "26.0.1"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "flexcache" },
- { name = "flexparser" },
- { name = "platformdirs" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/5e/9cb2a56d5e293aab4e2fbecc5b0620d93227d66b8d5a63c554c31e1f80fe/pyarrow-26.0.1.tar.gz", hash = "sha256:93affcbd8a343e98dc19e9ec8fe17271ebf8fe98102d8b7454175074409f8b72", size = 1237985, upload-time = "2026-11-03T10:54:17.563Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" },
+ { url = "https://files.pythonhosted.org/packages/de/48/999f6e30323e2e95a4439180b62f6308023970e4a37d31497080ea2a1a78/pyarrow-26.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:15435b595c14583f6ac8495c3ef89ac3fed6bc94f168467bfd3b6ba53ad3a643", size = 37334639, upload-time = "2026-11-03T10:50:59.462Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/3a/4458972c69ead95af5d3b701c80885133c351075895382499df49ed92d36/pyarrow-26.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:a44a244867cac106cd326dd0a04be0850d2b8af3b1418ed7aeb8146d13aba360", size = 39802150, upload-time = "2026-11-03T10:51:05.743Z" },
+ { url = "https://files.pythonhosted.org/packages/86/d0/fa4bc5d66f78cbc4ec8482b210c2fc3b8cb59564efe171f3ca9be416f8ee/pyarrow-26.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0044cb9303c3ae082cc52a2493312109c955bcfa84e87be768736f7d6d8f6257", size = 51880097, upload-time = "2026-11-03T10:51:12.75Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/57/755a1b54d37c2a9e2c283d8bc648a50aa4ce57f977ce6a4b1d65eba29f51/pyarrow-26.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:7ae48ab53610b85e004b03bccff593fbdf22186d0ab48ed42c90797915dac814", size = 55063542, upload-time = "2026-11-03T10:51:20.417Z" },
+ { url = "https://files.pythonhosted.org/packages/45/ba/7cd1d78c2ef5a36b1f8d137d1790179875639f197ed90702268084a2470a/pyarrow-26.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a97df0c52c966ae8970ac0899ae75d81083c5400eca267de2608ca0f13e6e118", size = 55490507, upload-time = "2026-11-03T10:51:28.295Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/02/39e18c5381ad908d7161cc16a02dce03b3db236d3c3ac99fc020878287fb/pyarrow-26.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f665e73790222779613ae3d23d2a1d72f0f13a4646862277e9784bb46e14fca", size = 58648328, upload-time = "2026-11-03T10:51:36.303Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/18/2cd453cc2e282d465e9c9b60834d6ca17601dc08e2d402751cb40af92356/pyarrow-26.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:492dc499097968e51cac46e03e0054c68dd3c5f39d75098343f5ad05ec79f7df", size = 29757743, upload-time = "2026-11-03T10:52:35.061Z" },
+ { url = "https://files.pythonhosted.org/packages/db/46/b97f117adb46f93620aa94bf13747a81ccc5e099be534cc195c94b281ba7/pyarrow-26.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ec196b35270263eda71fc3f43bd31a89b778c6787f19872df5abb9d4264057ff", size = 37452099, upload-time = "2026-11-03T10:51:42.419Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/40/ac1796654fc8591049d200f6bcd646a2319509010784f47c1cf6b09d1000/pyarrow-26.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:8af2b4e004dc18da8dc77bce27d97c675d2f71348e3ade2741cecac85c3f0c21", size = 39888805, upload-time = "2026-11-03T10:51:48.766Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/93/bcff79148416896738a707992570c4ae8e3bb087bc9e0dca8df7dbf6e983/pyarrow-26.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c2bf4444a7e86f109dc03e28b2c98eb6671e2eb95e6057c25043d25684e337e9", size = 51884587, upload-time = "2026-11-03T10:51:56.701Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/62/cb05e81d31cd1dc97c2e16151425e61fe3a19274ccd1254cafab3a173f96/pyarrow-26.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b121de63e2f92704b6b9881fbe69cb3eeca786ea882379b59ce80b8012c1bd8c", size = 55018252, upload-time = "2026-11-03T10:52:05.813Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f5/e8dbbb803bb28a32411491d502679e272c569af7f590816327e84725c748/pyarrow-26.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:40b9c1a5841559030ba8d1b8c15d6160bccb702ff2596b4ce296e1efd7665781", size = 55522075, upload-time = "2026-11-03T10:52:14.577Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/7a/5000d6082b116359d3db3ee6073a8f708f41cb4bb7d6414f94cd40a90fe7/pyarrow-26.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9519f70bd847f7628a2d16324d1ea8827e2ec1da347ee0bf96cc7c248195e923", size = 58625604, upload-time = "2026-11-03T10:52:23.045Z" },
+ { url = "https://files.pythonhosted.org/packages/41/70/74a5a07eeee1a84cd51e81813b7fd2d5ffc90192a95981a51e97a8ced99e/pyarrow-26.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6d9b558e1bce3270f1f422c61fd682615cfb34ffd320a51e765e868e8c155a72", size = 29925653, upload-time = "2026-11-03T10:52:29.188Z" },
+ { url = "https://files.pythonhosted.org/packages/77/09/4e9461479930e3f4367cc0b4f4356d4444a23be9cb350272b159cd0c7fa9/pyarrow-26.0.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:2c118754a0abe0bf20ef17432f95534ebb0ded37a48b6e8690cdd02625a86d97", size = 37329458, upload-time = "2026-11-03T10:52:41.622Z" },
+ { url = "https://files.pythonhosted.org/packages/40/5a/9e44840924b0939defacc2561e54dc9aba10eff1fc5dadcf41921202d99f/pyarrow-26.0.1-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:99574d8eabad06cc4bb3fed327b298c8a44a3596abd2d575738fa019df7ac59e", size = 39799948, upload-time = "2026-11-03T10:52:47.507Z" },
+ { url = "https://files.pythonhosted.org/packages/70/3a/5fe40369653a082d86fdb6ce2b7c40a4ef435d90a68da60c05dda0c554fc/pyarrow-26.0.1-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:12c5edbdf9b8a0f121a0a222be150c3a711149bc4322736d6a04693fbfd8c723", size = 51875337, upload-time = "2026-11-03T10:52:54.59Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/87/f768a458cf975c17c73912fb25a1e0693ceef8d76d78cf83ffdf1f63ec70/pyarrow-26.0.1-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:8cb5bcfe627ee6b910fbec27871d38cf4f28f331ea6328902ce6a6272851ac70", size = 55060370, upload-time = "2026-11-03T10:53:02.19Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/30/280c24a7da9085b782cd6960ccae6763b0170f51385b9122385c12226ff2/pyarrow-26.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e2ee5f01f84996109405759cba8bb92a16455c50deb6526a535ab3b8d5f4e326", size = 55488705, upload-time = "2026-11-03T10:53:09.8Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e7/8e7b7f7bd58c88fd8da93db0d0869c69175c3ddccf365afbcc13fbd691da/pyarrow-26.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e25d4a886868c5ce136d194f308bd690e1335aebf6b8bcae3511b0becdb3a65a", size = 58646822, upload-time = "2026-11-03T10:53:17.902Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c7/6094fa2c0b5602d81b1177ac7f5b2de8a386862d08e57a75931da774f3e0/pyarrow-26.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:6cdad5f67a30bbcea54c3c2a27ea45bd5a170377bf45f80f4d7e3a726e8e3283", size = 29755706, upload-time = "2026-11-03T10:54:14.582Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0f/86747650c8218bcea7443884e7c8ba9d3fe9a13fb5a4be99acdd5c2b9144/pyarrow-26.0.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:12dc85cec96aa62d429d7c43749c072e9330c373b67d6ee458edeef444c8941f", size = 37445268, upload-time = "2026-11-03T10:53:25.531Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ec/4159b2362267009b754a7c2f138f16bdfd6623b308005678eb7cf1b65863/pyarrow-26.0.1-cp315-cp315t-macosx_12_0_x86_64.whl", hash = "sha256:52e4daba8f1b4316b7b9ba7e84903a339cbc52998ebddc9a7c78b5ea37c23bbb", size = 39877540, upload-time = "2026-11-03T10:53:33.046Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0e/df0501e93eef78402f94f33f948aba996181b3d3c54f97a3f16544916d05/pyarrow-26.0.1-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:322f6fc80c6db1ec87c9d905df3ce259cd22e922f47fbad3c58d0dd6f63bc7a9", size = 51874165, upload-time = "2026-11-03T10:53:41.281Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/30/0d43bf6a49fca51142a96c73d693ec34d229c902a46e007c25f3b9798b52/pyarrow-26.0.1-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:68b91f34e3424d55564e9d4d4f7491aaebd6db3998039cc33ba795a63167f04e", size = 55012741, upload-time = "2026-11-03T10:53:49.153Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ff/0e6cd73832d97c3852284eda9bee2428c53a92bcf6f6aa4bda1e15314a59/pyarrow-26.0.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b801300d65859adaf611c9c0a4cb39bd3cb1713b60b974ad20650a8a0f8af2fd", size = 55512474, upload-time = "2026-11-03T10:53:56.512Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/1e/43cdcf9719169f0228b3e516e5004ed0acfc3a4c70ce35e3f2a80a485baf/pyarrow-26.0.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2623b6c20637c0e2c40be96bda3d127dde49d6e6734ad6e71df91a013e40a3c4", size = 58616300, upload-time = "2026-11-03T10:54:04.472Z" },
+ { url = "https://files.pythonhosted.org/packages/df/bf/8e29b47c61fc961e92e96db129d5dc7b716c2091dfe05359e70f20d712b9/pyarrow-26.0.1-cp315-cp315t-win_amd64.whl", hash = "sha256:55b1d871f21c8ed249c93f7375a35c35aa9b7b3af6896405cb8a6b67ad9bc7f9", size = 29909417, upload-time = "2026-11-03T10:54:09.613Z" },
]
[[package]]
-name = "platformdirs"
-version = "4.9.6"
+name = "pycparser"
+version = "3.12"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" },
]
[[package]]
-name = "pluggy"
-version = "1.6.0"
+name = "pydeck"
+version = "0.9.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+dependencies = [
+ { name = "jinja2" },
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f4/c9/f71032fca47ecc09d30904d9a610234b07139f89eabc2f054b141edcc30f/pydeck-0.9.3.tar.gz", hash = "sha256:695775cbfe51f5fdffbd9735ba469987fdc5efc96bc40a0ee4808170509c78b2", size = 5900912, upload-time = "2026-07-02T23:27:08.704Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/34/3998411437aff304a9ed4fa37a6fe1ef3132bcd2b5eac59851b80c86123c/pydeck-0.9.3-py2.py3-none-any.whl", hash = "sha256:d8a47c11c81fb12d51b1feb42427ff4f0e13cb599e48931021b2cba98b6849a6", size = 11428091, upload-time = "2026-07-02T23:27:06.399Z" },
]
[[package]]
@@ -379,6 +2147,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
+[[package]]
+name = "pymdown-extensions"
+version = "12.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" },
+]
+
[[package]]
name = "pyparsing"
version = "3.3.2"
@@ -416,6 +2197,288 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
+[[package]]
+name = "python-discovery"
+version = "1.9.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/55/70b46fae646bc6f5cb5e82569949d925cff1c7b5ce20ec4a11e0d977f8c6/python_discovery-1.9.5.tar.gz", hash = "sha256:86279f5d2728bc067de28b7ddc571be2eacc2ae0e7048f8629d2481a0f8848a2", size = 141121, upload-time = "2026-11-06T15:32:11.331Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/35/25bd092ca8a7e71192afdca67fbff4f797da74408e74ac3626e13b409ec0/python_discovery-1.9.5-py3-none-any.whl", hash = "sha256:a1fae8fca11e4bc61e38491c2aafff29e430b3c5e272d51ae52fb895bc4c0fac", size = 44377, upload-time = "2026-11-06T15:32:09.821Z" },
+]
+
+[[package]]
+name = "python-json-logger"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/25/5473e46b179f8e8b4ad3aeeb36773d1701b7770eaf5e5bc2025c7303b598/python_json_logger-4.2.0.tar.gz", hash = "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa", size = 18211, upload-time = "2026-08-15T11:36:38.232Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/55/6467fde553886cb293e41538f3a8b4e4fd4688c6df242cf982162d8367fb/python_json_logger-4.2.0-py3-none-any.whl", hash = "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9", size = 14988, upload-time = "2026-08-15T11:36:36.821Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.33"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/b7/15b905f6142a9a8a68f66900788da1a32e4d4254452783b37ad1eccb0506/python_multipart-0.0.33.tar.gz", hash = "sha256:b8112424a6abd162ec08d390565dd113ad699f2fe6fef3c987b8bbcbfe9e390d", size = 50499, upload-time = "2026-10-31T14:23:47.461Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dd/1b/6b1b4164ec734cac911460ab599bfbe46afedd42c5c9627fe4235942c314/python_multipart-0.0.33-py3-none-any.whl", hash = "sha256:8f16c07b36add9da6b2ee87d3efcaf0a45a6de9c3d0a5dd94e9ccf7675a7bc45", size = 32253, upload-time = "2026-10-31T14:23:46.33Z" },
+]
+
+[[package]]
+name = "pywinpty"
+version = "3.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/22/92b945dd2975275eb21e938524f265bc55a0f2c009de64c2cce6c7f1abc1/pywinpty-3.1.2.tar.gz", hash = "sha256:03e2d2abe7c0df64db354233c83479b2b43d9fdd1badceea994259ca3819b203", size = 38361, upload-time = "2026-10-28T22:48:55.321Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/c6/c6c174b6ef198de4057ea6ec5a653ae42e7f1505d84f34dad824e833b066/pywinpty-3.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:980af688956fe78c87882682732e6eea45aa0b8d10a9628812e95878264d5204", size = 2107056, upload-time = "2026-10-28T22:52:01.07Z" },
+ { url = "https://files.pythonhosted.org/packages/41/69/01fb5ce4e563ff60d8093812a5e70a3234362b38f77edeb2f1d89057b73c/pywinpty-3.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:81d83678211cc6f8cd2954e98a8a18d8e5de1e0666900a1df8da70f33572d34d", size = 830669, upload-time = "2026-10-28T22:50:42.715Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f7/c6f81de3c60a93cb8357c4f86a5539d914539a46507f16b8abc3090d1aaa/pywinpty-3.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fe0abcffd095642c78ba7c6abf4cc8129dce9568e97a1c9194eea23383d0de29", size = 2107220, upload-time = "2026-10-28T22:54:08.299Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/17/84bbe1a907417ae4971394f2ef48b62ac738f01e5bd61f0b0ba84838c37b/pywinpty-3.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:30235f396856fbc03c9bb181b4e27bf50f7b355e432e5731729063d920a09867", size = 830569, upload-time = "2026-10-28T22:52:56.057Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/f9/f1dee749c60f19cb4cfb89b5dac5b13ca9689091e8f13498d4d7649c30c8/pywinpty-3.1.2-cp315-cp315-win_amd64.whl", hash = "sha256:b8c47556fbf19595898dd75f45763072ff1160b080eefabeae8cc43c5acbd720", size = 2107342, upload-time = "2026-10-28T22:54:57.49Z" },
+ { url = "https://files.pythonhosted.org/packages/83/fb/50e01614512e79abd5af65cc380d6d470062794a5ae4fb49fb683d661d09/pywinpty-3.1.2-cp315-cp315-win_arm64.whl", hash = "sha256:7af19f25e769f0c70f5920ce0964c508abdfe01e676cf05d5ed935848d8c782e", size = 830854, upload-time = "2026-10-28T22:54:06.899Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/37/76b4b8e9b1101659ba2363b80597309505a93238233ec101dc2b84488c8f/pywinpty-3.1.2-cp315-cp315t-win_amd64.whl", hash = "sha256:ed5c5ca288001590fbf1d5a52c6389f0f6ae7e4751bdc8b2f8f9c7249f266f9b", size = 2107530, upload-time = "2026-10-28T22:55:40.54Z" },
+ { url = "https://files.pythonhosted.org/packages/15/62/f1109ffb61549598f519bdf802bf1b3123736d059940b192d1d088c68ede/pywinpty-3.1.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0a40537712279ed74d5884b1af9037add1ed4b2e2339e9152268b582abad3a57", size = 830724, upload-time = "2026-10-28T22:56:12.712Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
+]
+
+[[package]]
+name = "pyzmq"
+version = "27.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "implementation_name == 'pypy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e2/e1/ea46ab0d707219c098065a37d2add5b53d3b124241aa19429bcd0863e5b4/pyzmq-27.3.0.tar.gz", hash = "sha256:bbedb3d1d99babc625f1dd9d7438acefd442b36b75073fd6bc1ef7c0a4b96d56", size = 293249, upload-time = "2026-11-04T00:31:33.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/9e/6663ccc8c1af4e7db85de65921560a8058ad4ad4c40ebc49af4b1164f01a/pyzmq-27.3.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:15155fd8f308d3bd47f67a8f6cc809f9db90abbf63c514c384a17622e2cf080f", size = 1232964, upload-time = "2026-11-04T00:29:09.922Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/90/4fc65d7a48077f535705ac957c29a3c07fa54a1f8d2595ca2f5f1489b21c/pyzmq-27.3.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8b828407caee77ff3ab0bdb2984ee8d48922328b226e8fda7c84a71a55dbb4", size = 770405, upload-time = "2026-11-04T00:29:11.487Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/62/e4721d8b8b9de685323717489a408521067a786cabc2b73ccd91aa9b9077/pyzmq-27.3.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fe25231e0fd8612da142efde019faa5ef63fa150a821a27870e791b38c798", size = 855178, upload-time = "2026-11-04T00:29:13.042Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5f/8190ff273beb195d9c9b62a36d0d87bad79019e76ff4407cabe463bc650d/pyzmq-27.3.0-cp312-abi3-manylinux_2_28_i686.whl", hash = "sha256:8b174298b57d5c2397c74fbefd656101d477357035916881970ed5f53b7b904c", size = 919158, upload-time = "2026-11-04T00:29:14.854Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/66/ab116baad852235c82be3d8d83af7098a7165c48995a03f2b6e8d50297dc/pyzmq-27.3.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cffd75359342ef88c86c287c99b16b2b100795dab467fd5398bcedd87d49bce7", size = 813613, upload-time = "2026-11-04T00:29:17.239Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/55/5075ef84055eca159435e65f8d67591bfe06c562cf310792cd50a6aeb0c9/pyzmq-27.3.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:220095b4c11b0542ae5c0d67411331d3819753ee9ec0468e63a4ca6fd4ccf50f", size = 1762126, upload-time = "2026-11-04T00:29:18.994Z" },
+ { url = "https://files.pythonhosted.org/packages/28/6f/e75d5e5aeda920dbcaaedcc413605379287b817578d00e47bfa89a8ea671/pyzmq-27.3.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:e9e12fe784529e12177937aee8c714b7aed8913a2429ee2567400c904d4d55b5", size = 2067522, upload-time = "2026-11-04T00:29:20.762Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/b4/0d91b1e5ea8d659c1014ef39acfda279f4768fc4043120e98b764a59f1c9/pyzmq-27.3.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d317685b83663fbd03ca8b5c34add7463e9bed094e3e1a69d2a895130b64ffaf", size = 1893864, upload-time = "2026-11-04T00:29:22.451Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/23/ed213d9dc6e48913a1f77090d1aed85bc4b1e5567ef74a86fd8fd473f423/pyzmq-27.3.0-cp312-abi3-win32.whl", hash = "sha256:07dd778d7cf242a1e131bfad6244f64df59441dda7ea639d037d0fdb22112b6c", size = 567172, upload-time = "2026-11-04T00:29:24.09Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/6d/23a02024011a494fb52bcfb179f10a3b230cff7b6fe0d5b8a3c775ce3b82/pyzmq-27.3.0-cp312-abi3-win_amd64.whl", hash = "sha256:3ea8e8fa5c097e68fef91d70b150fda96f307758103868018fc4deeb9c214122", size = 642389, upload-time = "2026-11-04T00:29:25.739Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/1b/410fc7ec4fc114b46da17ed0df3b88606f0624fbe172e184658d361b1dc1/pyzmq-27.3.0-cp312-abi3-win_arm64.whl", hash = "sha256:2187a5ca80691a5edb30690f02d9fb714e2c32d3dfdeb652e159ff5da2dd4283", size = 570810, upload-time = "2026-11-04T00:29:27.709Z" },
+ { url = "https://files.pythonhosted.org/packages/88/b8/8a80749971b1a42fc415c720a52deedd33d0ae72792544331fc76f306ded/pyzmq-27.3.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:fca5c2352c499e8355b581ddecfe52538984131b7efc052a30e2d32426baf637", size = 1143753, upload-time = "2026-11-04T00:29:32.923Z" },
+ { url = "https://files.pythonhosted.org/packages/59/2d/196f50920da3137fd54b1636c36aba97b95d18d467bf097edca5f6ef5be8/pyzmq-27.3.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:91ac316f440aca4e6eaf3786f54b80f4ab433e5cc4e5e9b82eb44a27c5c987cd", size = 1182341, upload-time = "2026-11-04T00:29:35.073Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/3f/bf2a67da7cfac3c1f85c0130b8bd1fc48e96c1e66e8a50c71faea83979b3/pyzmq-27.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d292cedc7250db240e9f41994cf3eefe2c7606c702ef5c56aab8d4f03870108c", size = 1271939, upload-time = "2026-11-04T00:29:36.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/43/2bfa60b57b3febd892d5760c811012b6850306962e3dd6a2f91b96cd5398/pyzmq-27.3.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:924f870055c05926887157485efe903c8f9668d0f3f19d56df7612bcb3d662cb", size = 779903, upload-time = "2026-11-04T00:29:38.52Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/72/4f88cd0d1da0e5db9a26a899e37f8b46d925f328f9bae590f86fc995074b/pyzmq-27.3.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dac496cd8aacf185a634e5cc8930331db1627dd214a58ee4c27948ce4ae79af", size = 864708, upload-time = "2026-11-04T00:29:50.209Z" },
+ { url = "https://files.pythonhosted.org/packages/79/5e/fce842845aad4135a2e1936f3cd1a418b4f49e2d45cd7b5576d6f836d95b/pyzmq-27.3.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:665b1a3d35a4ffe6cfa626191a5439108159d51d17bd9d44f1beb1690114adfc", size = 927234, upload-time = "2026-11-04T00:29:52.003Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/28/945736083a00750e101c4259312b5303aead36ef08539a0f0ad93a1cb899/pyzmq-27.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0fedab58520a5091ffa62547eb507bea44882ce72d49929fa9626061ae1b9a2c", size = 1771921, upload-time = "2026-11-04T00:29:53.677Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/30/2a584e845a51c0e5d6e13889fe659aa90912004fbb5bc4750491f558528c/pyzmq-27.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1b9b9d11e2e775a81d0e8b5c6cb76a395ddaa7c6b6373f3b0d2c8f8146d95f4a", size = 2075396, upload-time = "2026-11-04T00:29:55.772Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/6b/93066f121c31b825c56c74dca01ea5c00847f8b44645ad990f92bd04d29c/pyzmq-27.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:169a522c4d166c3bc7b67ed827d3beda549a8e65c4015c9410687757fb39852f", size = 1903986, upload-time = "2026-11-04T00:29:57.741Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ae/ecc7c77cd6ea18ff7b2b07a2fd0db5c31c2204ecf9a774e80bd1250dc9d9/pyzmq-27.3.0-cp314-cp314t-win32.whl", hash = "sha256:0228fe121f05a8b638ef218dde7508477a8c46200a588f71ce7143d85987cdd7", size = 597206, upload-time = "2026-11-04T00:29:59.35Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/0bee73e1e8ab1c26eecba13a4725dfbe0405b53f451a91671dd515effe00/pyzmq-27.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a3d02fc710302a23c9a67b287bc13f2992067d398c3192b50eb0e30d44ed885", size = 685132, upload-time = "2026-11-04T00:30:01.144Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/23/13a768f9a8e66a999250abd216e50e1961a1cd4a8708749f656f5c0c2aea/pyzmq-27.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b9f08778b608fe4f6250e2d132f41b4f99fa8d03051cf2809b1c0420def81b4", size = 595549, upload-time = "2026-11-04T00:30:02.693Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ac/674d5c512c196c3ff2c37b27ce87764e9e69c7591902ef48330ff0ebfc07/pyzmq-27.3.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:81fcd4e798f384a158b1790171a63541e66ccdee5a139a4dd0848a6e88dc7b9e", size = 1143291, upload-time = "2026-11-04T00:30:04.411Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/eb/6bf15cb88fb5d9f8fe6ee6b57185c110f862b6bc1ad02772767571bd3c5c/pyzmq-27.3.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:e9b87a6f40b62c7cd47691fc2ba4fdc28bef58f5fccf48eaf39940d183d2a16c", size = 1181899, upload-time = "2026-11-04T00:30:05.993Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/bc/e00cf032d751a709e12d71c3e2e265957bfb0097d8d071fae4f6a45e2d4c/pyzmq-27.3.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:23ddfe5d4d0c6ea529614c8999065d609a113f343c7e60dda9746d7dcb00d766", size = 1270101, upload-time = "2026-11-04T00:30:07.977Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/87ebb4664bb27fa53382d45f21f9f49d6194c0eb6737d7c2c5d5e921f1ee/pyzmq-27.3.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b437cf437fe737e17082625f470258f12e4bf5d56edfb88cb21d3aa4089fcef9", size = 779378, upload-time = "2026-11-04T00:30:10.504Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/85/e998ff27a29aca9b0f8529e32b0913420e75f8f990a54ad9434faa3d4c5e/pyzmq-27.3.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc2cc98f37a14a3c9baccc18173a9fca9c6d18ffb233bca7079e58b2e47ee7f8", size = 864317, upload-time = "2026-11-04T00:30:12.259Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/7a/6535a0b2fbde9554291caeb3c41b6452207ed7afeb910af830f48e6731f5/pyzmq-27.3.0-cp315-cp315t-manylinux_2_28_i686.whl", hash = "sha256:1a1e092726596141e8ad2cc7f542fd366ddd692b8a9e254d2972ac47b399b982", size = 926240, upload-time = "2026-11-04T00:30:13.862Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6f/0c3b1b621f17ebd3eaa2c974b11507dc88cef529ebdc72b42935d00e4a90/pyzmq-27.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0c9c332883373eb85b72f4d912a9a4402ba7702709fa783d3c76ff2bd703a491", size = 1771499, upload-time = "2026-11-04T00:30:15.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/a5/b753eb9acc131fbb4da67c4c68037965f12f8782d1fd5e461e803e0a1dc1/pyzmq-27.3.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1f724caa222b1ae3c48f7d25147c816c8bde56fb109c164a650f0502b9005ab6", size = 2074632, upload-time = "2026-11-04T00:30:17.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ad/8bce8d0ff0f0cc0958bdfeebbde9ee319cbd025c949607ab2864df657ce6/pyzmq-27.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:129ca693e543940527e105144dcacdf9e0ec01d41421098e08893de8227aca58", size = 1903559, upload-time = "2026-11-04T00:30:19.142Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/25/e5b3e6a066b59840cdc0ea6de8244b1a50b5f237edc7b7adc0e085973708/pyzmq-27.3.0-cp315-cp315t-win32.whl", hash = "sha256:39b9ff1fbab854ccbc53da0e79cb6b78b61b470fb7c191b89aa034825ea9a883", size = 596443, upload-time = "2026-11-04T00:30:20.811Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/c8/20ecc504500aeaab0440a4c8487a3c1cd32f4ac38f3e92482f1671fcaeca/pyzmq-27.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:3b850eaa955ae7edb926fe266600e57da818255d1f6ee9c3d8049967aa4a22cc", size = 684767, upload-time = "2026-11-04T00:30:22.784Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5d/33c528d9b7ee074d0807e3af1a23a3e50bfdbe28cbc178e99f0466c13648/pyzmq-27.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f0b9d192b285a9d38e390e762011f2672432742a301859b6c8cb4fab5f10f687", size = 594143, upload-time = "2026-11-04T00:30:24.33Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
+]
+
+[[package]]
+name = "rfc3986-validator"
+version = "0.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" },
+]
+
+[[package]]
+name = "rfc3987-syntax"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lark" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" },
+ { url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" },
+ { url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" },
+ { url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" },
+ { url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" },
+ { url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" },
+ { url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" },
+ { url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" },
+ { url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" },
+ { url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" },
+ { url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" },
+ { url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b7/a662f367d4896dd0a10cef2fc91f10b7f08af1c10858e287e019563f338d/rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", size = 347463, upload-time = "2026-10-04T16:30:48.203Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3f/ad45d03df4f84ebae5439577ee81f3999d182711e82235c8037b6528890e/rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", size = 342051, upload-time = "2026-10-04T16:30:49.872Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/31/3dcd68c13d4bcc59c1f7eb33ac8e80698f06f3eb0d8a1e06419836071c20/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", size = 374143, upload-time = "2026-10-04T16:30:51.508Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/0d/68c1f058a250fbd1380ebda9fc227cbf50117adf8ffe8161ef383ea79f68/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", size = 378604, upload-time = "2026-10-04T16:30:53.206Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/d6/2d4c59b85397cb4800594fadf692688ccf5ce556adc930e7a5bf21061a5e/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", size = 490790, upload-time = "2026-10-04T16:30:54.925Z" },
+ { url = "https://files.pythonhosted.org/packages/da/04/7e05dc3aebaf52f4e026766bd668fdd09a9d0e23f64a14686b36b3501892/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", size = 393146, upload-time = "2026-10-04T16:30:56.625Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ca/e2e9a0a46a74ed51a0498ba1fe10f4ea6b2d9a155f372a2f91e51f18cf10/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", size = 372022, upload-time = "2026-10-04T16:30:58.295Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/cb/8f8774df5134e23424372838bcc5c7ed4127d723e1ff52f7bebd4dcb2563/rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", size = 377092, upload-time = "2026-10-04T16:30:59.984Z" },
+ { url = "https://files.pythonhosted.org/packages/90/02/8d7095d73bf9114219be40230baa5df00611e0a82ed9517779ff9c19f82b/rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", size = 400830, upload-time = "2026-10-04T16:31:01.721Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/02/8206856f8f363cd042a8315dc86b3912f5dcb6d3c66bbb24b69c2bfb0775/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", size = 551478, upload-time = "2026-10-04T16:31:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/41/6b/36211f1bb1f0b0313f496d92f5905b74ea107f27cb16fa3355a82f04575e/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", size = 615173, upload-time = "2026-10-04T16:31:05.281Z" },
+ { url = "https://files.pythonhosted.org/packages/35/77/cda0c4a6f055446b692f0ed5692f73707cafd3dff82672ede31b1a9b59de/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", size = 579653, upload-time = "2026-10-04T16:31:07.065Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ec/d8385f446240aed643b9e92a5055cff3015cc04a13c61f73b2883c478ed5/rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", size = 206498, upload-time = "2026-10-04T16:31:08.989Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a5/71b5cd00e0521e3b6b81828baea368c62b6b700ebdd9554cd7d41ddf12fa/rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", size = 223121, upload-time = "2026-10-04T16:31:10.84Z" },
+ { url = "https://files.pythonhosted.org/packages/33/58/dba857c3bc8221b31b62eb170a3080f4f191de79f047200389b7ed1b06a7/rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", size = 218741, upload-time = "2026-10-04T16:31:12.592Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/d0/320ab28ccc1415eeb509d68682b0014fb74690cd49f1c2d29a232475af50/rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", size = 349719, upload-time = "2026-10-04T16:31:14.48Z" },
+ { url = "https://files.pythonhosted.org/packages/56/88/f5b12f1358f443c08b7ce3cc8391d82d335f4872580e2e097847fd36087a/rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", size = 343267, upload-time = "2026-10-04T16:31:16.827Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/0c/c765b0059d532acb3b9c45d781ccc22f15a96dbe443d00903f643ba9df10/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", size = 374555, upload-time = "2026-10-04T16:31:18.931Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8a/cafddfda77564a10cd21184640c3bffda6a2b8d20972fb5dedd5e0166328/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", size = 378346, upload-time = "2026-10-04T16:31:20.75Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/01/5e626016eff72c183bf6c96539240cace15d402468647a453ec08415b2fc/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", size = 492373, upload-time = "2026-10-04T16:31:22.614Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/9c/15a2469e9389242f46896b3f0a01d68caea8a5a35c011fcb05ef333aae73/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", size = 394371, upload-time = "2026-10-04T16:31:24.768Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f5/c100ff77e1e6366e947c75969c258fdfc4b7bc5bbfe7351e62ffbf2a1228/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", size = 375132, upload-time = "2026-10-04T16:31:26.588Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/f4/fe0269c9de253e99c81cabc12b8971a5feaa083debdaff1221e06264d9e3/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", size = 377642, upload-time = "2026-10-04T16:31:28.677Z" },
+ { url = "https://files.pythonhosted.org/packages/05/65/b34a7b257baccff8f4a24a722933166d4941d5ebdc9c3f4bc4ffcd5ce4f4/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", size = 400855, upload-time = "2026-10-04T16:31:30.802Z" },
+ { url = "https://files.pythonhosted.org/packages/98/32/844e54176b6071b90b38a564e6940bc6eb8f97b2890dc709c19db9dec0f4/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", size = 552573, upload-time = "2026-10-04T16:31:32.709Z" },
+ { url = "https://files.pythonhosted.org/packages/17/73/6041d20729dffbfdf155c02d65be58bc225a1c1fb548fd87c23ef306138f/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", size = 615840, upload-time = "2026-10-04T16:31:34.565Z" },
+ { url = "https://files.pythonhosted.org/packages/af/9e/418094adaee6b056ce199051b255448ed872829051e341b2294c80da0977/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", size = 582764, upload-time = "2026-10-04T16:31:36.665Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/9b/1698ebf6b840ddfe8b472198abbed6ace35c6e398faeecd6c47dae742a4e/rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", size = 203795, upload-time = "2026-10-04T16:31:38.53Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.17.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/0e/3d154514b5bad104f7e1c13169c2d09aa9959fccdc06eb47610ccdfa83ed/ruff-0.17.4.tar.gz", hash = "sha256:f32d178b50e826e00f6c6c1be3f56333068acb9b67fe167143527fa9b81e890d", size = 5357750, upload-time = "2026-11-05T18:52:17.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/61/c83c78bbb6108f6c6270a106cf12b54175f4f42da088b9bb880f18e31289/ruff-0.17.4-py3-none-linux_armv6l.whl", hash = "sha256:2b08c4ff98dc234f4a3871a3105353b922fa875ea1073ca31001eb8c06bb4d89", size = 10252195, upload-time = "2026-11-05T18:51:36.594Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/12/dd158708378866e8b53e145a465dbc51a7cccb9fa197a74d67eaedde7234/ruff-0.17.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:13e781c3c5f663e7fba737eaaeb70dd943dd55171daf316afb25cd5268cfe0c7", size = 10266044, upload-time = "2026-11-05T18:51:39.502Z" },
+ { url = "https://files.pythonhosted.org/packages/28/32/89a94a8eb11fd505a381ae6f04e190d038d9958695209dc1786ce5cfcda3/ruff-0.17.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ed2f416349944f908f9574215994602242b0836844eba48eec61473033afca0", size = 9432227, upload-time = "2026-11-05T18:51:41.968Z" },
+ { url = "https://files.pythonhosted.org/packages/58/de/a392dc38b215798ae72e639d228b7f6c02d826a95d1c0ad7c4c0ee49ef5b/ruff-0.17.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ea6e0017c785f7282092b11e46338e4ebed1b9e575ebf2e55a8a1eda968241a", size = 10136900, upload-time = "2026-11-05T18:51:44.562Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8f/a880f60f42005f6ffdf58f77af34c7ee327000b5607f48a2f6a69d8b242c/ruff-0.17.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e061dcc0933c7031a616ac8d66ac540309b2aa21ab558101a8f53e9ac7a1185", size = 10051245, upload-time = "2026-11-05T18:51:46.7Z" },
+ { url = "https://files.pythonhosted.org/packages/09/55/98844264108af7d5ec27ca3a9cd5a989094ca657a14599c4b69a537a0e9a/ruff-0.17.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e95a1515161138be3cbd1b06c06e739d8feb38ade35564556999fffc3686e", size = 10704341, upload-time = "2026-11-05T18:51:49.228Z" },
+ { url = "https://files.pythonhosted.org/packages/28/a2/5edf97659c3ac7c73f978003bd314936f671037fdac6b978e9de3e4b2b65/ruff-0.17.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f28cf0dd37cb172fa5a8d2e92e6ada3a9dbd0609f094ed24039929eef0b7524", size = 11394465, upload-time = "2026-11-05T18:51:51.534Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/8f/51371ae856b2bef4d15a044a574695fbc0f6ae63aafa42a2a22428070fb6/ruff-0.17.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b21c771b296888e8a35c6f48555f4b44e4483f61675d6ed51810a5203469ce6", size = 10988262, upload-time = "2026-11-05T18:51:53.91Z" },
+ { url = "https://files.pythonhosted.org/packages/af/1b/62874f14c7873f27c7887d2b1ea41de990e14ba9518801ffe8eacb5d51a6/ruff-0.17.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cd025509b2e15721b426fc70fd946d71f43b3db3b08e946dfec2d696d911690", size = 10572829, upload-time = "2026-11-05T18:51:56.129Z" },
+ { url = "https://files.pythonhosted.org/packages/98/30/860b9dae67432b16b88558a9cb2d3194825fa62838f8b9cc49f5d9022095/ruff-0.17.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:49b77f7b3921669466882a4881168801bdb06ec2cd83fb936049016cf5fdece1", size = 10617545, upload-time = "2026-11-05T18:51:58.172Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/08/781a4b41a1c83fd6e30fea610d6502a6f94e49cf94693fd37bdcbc6bfe11/ruff-0.17.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e3b84eb74f260d1c2dbd236fd9d4c011d4fdd0a52bdf8aea57a10e3a1b71fca5", size = 10111955, upload-time = "2026-11-05T18:52:00.59Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/e9/94b1c7b677120f30839254518924f73a8157e9bcfa200edcd5796fecdb99/ruff-0.17.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:20e43d7a08e712e33067ba887e6574546c79bd4831596ce8f16803ac7ff9ad90", size = 10058355, upload-time = "2026-11-05T18:52:03.61Z" },
+ { url = "https://files.pythonhosted.org/packages/27/5f/a825910b9f309336dc33cb8c0774126662c1d2f765699b3e0a1bd538d01f/ruff-0.17.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0d1f4848fd2ec0499be4046db9b7dfcc952666885f00233cc7033515282556a8", size = 10415932, upload-time = "2026-11-05T18:52:06.264Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/b8/0796468423413ea4df1a6e2442ae26cf49b8f7dda0eecf246e0d5d0dde54/ruff-0.17.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6007e7f3a0e55b0fbdb947af69cf24c21b4928df6be8ef3e767c69b5ccccf496", size = 10826033, upload-time = "2026-11-05T18:52:08.675Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ab/d8e407669ffe5014429baf780c2df47e001968a6e4c17f615e2e7b13a9ba/ruff-0.17.4-py3-none-win32.whl", hash = "sha256:3971a78cba76298b284c8321e6d40d93045ed348bec5c369d85099efc97fb7cf", size = 9931993, upload-time = "2026-11-05T18:52:11.169Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/15/42af8a72d8946e3c9efd2bbf6641e462b6832783b0d043d63ef5b280ba7f/ruff-0.17.4-py3-none-win_amd64.whl", hash = "sha256:67c5a894fc054cf41f75f7afa24815de5a74329d97634126c2a1015740ff4e8c", size = 10292922, upload-time = "2026-11-05T18:52:13.436Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9e/f1dbffcfb8ff25dadbc21f87a8b73c3848abe4abada9d4b08ccef8bec40c/ruff-0.17.4-py3-none-win_arm64.whl", hash = "sha256:181ae38a9a8fb94925bf7d0794327b48d7fd7f4b50cfd84c0e999cc09ef8d131", size = 9889396, upload-time = "2026-11-05T18:52:15.549Z" },
+]
+
[[package]]
name = "scipy"
version = "1.17.1"
@@ -459,6 +2522,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/26/d4bc2ba3427a623f79a6c10c8f427c7a55b56eb8b3eddc369319d97f741b/scipy_stubs-1.17.1.5-py3-none-any.whl", hash = "sha256:58ebf054a86c000c72e8982e121c4ead0d3d9ba7a6c38aa5fa71b07f96a427fd", size = 607388, upload-time = "2026-05-25T21:34:32.073Z" },
]
+[[package]]
+name = "send2trash"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -468,6 +2540,155 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "soupsieve"
+version = "4.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/7c/a64eedae0b7776cb9602d2da9cb8f8caae8e1b4c1da5e8e87bf1e68121ec/soupsieve-4.0.1.tar.gz", hash = "sha256:7621e3c5f6b371ed2c5eabaac5de300ac3d66d05e6d7f34783e31b63c4f90c75", size = 153302, upload-time = "2026-11-04T16:36:27.749Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/5c/4f68370847b561a389dd9a2d7eb3d19fd8cbb8acbae543a0c6b8d0d9700b/soupsieve-4.0.1-py3-none-any.whl", hash = "sha256:2e68cf7dd5a0f29d81f9d485275ccf294e6475f615e211895d91cb81f8614f04", size = 50688, upload-time = "2026-11-04T16:36:26.158Z" },
+]
+
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asttokens" },
+ { name = "executing" },
+ { name = "pure-eval" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/bb/3d8757f21e9a53d9035c9eb039a777047139a16bfdbedfc7a58241c3d914/starlette-1.10.0.tar.gz", hash = "sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b", size = 2734356, upload-time = "2026-10-24T15:04:12.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/2e/947cfe02def9371c07cd5bf5b6b61c829e9aecb9904db02a66cfafbc1eec/starlette-1.10.0-py3-none-any.whl", hash = "sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4", size = 80571, upload-time = "2026-10-24T15:04:10.371Z" },
+]
+
+[[package]]
+name = "streamlit"
+version = "1.67.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "altair" },
+ { name = "anyio" },
+ { name = "click" },
+ { name = "itsdangerous" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pandas" },
+ { name = "pillow" },
+ { name = "protobuf" },
+ { name = "pyarrow" },
+ { name = "pydeck" },
+ { name = "python-multipart" },
+ { name = "requests" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "uvicorn" },
+ { name = "watchdog", marker = "sys_platform != 'darwin'" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/47/ea7596bda2d303370b6129688e09a44e5faa64e5b9d86cba4d89ae60a16f/streamlit-1.67.0.tar.gz", hash = "sha256:58159dae1199212250bfa5fbba0740996129003266c2c37d465151ef76c48f3b", size = 10672027, upload-time = "2026-11-05T19:53:54.821Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/c4/59f60e52e8f679488bf0caecbe3a8469af1d3d72c352bb0f306915a3e667/streamlit-1.67.0-py3-none-any.whl", hash = "sha256:f29da87c443bfbdfc52738616e3d65745d438e718da8faf69e2be0da1e52ce64", size = 11235773, upload-time = "2026-11-05T19:53:51.688Z" },
+]
+
+[[package]]
+name = "terminado"
+version = "0.18.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess", marker = "os_name != 'nt'" },
+ { name = "pywinpty", marker = "os_name == 'nt'" },
+ { name = "tornado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" },
+]
+
+[[package]]
+name = "tinycss2"
+version = "1.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "webencodings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
+]
+
+[[package]]
+name = "tornado"
+version = "6.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/02/5f549b1ad5da6267ed1e1fac2eef69257b1979cc812497002499b7895ff9/tornado-6.6.tar.gz", hash = "sha256:34c4aa916d7f2b2d5cadee2c6850f5a4a7f3d688cbf58399ea9bd3057af4abd5", size = 617998, upload-time = "2026-10-21T15:31:04.733Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/f5/d3e6f76b945f9812842db742d0708a3b6287e850e81be3ca10e290c103c6/tornado-6.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:01abf15429d64f04ccfafc238b191cc2c177108b7a331f4461707ad77198c8c2", size = 528326, upload-time = "2026-10-21T15:30:05.528Z" },
+ { url = "https://files.pythonhosted.org/packages/30/69/b20cf3a8bf641ec792a5a6050aa153e8f120604f9c23471142de535113e5/tornado-6.6-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:11bb97c00981eb8254e55bd6a5ad960c983ae343fef8ae3ae5584cd1fb70236a", size = 529544, upload-time = "2026-10-21T15:30:07.126Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/f4/d9d8d3ac345aa9df3a132ad03bd443160c2ab24ad4003b4e8d6d5af06371/tornado-6.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e152a3b2d3eae113b836d979823f7bb2320f7e334a84449729964921f374a4c", size = 530523, upload-time = "2026-10-21T15:30:08.478Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/82/1296ade1c14dee5a4906e5ca092997622ece7c4dc768c68ed9caaf895d1b/tornado-6.6-cp311-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e912d4c4babd04db6f34b25b748174a4a176debe86c8610beedf1815f9af7fdd", size = 530739, upload-time = "2026-10-21T15:30:09.98Z" },
+ { url = "https://files.pythonhosted.org/packages/66/30/6ac1eab66c81af46dafd93c9b24bd311f82e199d9d7b8ce2a64a91b6a200/tornado-6.6-cp311-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33aabe8a03d426cef4e0fe5669f0643cadd983b54957f1a7ac2789ec326e9644", size = 529738, upload-time = "2026-10-21T15:30:11.498Z" },
+ { url = "https://files.pythonhosted.org/packages/24/b4/6c1f6aa1fd148e4d0f500a10cd0c9949e7bf385e161ff497c81de5b3ae4f/tornado-6.6-cp311-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9aaf0e1c2fb9f046f30e2036b8d2060666399e8464d0a72769e43c754089567b", size = 527908, upload-time = "2026-10-21T15:30:13.298Z" },
+ { url = "https://files.pythonhosted.org/packages/54/5b/67d3d452b1179bc9e265cebd381a56ab4cf2d7b2a54607aedd8d6e1ebc2e/tornado-6.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61fc5513cea97720ab9bf54b04827ac0f4500c63f0b072b963d6145c798b4661", size = 530367, upload-time = "2026-10-21T15:30:15.183Z" },
+ { url = "https://files.pythonhosted.org/packages/74/14/4e8e9a14f77bcafb2408634b2d4c8c676ad7a43bf8dac4ee2d48370698f9/tornado-6.6-cp311-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:31e1a0d5cd704d2fe8a008c1cc27a0e2b67269ca263630a27985dddc160b5537", size = 530456, upload-time = "2026-10-21T15:30:16.741Z" },
+ { url = "https://files.pythonhosted.org/packages/56/12/e3ddef99a2089ccfc86f32bd3f39caaad87bee4868be1bacbf213a0b07a2/tornado-6.6-cp311-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:74416e41e9bdf70fbc2fb242f6fbbc7a58ec11e59d39f15fc5a421ecb6845118", size = 529610, upload-time = "2026-10-21T15:30:18.031Z" },
+ { url = "https://files.pythonhosted.org/packages/35/3d/62a692343d46b37e852e5078e0f6513534d925f70a04580072e3c127b129/tornado-6.6-cp311-abi3-musllinux_1_2_s390x.whl", hash = "sha256:8cdd69104ae9d705b3f04d2563aa96e357043e267a455802daae9a1e633a37c8", size = 529803, upload-time = "2026-10-21T15:30:19.326Z" },
+ { url = "https://files.pythonhosted.org/packages/49/04/6c59a9413adfe4810f648a4dd9fdf476f13b0495f9cc25ef856b71f9ce1e/tornado-6.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fc7c8a3263501fbe97518cd699101bc3c5f499a7dd4f75fc6cbb3bd9ee1db1f1", size = 529751, upload-time = "2026-10-21T15:30:20.826Z" },
+ { url = "https://files.pythonhosted.org/packages/31/36/a1b3b84cd347ae43f982d3bf95628de6c6365668dba2543af47c6a1c888e/tornado-6.6-cp311-abi3-win32.whl", hash = "sha256:b54ca69ff93d0374efb083a12a1e9ec2dd2036e8496d197e0e8ef77b8d9135c3", size = 530643, upload-time = "2026-10-21T15:30:22.428Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/50/dc813fe35a672e14ee8bed8b5c634eeb343a05c98eb455318a169de24567/tornado-6.6-cp311-abi3-win_amd64.whl", hash = "sha256:48f26ed9d7beb64e5f06adc864524e581ab6d62ba8ff46eaaa1ac2d0e6d753fd", size = 530886, upload-time = "2026-10-21T15:30:23.975Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/71/62a9b9e980026d38874740d0a7c162f53eb0563f6a441e3aff84c48db7e8/tornado-6.6-cp311-abi3-win_arm64.whl", hash = "sha256:29114ffd4e4de869d390393df9e9a0f191065aae1a96f38a58d35e464c04a867", size = 530899, upload-time = "2026-10-21T15:30:25.264Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4c/633e3568eb574df555d6ddd2dac44b7793a5c26cd63b444e653fcfb1d6ec/tornado-6.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:63fc8d243b52fd8dd9ed4ad02d0df08f2024f9aea632a433a8a6c91075076d44", size = 528432, upload-time = "2026-10-21T15:30:26.578Z" },
+ { url = "https://files.pythonhosted.org/packages/37/eb/b525d1870ad28a56f47b664dd8fd3fab4f7ddd237c87c14e97248bf4762f/tornado-6.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d4b85cac795bfc6a7f20ba81871abb8c2ae8fba019061b8a9ae1c2f97e29637", size = 532586, upload-time = "2026-10-21T15:30:28.027Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/35/909a33c253352346ca081a27ca66b45c1398468adf784b767597ca773c28/tornado-6.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e035c065edadfa696529d05eebcbeb25154c129b5aee6dbfb697a6abf5d4f697", size = 533641, upload-time = "2026-10-21T15:30:29.318Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/64/100202666d6e3d96288ab184827762415456c139798a15c962547f9fabf9/tornado-6.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b94e589ab855090c448b5c0937b3a75f1c1a604b7b36207e8236d209f720b9c", size = 533869, upload-time = "2026-10-21T15:30:30.882Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e0/af3ed62bc15830d60328d5b4a1a9076b35ea0ba142cb17b1c1772cb15c59/tornado-6.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a394c8c9db9137f2a2b42869d9f7b91398fcf19da120d0dde4c4546172710f7", size = 532767, upload-time = "2026-10-21T15:30:32.273Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d7/1e2e1a6932eb62d63324b64dba2f62e6eaf79ad883dd209352c297b7848c/tornado-6.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0215d1df6e4b5e736ddad5adf0af4138cf7437fdd39c77c5e69768e37850a632", size = 530956, upload-time = "2026-10-21T15:30:33.746Z" },
+ { url = "https://files.pythonhosted.org/packages/70/65/3b9a0e53d7a4c39f2ebe95486540fa8eeb67bfb6e8c437d603c00d35b128/tornado-6.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d711790e0a9576ad924d942c4607e4e401d697588ea6f30f6662824008a7add", size = 533372, upload-time = "2026-10-21T15:30:35.085Z" },
+ { url = "https://files.pythonhosted.org/packages/44/48/90979cbfaa3ff941324a2605eb794af0f087c670cd99f18a69ab40498073/tornado-6.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:034a66bae0f55421d007803e2385d2a59707d8829b2f026373d42f08256e9922", size = 533425, upload-time = "2026-10-21T15:30:36.425Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/c4/ce743a86d91f85c255b77a4282eb5b2716b61e74430eb9d1df8578a539d2/tornado-6.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5cb38483b077129056a94dd415a71d29ab4e42b46da894fee8b4657003f10b21", size = 532545, upload-time = "2026-10-21T15:30:37.789Z" },
+ { url = "https://files.pythonhosted.org/packages/02/5e/a82131e35778540fc19139229d02aa8cf2453a8022e202be50c88f0f8ac3/tornado-6.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:46bbbd107efa37777ac4b83601866d19b4dbd5b770a7a9123582633d95959ad3", size = 532700, upload-time = "2026-10-21T15:30:39.336Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/09/6dac8c2c677c65d60b3d7dae94176dbfe392f314e7c8e2d709623edd6a26/tornado-6.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b85e9d0263cdbc027b95947c8b873a7bc7fd6a08d16bfb4f5047350b0808e464", size = 532712, upload-time = "2026-10-21T15:30:40.698Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/ac/71e5776cbcdc3264e81aa6db28c69717ba50607a9eb8d5b766f11a4b020e/tornado-6.6-cp314-cp314t-win32.whl", hash = "sha256:4276eef0880e5fe4488e6799c2842f59af3b06637e9313ab4fb506f9c9c2d6cb", size = 530491, upload-time = "2026-10-21T15:30:42.115Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0f/0b6a6908a5b5aa8320009dfdb84434b1cf160b577a00456e59ab9e6ea598/tornado-6.6-cp314-cp314t-win_amd64.whl", hash = "sha256:10d4af98c6df09f8429b1340ccaea3a72887b86f695dfb8056bda517c8115a00", size = 530780, upload-time = "2026-10-21T15:30:43.414Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/39/c152e1cc5371f75f5f9f0a4f44bf2dc9396be9407d384de7428446fb4651/tornado-6.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b1f0ef20a5278854f5fdac31637bafadb055405df28b7fc945e54add12b172e3", size = 530781, upload-time = "2026-10-21T15:30:44.741Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c5/f96c7d7a8895745bb37dfb6e6107e1ee40ad02a4b162d7d6c143644455ab/tornado-6.6-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8f8e382da30fe042f27f27a871e21aefc2613870e5a41648de18446c72d6ea59", size = 528446, upload-time = "2026-10-21T15:30:46.112Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/dd/ab8f6500d274b9c55b025798eb54e92591bcff27515799bec443bd54e35a/tornado-6.6-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8c7a8a328396c2195454a2ecd9499512441bbce99505544a717e6a87d16ff6f9", size = 532774, upload-time = "2026-10-21T15:30:47.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/23/b78e7d1bf329160b8157284005395421ea941864c16c7f04c3e48c98bcf9/tornado-6.6-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca025bb0da81c294aef26f27427316df1525220820315f4bf32dc1e84a4d2232", size = 533872, upload-time = "2026-10-21T15:30:48.884Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4f/ab66b614ad0bc9a219484be6bc2c002c8de9916a7b6c7b27fe6273838e5c/tornado-6.6-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:59c22ebc905ac2a267f8d9d8a878f86afbcbbde329d75c485f1422fc45a81b83", size = 534027, upload-time = "2026-10-21T15:30:50.157Z" },
+ { url = "https://files.pythonhosted.org/packages/da/63/501fa15a36bace2f4b9158228cf922d606403261bf71c960c433ec3ca0a4/tornado-6.6-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f89802bc0bcd15f988d9325b4e6d2171bb04ea68d188732556335e10d3fd00bd", size = 532936, upload-time = "2026-10-21T15:30:51.45Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/b7/82d643f635f0300bda98613ca2660b4f3134b6fb594c6fddfa4de706806c/tornado-6.6-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:70043256d5958f9c655066153d23d60ea3e29bf9854f7fb75850e223c477758a", size = 531254, upload-time = "2026-10-21T15:30:52.865Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/53/92c40891a94750bc09257310af964bf25c59e3531d6199736c318baa864f/tornado-6.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:82b49495e3849d644b841ef33b35491160055997deaa93720ff647ef019421db", size = 533566, upload-time = "2026-10-21T15:30:54.15Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/88/b82f5c5efde731b88d839ad99cb6ace2a521f03bb6d7346d2de089c76da8/tornado-6.6-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:07b2bcaa4bf447bdbfb664e4c28344226540db3f0ebccd74886528338a240765", size = 533567, upload-time = "2026-10-21T15:30:55.451Z" },
+ { url = "https://files.pythonhosted.org/packages/02/2c/e9a7f5d0fca0bdbcd48b2d9c7884dd0b32669c9945d98bc6d303a80ecfea/tornado-6.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c36b921374c302e54909bae17d9f354061d80e7ae7d8c94f73abbcc217866460", size = 532849, upload-time = "2026-10-21T15:30:56.729Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/f5/4ade737340ca6ce9c677ea0c2843591c8ad065d07f0913c28d922c7e078e/tornado-6.6-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:7412d9e37aa9f7048e4b591f520a95f7a097278ffab15f1ddf3f5c2320cf6648", size = 532873, upload-time = "2026-10-21T15:30:58.067Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/04/6acdc937d0bb6189ddb34ceded517d7bd1c7b3bded5d4e1b3442160e6b6f/tornado-6.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:15bc95112b2d24b0bfaee9a004e3f974da929e33b4244a7f695a22f45b08a5e5", size = 532883, upload-time = "2026-10-21T15:30:59.395Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/27/0a766fac6b1cc629d96346329a0485287fd2857c51a887a3a154f2a69938/tornado-6.6-cp315-cp315t-win32.whl", hash = "sha256:f661c21117b875d612371f3eb243d0c69f56d9bdd6aee38b7c24453419dfe554", size = 530488, upload-time = "2026-10-21T15:31:00.77Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/80/3e4142570883da27b3f60ae82c638ac96e60997eac684740613557519c96/tornado-6.6-cp315-cp315t-win_amd64.whl", hash = "sha256:ee8853ab35da510dc952879ebd9995c82bc10ffb7925fd87930dc0d6916f4b39", size = 530777, upload-time = "2026-10-21T15:31:02.087Z" },
+ { url = "https://files.pythonhosted.org/packages/17/57/4a85c617b6349962320b4afbe1386d5b05fd57218ee8ad6e82e0110ec9ee/tornado-6.6-cp315-cp315t-win_arm64.whl", hash = "sha256:7de62e2dfdf559fd7154d2b9aa58ff3d9cff8304ae0519341feb082618bc2a33", size = 530780, upload-time = "2026-10-21T15:31:03.425Z" },
+]
+
+[[package]]
+name = "traitlets"
+version = "5.16.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137, upload-time = "2026-08-03T08:32:36.848Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -476,3 +2697,258 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
+
+[[package]]
+name = "tzdata"
+version = "2026.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7a/29/6a1391d31cd00ff9e927215a224a880e1e84e826b64ba80a499e51c074d8/tzdata-2026.6.tar.gz", hash = "sha256:85d2cd9c09f81f0bb29db27759fc83af4f1e620efb814f8c5bf8b4cc4feece6d", size = 201017, upload-time = "2026-10-28T09:57:09.137Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/66/9a06c2227fedfd4919f0eaaa8b94377d9730882a7e4f95823c9a331ab5e1/tzdata-2026.6-py2.py3-none-any.whl", hash = "sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec", size = 348210, upload-time = "2026-10-28T09:57:07.276Z" },
+]
+
+[[package]]
+name = "uri-template"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.54.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "21.15.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "packaging" },
+ { name = "platformdirs" },
+ { name = "python-discovery" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/bd/e40ad2df6a9efc1f859ea23d63fd7371cff68252833f182ca9af819bb4d3/virtualenv-21.15.6.tar.gz", hash = "sha256:c8b65c7d206ca6ef18de9352c160b3f04f66fff9f6b0ec8fd31f14688eba52c4", size = 5641659, upload-time = "2026-11-06T18:17:32.46Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/63/fd514dee3159f74a96ac01beb8f460b65e4ef0b972f63f4fe9f0cd2aa0e3/virtualenv-21.15.6-py3-none-any.whl", hash = "sha256:4b99ab9b95ff11043939bb4cad2fa0449175fa5a478372864f62160cb6c1a7ed", size = 5680963, upload-time = "2026-11-06T18:17:30.128Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e2/55/087e45c2a3ed8223bc9f30d6b65f051de35513cfc30c989c2b1a14bf862e/watchdog-7.1.0.tar.gz", hash = "sha256:d3861e255416f49a1384474cb2efa097de165fb330d4c5432ce6708b509ed922", size = 218748, upload-time = "2026-11-05T09:33:21.28Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/8c/a25dac9642a4962d70cace9ffbb9ea4e575e6a61412ba5c80569460e0c81/watchdog-7.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a0fdb62a48b5073ea3bcca29b9910f1cbde1026ab8bffb87a69b112bf6d97d1c", size = 122360, upload-time = "2026-11-05T09:32:39.85Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/56/b262a7c5acb47cb7eebc0cb25ded697ac4fbfc2d65c3a505a210ecb2b2e3/watchdog-7.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8b637a1525d7e23548419368485bf1de931aca4c9e132e89a41bad52ff435fca", size = 113003, upload-time = "2026-11-05T09:32:40.873Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/5f/5133b328bae8ec5fab02c329c4364445459d678194c42457c9721b6f64ea/watchdog-7.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1a669c90ae6dc734f2a5a417f15ba30d5ef41d890bc28aef161d51bb62b7882", size = 113518, upload-time = "2026-11-05T09:32:41.924Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e1/0de47b185b82317696f59c0f0b9bb105f6492d6d3423cb1535ce60f70c2f/watchdog-7.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4332ae52b5e530d2c25a8525d765f1d34e595cfb07096c6b6368f9da10b5a8bf", size = 123108, upload-time = "2026-11-05T09:32:43.453Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f7/84b94e50c73479a291733027bd572336ba5b85dca27c18982186293bd8bd/watchdog-7.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:701586db59c9c421389e2e007713a821d934c3d83d034f0cafa95516a422758d", size = 113369, upload-time = "2026-11-05T09:32:44.689Z" },
+ { url = "https://files.pythonhosted.org/packages/12/71/a6e482e81cb7ebe9f2aea7955254dee2865ea99a580bfeceaf53c274875d/watchdog-7.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c9804d3db8e18659cd7cd968e2dbb31a655d12fcc07ec54df5ae47ff929f50", size = 113900, upload-time = "2026-11-05T09:32:45.742Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8b/aeeacda90fcbcf2c8c3eeb761f9dcce798b0209c4413e9f89733d4bc8c8f/watchdog-7.1.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:a78673ab08b15f42760ae036f9251d02ac4107390c074963ccad4fc91cf880ac", size = 122415, upload-time = "2026-11-05T09:32:46.9Z" },
+ { url = "https://files.pythonhosted.org/packages/66/07/69e1962eeccd885cc6196fea3f45f610741ca259ce9f69f372aa42e35146/watchdog-7.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:b0747551092578b70a4e8c48260ab956a9aa7efd2e9f7614471a7f73ada91c44", size = 113001, upload-time = "2026-11-05T09:32:48.023Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9a/3beeb0a20d9907468b48bdc63e2e0e5ff858b308c46051a823280164bbcf/watchdog-7.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3dcf1a72c8c83a945e99965746c818bca3f50c0835b66ef1deb388d89bae792a", size = 113576, upload-time = "2026-11-05T09:32:49.407Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/f6/d552bd549125010e7c7d78fbb8057ee4ec6f5d258ffdc8cd6c675f9a70ef/watchdog-7.1.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:22fb8c80a2e510ec7773e4f6b2c6db869c2aed80bfb09962feb0962eb54f3ac9", size = 123152, upload-time = "2026-11-05T09:32:50.724Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/b951aeea6d6db7a917e3c1c5a341cbb3e5c8cc02ccb9201b2715547603c3/watchdog-7.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e226f1797764c3995ff9e91ce46f1b2ae65f59ab231785ef2f62243f06ba7956", size = 113366, upload-time = "2026-11-05T09:32:52.04Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ce/9e944c1d21b9b767023fb63cc764affcff1c8f4853de8e0ad14005cadbba/watchdog-7.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2dc548262230493c8740c50cb0016d5983143ef3e587284d55f8a9fac393340f", size = 113951, upload-time = "2026-11-05T09:32:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/5b/1e62e5ab365634f84afe91f8fd78563c71226056d57e36d5fcba9d99b536/watchdog-7.1.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dbd4e701a31b9bfa686bd831177b433695f5e4dabee6aa0bdf4efcf6f0fd5b9f", size = 103268, upload-time = "2026-11-05T09:32:59.401Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0a/1da133041dfa1ca8d512e06485ce82dd2a9af7b6cc4fe9c277008380d10a/watchdog-7.1.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d47f3067eb77a83d04de1cac214284da604f960eaee9816d7ebfd01a165b4175", size = 103269, upload-time = "2026-11-05T09:33:00.478Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/b4/3c9142add82c134fcbb748cc23e52603519679dddc4b5900d83e473d0648/watchdog-7.1.0-py3-none-manylinux2014_i686.whl", hash = "sha256:400788184d62c0e384da67de4d7fcb892bfeb064b2e59a7d1a1dccb9ff4b49bc", size = 103267, upload-time = "2026-11-05T09:33:01.522Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/d3/dbbc79e5ce139516cad0b88ec08df27fd00ef478547c0e584951f2c687b3/watchdog-7.1.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:b76e3ac088550b33046c4eabacdd7dd4cb9db9147c8749b202643a4b5c2433c8", size = 103266, upload-time = "2026-11-05T09:33:02.614Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/ad/721ff870f9ecf25532a07d58fb677764e17390a46b675486bf23ce278873/watchdog-7.1.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:d94d0bcf30a9ea5ac5387df290b9e175c04747558c33443d2b0f763471699531", size = 103269, upload-time = "2026-11-05T09:33:03.694Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/4c/dfe0265c572027b2af9e824c6370a6ca39da33c819a99af48ddbc0cf1706/watchdog-7.1.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:cbae4a087f712bac4086e653273a949f86e2f908d16f157fd3568217e0c07cbe", size = 103268, upload-time = "2026-11-05T09:33:04.738Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4d/60e1c38c27973d63f8748cfe8da70bdf698d1c1ab0e94fd18de15b3044fe/watchdog-7.1.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:77f06d709fe5d766ec51ec07b79773f5e29a0b3c4c3d195cfc7aa1e909fa566c", size = 103269, upload-time = "2026-11-05T09:33:05.95Z" },
+ { url = "https://files.pythonhosted.org/packages/72/88/9190129165760c76191f4711b64a728d1bff02de74649c675797ae253f26/watchdog-7.1.0-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:48db8dd8f9f92359ca5d4bccbd82be814facc3f57feaa3c67be964cc86b1dc33", size = 103270, upload-time = "2026-11-05T09:33:07.48Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/6c/1c3a9066266b5c3f0db9146422f67e5d13781b2fb1b9e777ead5553fc1a7/watchdog-7.1.0-py3-none-manylinux_2_17_armv7l.whl", hash = "sha256:a47d6e1418d7ed80f69454fb7f000ded554363209f8b04b812bf46b6c9e3093f", size = 103268, upload-time = "2026-11-05T09:33:08.639Z" },
+ { url = "https://files.pythonhosted.org/packages/40/6b/aa8fb28263d3965f91228ce9b251a786e36060a3571f4d724b2fab44a1c1/watchdog-7.1.0-py3-none-manylinux_2_17_i686.whl", hash = "sha256:71b0ebfd5b3ffe7cf53143f2ab63fa5038ca190fc10782ec23202765a859b057", size = 103268, upload-time = "2026-11-05T09:33:10.041Z" },
+ { url = "https://files.pythonhosted.org/packages/63/bb/8757c54c6dcb0edcc514a47b0dc026f8751a266b6c320a0df8830d9f89a4/watchdog-7.1.0-py3-none-manylinux_2_17_ppc64.whl", hash = "sha256:c71bd120d57b16c2c49abd55d74d3c83f984e19024249ebe7ba4dc4903eb27ee", size = 103268, upload-time = "2026-11-05T09:33:11.147Z" },
+ { url = "https://files.pythonhosted.org/packages/79/47/bdd6c0176695fb5b44fa9164ab5aa3edbefd322eea7dc584073fe7bfe67c/watchdog-7.1.0-py3-none-manylinux_2_17_ppc64le.whl", hash = "sha256:a17f37720ea088430754a7cc5869fad93915c69eaff8d5284adbac41f1befaaa", size = 103268, upload-time = "2026-11-05T09:33:12.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/99/13406d1424ed4a8796a800016d21ac6fba3ba9ac508cc860e97ce352f14e/watchdog-7.1.0-py3-none-manylinux_2_17_s390x.whl", hash = "sha256:89f78b2b1c153715463e27fc86f0f95fd4dd6cb07a2cdad932fe01840101fb5a", size = 103269, upload-time = "2026-11-05T09:33:13.414Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/28/96130c970c236e4278a188cb6b8dec2500ff395ddcebb74c6171f5fc6069/watchdog-7.1.0-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:ad6d0b473cb8cde0721692b22b30fcbea8b52102430528874b3593549888bd81", size = 103269, upload-time = "2026-11-05T09:33:14.916Z" },
+ { url = "https://files.pythonhosted.org/packages/17/54/312036da06d83b4a21d25a0d83643d2a9b2188eb2e5031e52fe82eddbe0a/watchdog-7.1.0-py3-none-win32.whl", hash = "sha256:9c937139c8afdb26405639309956352ef72c453006e772e0cf5c0bbc0d1cba82", size = 103255, upload-time = "2026-11-05T09:33:16.394Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bc/c04fbb2d6366afb0850e39c6d848f2d38066ff6dadccc886b3cb12c39e32/watchdog-7.1.0-py3-none-win_amd64.whl", hash = "sha256:16a5ba8694587cdbeae4ab8e6434cece994f9cce4e5bab03e61e6d1a0a93a643", size = 103260, upload-time = "2026-11-05T09:33:17.687Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9c/5a39ec8880c08e50f75d90f4e0e27b95dc91a04deb5e6e04871dd4b7e006/watchdog-7.1.0-py3-none-win_arm64.whl", hash = "sha256:5ed4b94365c53ffd51d88e47ca48d6afae02fb1bd849175a9a10927a2fbfb932", size = 103260, upload-time = "2026-11-05T09:33:18.987Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/aa/7a7210433e000f2491ddec7756d312009b0a47847e5c3457ccb03852cfdf/watchdog-7.1.0-py3-none-win_ia64.whl", hash = "sha256:0cfc5a6b3d4e76fc3e18fd371ee79d57945004da4105b589f71bb16966fd3e67", size = 103256, upload-time = "2026-11-05T09:33:20.159Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.9.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/01/e5ac23733628a6bbb91913a765fcf55ee8070d8d991bc88fdaaf805052c7/wcwidth-0.9.4.tar.gz", hash = "sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954", size = 970051, upload-time = "2026-10-24T18:11:44.954Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/b8/d251589a100250b71a7b8e83fedfcabfd8bd60e78cd1e3329372f63bcc93/wcwidth-0.9.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d", size = 612952, upload-time = "2026-10-24T18:10:55.925Z" },
+ { url = "https://files.pythonhosted.org/packages/04/e9/dfdca30ca36c472de3e260406dee29fc108499ec590c63b3fe279887ee9d/wcwidth-0.9.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4", size = 618904, upload-time = "2026-10-24T18:11:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/d7416f38054a107d2fb17c2b73b68723c6c334f69c4e29545fb9e6df81eb/wcwidth-0.9.4-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09", size = 778780, upload-time = "2026-10-24T18:11:17.299Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/5b/42bdf9a4b0e4f9126584af2ec73642de495f3bd8add907430f13c3bb98d2/wcwidth-0.9.4-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef", size = 789012, upload-time = "2026-10-24T18:11:07.829Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/e8/0485850748dfdba9443f4151c7b2442cfa4987ba6cd871038eb5d6b8eca9/wcwidth-0.9.4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2", size = 785946, upload-time = "2026-10-24T18:11:18.748Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/11/dd3d0379c6094524df6585b76ae3d9727161ff29c003177e3661c36437c1/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f", size = 795348, upload-time = "2026-10-24T18:11:09.931Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/48/1b5299983d9ec084be897162b7cc6014622dfe62847224fa3272b6d79ea5/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763", size = 793420, upload-time = "2026-10-24T18:11:20.551Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/be/e3097d859b075344a7be57774a58d468eafb0ca88e02aad93eb31d69f6ba/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17", size = 796192, upload-time = "2026-10-24T18:11:22.552Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/cd/739710c8cf0de461239db5bb6ee678c9a33d30aaa22beff55cc356781818/wcwidth-0.9.4-cp310-abi3-win32.whl", hash = "sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62", size = 593229, upload-time = "2026-10-24T18:11:35.609Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f1/c123f902bc2ff9e9c2ab66241b8872204df87cbb18ecdf035e120860fedc/wcwidth-0.9.4-cp310-abi3-win_amd64.whl", hash = "sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c", size = 597780, upload-time = "2026-10-24T18:11:36.952Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/88/1cb5ddaf5510b918e01aa95fc4b68df91dead784b63e4931037c5673e85f/wcwidth-0.9.4-cp310-abi3-win_arm64.whl", hash = "sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49", size = 598786, upload-time = "2026-10-24T18:11:30.886Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/cc/46a31c12b4cdad04f546a3c0aca5d000d77aa5069ae88b4db24525cc29c4/wcwidth-0.9.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f", size = 613805, upload-time = "2026-10-24T18:10:58.887Z" },
+ { url = "https://files.pythonhosted.org/packages/18/f6/5a8327af9a65153ea9823d6a12fc182f325264050aa25c05a5deadc189c9/wcwidth-0.9.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7", size = 619936, upload-time = "2026-10-24T18:11:04.073Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/d3/bb8a7f4aa9b915e56e09841b07636ba49c82ec987ad3d2022e1696c038d9/wcwidth-0.9.4-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5", size = 794563, upload-time = "2026-10-24T18:11:24.4Z" },
+ { url = "https://files.pythonhosted.org/packages/12/0c/986acadef534688a57502d484a40e0f30bdca2d39514fe0a311f7d575f70/wcwidth-0.9.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967", size = 806565, upload-time = "2026-10-24T18:11:14.297Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/1d/66a94b9ac07037d7cf2c1f1288867c9932215d467893eaa5ec8d548c7f2f/wcwidth-0.9.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633", size = 802372, upload-time = "2026-10-24T18:11:26.275Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/00/1d50b8e8b45a8330b582695bf8196081a609317928a3718cf21b3d14a899/wcwidth-0.9.4-cp314-cp314t-win32.whl", hash = "sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3", size = 598917, upload-time = "2026-10-24T18:11:38.369Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/fc/3e39a33fc8f5c4e07611578710275039c9d4a6655e2be7cf3459401a30cc/wcwidth-0.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab", size = 603536, upload-time = "2026-10-24T18:11:39.922Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/8c/b1f399fd2f8449e42978192967ab6df441d7cf894b10117983c36c062b18/wcwidth-0.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95", size = 605127, upload-time = "2026-10-24T18:11:32.367Z" },
+ { url = "https://files.pythonhosted.org/packages/04/46/839780e772e9068580619d4d691348b7a5f9c4fa5ffe501e522eefeb27cc/wcwidth-0.9.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664", size = 613850, upload-time = "2026-10-24T18:11:00.725Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/66/b2559c7012bbd4268f5b5c0288cb793c78235d354e306871a55893aa9d1a/wcwidth-0.9.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279", size = 620207, upload-time = "2026-10-24T18:11:05.975Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/fe/7d2c39e28924b2430b20099e36276f14e00e75c7bbe4ba5b1ed44d1d465a/wcwidth-0.9.4-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed", size = 797056, upload-time = "2026-10-24T18:11:27.911Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/d7/f4f9967a411e02b89b74df2f49f44977c4badccf29738f3ae6941619e27c/wcwidth-0.9.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d", size = 806169, upload-time = "2026-10-24T18:11:15.724Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/bb/e2b40c6522f3aa1222558487dda3551d297e07e245761b4816f6caeebc29/wcwidth-0.9.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d", size = 803462, upload-time = "2026-10-24T18:11:29.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/f2/ab69009f2dd0d3772d3e07f025fa046b33c60c195e4f2e5f000297d23547/wcwidth-0.9.4-cp315-cp315t-win32.whl", hash = "sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27", size = 598931, upload-time = "2026-10-24T18:11:41.659Z" },
+ { url = "https://files.pythonhosted.org/packages/63/6f/b321f10c2298e7d16cffc1a5f05f3d7e5231b45f9e68af5ee378e23bb2e5/wcwidth-0.9.4-cp315-cp315t-win_amd64.whl", hash = "sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6", size = 603485, upload-time = "2026-10-24T18:11:43.052Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/cc/7eeb28059186cae72a770df79e744f232ef068192a5a588b718473e35bb4/wcwidth-0.9.4-cp315-cp315t-win_arm64.whl", hash = "sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b", size = 605198, upload-time = "2026-10-24T18:11:33.75Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/0c/309b1813b230d1dbbd6f2c2ab2bb21ef68992358977605d9ef289bbfedcc/wcwidth-0.9.4-py3-none-any.whl", hash = "sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d", size = 301806, upload-time = "2026-10-24T18:10:52.651Z" },
+]
+
+[[package]]
+name = "webcolors"
+version = "25.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" },
+]
+
+[[package]]
+name = "webencodings"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001, upload-time = "2026-08-15T14:22:57.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745, upload-time = "2026-08-15T14:22:56.31Z" },
+]
+
+[[package]]
+name = "websocket-client"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/cb/a5abcc2891249f393827c650c6296660ce40374ac22d99ab9aea41f9d2a2/websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", size = 84110, upload-time = "2026-08-31T14:08:40.964Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/d2/cc4dc1271e464942db7ee278baae2daa99ee77cb2af744025c04da585a3e/websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce", size = 95786, upload-time = "2026-08-31T14:08:39.899Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "17.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/75/79/590c5f0cc7d1661195aeefccb0e342b79f4c77d368d526d028821a390332/websockets-17.4.tar.gz", hash = "sha256:09d3939b70df0c8ad2da145ebd50978bccaee1d565e838b0e7921a34d38571eb", size = 191391, upload-time = "2026-10-30T15:10:35.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/5b/085530961668661300de7248f9bf3ba84e98f47875f0fcc87637d024f82b/websockets-17.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d0f943b53bb1d7389d72a5380078670e5fcc34a1328142148b72d20fd82031c7", size = 218810, upload-time = "2026-10-30T15:08:12.14Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/fb/5a29e9569f74988822b7bc076287dd7f9f5b68a2070cf5eb4cfada5863d8/websockets-17.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2fd562372d4afaf18d1c7f1c68e39bd32818bb16d5f57f778a4a4ed88eedcaae", size = 216506, upload-time = "2026-10-30T15:08:13.561Z" },
+ { url = "https://files.pythonhosted.org/packages/df/73/eb8544367c13cd87df39d41b74853741fb7182e2a4bb659ad7d36f9066c8/websockets-17.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a0457561e8a87a6a3b509e21c35084d7d207ced262139de3f738e9676a476bbd", size = 216719, upload-time = "2026-10-30T15:08:15.08Z" },
+ { url = "https://files.pythonhosted.org/packages/24/fe/c3707e426d9bd779dfad9f319b2b431e5e60d563d7547d3780cbd1646028/websockets-17.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b4ee34acefa9ba1333f02b817982f1e4127f6f9b975017a707c4e738d7f9fb9", size = 226098, upload-time = "2026-10-30T15:08:16.581Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/24/d203439f909ffbf37260ca28a3432381412d2e79912ebf98be572e63055e/websockets-17.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ef03fb4b3685df0ed9dc6a074a090f030345cad2c23c61818da3e08e693a9caa", size = 226449, upload-time = "2026-10-30T15:08:18.29Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/17/3ae5d9486a170a222dd0437b4f2bc02ecf324177498a942dcac200e65d05/websockets-17.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5e970d3996c597186c9dbb6e77a8048737a834ac03cb13186a54999e0e1c31e", size = 227695, upload-time = "2026-10-30T15:08:19.692Z" },
+ { url = "https://files.pythonhosted.org/packages/34/37/1307a8e30a961726f2350982fc01b0b3498db29b030c78c566e65ba64487/websockets-17.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6afc32a7a1a40b071070419537df3559d26907086556769683fa97b7b6e90fd5", size = 229069, upload-time = "2026-10-30T15:08:21.682Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/bd/4c6167744e202d20a3c770bcde8fae5d427317b1885712c2e02718cd8133/websockets-17.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5da7f7f1ecf866186f195f7ecc374c915e1838496b86f2f3b41da27c798a25c3", size = 228296, upload-time = "2026-10-30T15:08:23.371Z" },
+ { url = "https://files.pythonhosted.org/packages/14/b3/3438eee41b39afb44c3c6e45dd686b88a7f8d742b96877b77de4fe0e0255/websockets-17.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:043945a43873c7c11a3f75238d475c95f902e967de329eb8763704546cfaa6b6", size = 227045, upload-time = "2026-10-30T15:08:24.749Z" },
+ { url = "https://files.pythonhosted.org/packages/46/39/0bac9052d402efaad1b202ff2f1a57b6334aa624d20f509312765601b507/websockets-17.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:927d602fb8fb595a69e6b1ae998a6ec19851d0fb9391759456e6425090996647", size = 224297, upload-time = "2026-10-30T15:08:26.275Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/7f/5809f6ba5a7d1065301800daf605bdb661a9db5ef868ed189dbf691122ac/websockets-17.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:769e4f7b090e10cae4c082e3e39b9458ec2444505df84953c16c0468b16539c4", size = 227169, upload-time = "2026-10-30T15:08:27.703Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/f7/c8f6771e7cc2a038cf78bb12d04d29dbbbabf2a84d76999bcc94532f3020/websockets-17.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:03ddf4eb76ced8725dcbb000ff60b0fb8d7330ed011f6dc725a1925c5fe02051", size = 225632, upload-time = "2026-10-30T15:08:29.441Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2f/9104786d952978ca2d011321109e35c05be24c3937ef2a62b4d6e25f7515/websockets-17.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:94622cf24e80d03054ceb69a06a86c950e12fe993ed06b933f7895232cacbc3e", size = 226415, upload-time = "2026-10-30T15:08:30.962Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/a7/2e1606b3d616b86bb6c70a62d5f87fc9a3dbd81f0bbe24c4706ace76751d/websockets-17.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bc581579885728bb54ef15000295c20e6a64a200b1b73d5cfca71879a3eb3e1f", size = 227594, upload-time = "2026-10-30T15:08:32.443Z" },
+ { url = "https://files.pythonhosted.org/packages/67/2d/78ca44e2be6d397aee63a4e68d3a99a33bb14bb3405e94688bc5a2160db5/websockets-17.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f42671900e0b602d05a074c52fd3d757c2f8e5c4f73f771794dae1aa4aea5f0e", size = 225146, upload-time = "2026-10-30T15:08:33.888Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b6/6dad684e54fc8e09d70c9a095506db5c836207d8e846e3cb1e79424e3492/websockets-17.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:2b23dacf7a8a88afe92f77bcdbf4cf043a84692bac2f2da02f4011c687e19b48", size = 226095, upload-time = "2026-10-30T15:08:35.285Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/7b04bad7c11c1f92238429352defbac1406743338049afe1328c7a7f95c0/websockets-17.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d48795c7d64ca1c42e3d61fcc6478cb15a19b6ad3f9c882bc8d8dee49d4a420f", size = 226395, upload-time = "2026-10-30T15:08:36.8Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/4eb29c4b12f77b3139470063308ae1e6a0860b9073b1d27d56e8ce19c512/websockets-17.4-cp314-cp314-win32.whl", hash = "sha256:8588edf317fbe4f16a949bc5a6ccdd22b6e6fd5b4745d128bc40bccf31024ce5", size = 218784, upload-time = "2026-10-30T15:08:38.263Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/85/2496a24de80c215b55a15f037bc368a2ed0ce7ecb1e909039ff4e76395de/websockets-17.4-cp314-cp314-win_amd64.whl", hash = "sha256:70293c25d79d1f084fdd665894481547575e429d2a75e8d72caec2f6b6b07af8", size = 219137, upload-time = "2026-10-30T15:08:40.112Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a3/cfe95d4eb569035e5a1d2e5cbeb4ccf0eb15a19e1ef2ad819a85960bf375/websockets-17.4-cp314-cp314-win_arm64.whl", hash = "sha256:adc30e5d064c9e33049e825408fa4e952f4e8a0cf4c329dc2ded72e1bb5bae08", size = 219017, upload-time = "2026-10-30T15:08:41.778Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/dd/4143048a2b27303e7de8ef53d7d0ee3987734fbf93e09491d5a1b0a55187/websockets-17.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:bbb6bed49f083e2ec8ae0bb21c00c7c5171ab8e08a8b008c61a43fc6c944fdac", size = 219141, upload-time = "2026-10-30T15:08:43.184Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/3e/a63b5f3a5c24be2898eb22a3093a56d4ac4cd24ae79e7ab7a1b971ed4611/websockets-17.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c7a56140fcbf4662cb263754a754c651a16fe2196cae1fd232d18f89acd30c83", size = 216675, upload-time = "2026-10-30T15:08:44.682Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/80/f238a10f0b23853c665c602a42784023995ac26674e480252c21af39f4f8/websockets-17.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:18d3710becc612ec6f0229425bd6495133dfcbf30f7b6569585acc8ed2f1b452", size = 216880, upload-time = "2026-10-30T15:08:46.304Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b1/899b69aba1e8d65ac5bfe90b97ada22a815f1dcd3840bf188104738aac33/websockets-17.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00ad98e4bb0df7a6711c645eec16d5a9fdf99f0692bd4f5b6cb85a68677ba8df", size = 226462, upload-time = "2026-10-30T15:08:47.723Z" },
+ { url = "https://files.pythonhosted.org/packages/28/a8/598c2b2a22dfe14d352400df804db8b78a68c1498b3f775a658f07f21618/websockets-17.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7849a096d6fdba3f2bdf849eb89f2d233a24400c92faa3efb25fa8dc615b706c", size = 226741, upload-time = "2026-10-30T15:08:49.314Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ad/84a6de3cc3dd4ee336428c31f89a82e002299f83e10d967f0e9a5cbaf591/websockets-17.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e30550965c78223439815dbdf40c93eb8931dfbf12427110dcfcaeebef76feb6", size = 227914, upload-time = "2026-10-30T15:08:50.746Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ab/1e191b97d60a934c071b7b73c39c5db39721a925cc53acd929c615f85550/websockets-17.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:725be1aff55f1c873a79cea0f612df9dff21a9d2c2e05783665b824b8aa7c24e", size = 230059, upload-time = "2026-10-30T15:08:52.596Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/57/ef8a64732ef4367091ab05f9b22c1ee4c28657d4a0d088a644198eb5ded6/websockets-17.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6c7f91520c29420639925fd1cecceac9ca07366b50c09ea24eac1cd116b4866", size = 228486, upload-time = "2026-10-30T15:08:53.967Z" },
+ { url = "https://files.pythonhosted.org/packages/48/1f/dbad3afcacf2c52d1fdf83b611d849598f0a047d7333ff476266532877b8/websockets-17.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6531540aa63e6864cb0615a3a5da70ae03191334507852a5385320bb7916df3f", size = 227216, upload-time = "2026-10-30T15:08:55.687Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/47/59e325d6ca3e04ba5cc7a139d891ef83cad3fb460a183c8f359f378b69a9/websockets-17.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a57b99f2e28922b5a2434317580fd23fdf118caff4c5b752f5452836426205a", size = 224755, upload-time = "2026-10-30T15:08:57.203Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/b6/28a471318dfb0060b58c47d0a252cb5078528a1aa1fdf5e6e88e2a61cf18/websockets-17.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8fd30d217a4275107667fe1d4f601e2d50c797d0727148cfbb384536d65f04d3", size = 227508, upload-time = "2026-10-30T15:08:58.597Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/29/7485ac6fc4b342b43964811381f56276fec967cae6a851ae4b815613b10c/websockets-17.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cc0cd4065a65ac98f3e2742ee9f314da9d224db49b07dc17d3a20cef0d79658d", size = 225961, upload-time = "2026-10-30T15:09:00.414Z" },
+ { url = "https://files.pythonhosted.org/packages/95/a6/520e1f7aa9644ddf58f5a51fc37f46e15293a68cd35b70d5b81b7331ca1e/websockets-17.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:71f76bb8e59be847330b8a8d3753f85397677e128f9f9692c215f8cf721cefc7", size = 226851, upload-time = "2026-10-30T15:09:01.979Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/0d/444799abe7418f361da8c201c6858b64b4e8d47265f90851e5cb68b4d90f/websockets-17.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c2db7b9e2d726f7e051bedb67abcf460b84b11437b2f9fdacda7e6931dace85b", size = 227869, upload-time = "2026-10-30T15:09:03.405Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/86/6da20322a07364a0ab0787a2b0d4367770f59404fdab6e35122413a7f63e/websockets-17.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:332b88022d0acd47c44158bcd43f47f14eaddcd979247f242b1a33291c45cdd1", size = 225562, upload-time = "2026-10-30T15:09:04.891Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/da/53e44d96238dc207011ad5d93b0540c58e1c680a86f48dc3fa8e5138b343/websockets-17.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5cec3e7fbc911ff4fcf43b24aaf1b96906381ad36030f42897def3ff4b78729e", size = 226334, upload-time = "2026-10-30T15:09:06.42Z" },
+ { url = "https://files.pythonhosted.org/packages/18/08/82cea972a87f229ee1903e9adf74c17d1e6c4371d581ebb11ccbed4e3a7b/websockets-17.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cbdf4b42f4514422051889424fd03bc269be8c5d751bf39ec29ad761590bd10d", size = 226767, upload-time = "2026-10-30T15:09:07.837Z" },
+ { url = "https://files.pythonhosted.org/packages/96/f3/ee6afff802e39e8d6ef29ee4bdc30f8272aaa38295c19a911fad91fe95fc/websockets-17.4-cp314-cp314t-win32.whl", hash = "sha256:16550e24ac22c27e0a728100d8de196b5f054bab6748ef9f0b168fb8b3c32e35", size = 218918, upload-time = "2026-10-30T15:09:09.31Z" },
+ { url = "https://files.pythonhosted.org/packages/11/56/b0b07917f9c85a4d24a37300f4ae888bfa7c20f08902dd6a5c61a0d1b26f/websockets-17.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5ec906bb21b35e66cfdcc2ea0133f249f52b7d89e3e45ec8d2bd2d84a0255d7d", size = 219260, upload-time = "2026-10-30T15:09:10.832Z" },
+ { url = "https://files.pythonhosted.org/packages/19/a6/a63113909ea602d3d22b0f32a7a9950d8d18b725cfdce735eab9b5f5d198/websockets-17.4-cp314-cp314t-win_arm64.whl", hash = "sha256:c00f0c45851a2c61efffd03dde8fb4d594026b5cc18196c94e15460f1225f2ed", size = 219176, upload-time = "2026-10-30T15:09:12.444Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/be/4032fde05e62d1c52edd250fda475e910cac0cd180e377690bff24147b5e/websockets-17.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:8c51ac7b31578a7275650f580aac985940985b6d2e57247ac6d5e07f2c936cd6", size = 218740, upload-time = "2026-10-30T15:09:13.833Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1c/b47f5786eb45599072411571651606fb22f6e825d522d627daad974ea636/websockets-17.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:023dc13bdc42a55e152acc81c39c90ec6cc4159ed17d23c6d73460fa2a73ab16", size = 216470, upload-time = "2026-10-30T15:09:15.394Z" },
+ { url = "https://files.pythonhosted.org/packages/70/98/0021f55de633617b4a8390cca17e700c9ba5d99323103128412dc2c15aca/websockets-17.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6fd8cac4907fec8302726d49e6969acfffaf720bc39b1cee23f473b81f4dce01", size = 216695, upload-time = "2026-10-30T15:09:17.185Z" },
+ { url = "https://files.pythonhosted.org/packages/28/56/2c67389b47c4b9e9e9318d9b3e4bb52a2fef3749ff00b0b151a342c4d42d/websockets-17.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4441d655a53185ef1b01b1d42ad9b3a96b905110a9219043e3fdaec855ba3b6e", size = 226134, upload-time = "2026-10-30T15:09:19.156Z" },
+ { url = "https://files.pythonhosted.org/packages/75/bd/ee3a679a10741a0cdd7aa8bdd6d03999069672826aa7a6495fdf589ed961/websockets-17.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3acc45494c08a4d27375a2c28474537f68910219f42624a08011221b04623d04", size = 226569, upload-time = "2026-10-30T15:09:20.587Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/42/b293a5d07099549797138d09fda5bf878d302b4980e61678981c8dfdc98f/websockets-17.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b78b39377630a70aa2198e5f03be416e690364a046b1d8af80a9263733083d0", size = 227726, upload-time = "2026-10-30T15:09:22.015Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/81/d048ff83c4d0fc386f6008529d12690d0dcb410f73570430f13fe4d543f0/websockets-17.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6cf4c002268da5c11cae9083aec71ac943bce9d95e0f83c8bf9a95a3ce421fbb", size = 229426, upload-time = "2026-10-30T15:09:23.444Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/21/1df51f94047dfb76c32fd7508036e4b55352d0f04fd0e3bee525a2122225/websockets-17.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed7fe197b8bb66eef4f62c052e33752d456d2fdda74805469d4bd954950ab339", size = 228211, upload-time = "2026-10-30T15:09:24.902Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/13/33d6a28471da4c30a98a2313c76f984be2056a703c5c532225dc28f5dba3/websockets-17.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f0494d3d97502e270282cc6a1f8648cd7335b4fe8d5c452006e5b7f9d62b8892", size = 227106, upload-time = "2026-10-30T15:09:26.315Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/86/ff8441e56ca677e51f9c826868fccde2a82bafe0ffa337fd38823b64253a/websockets-17.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab9e4bd60796866c578e3afa0a17c98636887827a39c2e238a61989c18fd8e7b", size = 224499, upload-time = "2026-10-30T15:09:28.116Z" },
+ { url = "https://files.pythonhosted.org/packages/da/91/470d7a619ce4aa837ff5b6a5836227c43bdfdc42ef837cce48f508992438/websockets-17.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8d148b4b295224d3a19e7bd9f02ebe7344de254b3b2c4d0628ce42ade0bca119", size = 227213, upload-time = "2026-10-30T15:09:29.847Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ba/55ed1422d6c73b5b81996ca195f2a256a339b26c553f5686afc2a926bc2b/websockets-17.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6597a1f7413189b4bc58437babcdf39b79da416e5c477ae98323a88ecc108188", size = 225876, upload-time = "2026-10-30T15:09:31.249Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/59/f133d260e7a48d3e231c3a495eeb6114b880c1e96bfc781a949c1f73991c/websockets-17.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:a9b0026538719f7a532998d84640ab2e91ca1a051ce4b8d924fdcb607307e921", size = 226403, upload-time = "2026-10-30T15:09:33.199Z" },
+ { url = "https://files.pythonhosted.org/packages/45/16/bb3355453ffdc31d3a14ddc0b50e058f13a97d2ed019ba6c380e03456169/websockets-17.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a703f23ca15a51251fc94f1774683f5aa511588a7ad6bbb83960cdb2a4dcd978", size = 227558, upload-time = "2026-10-30T15:09:34.703Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ab/f3d55a83c2a4daa8bf62594b00a721dc8767518ccb833f932b0178eb6c19/websockets-17.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:6b061ea7ca94966af7cd7a87cb189ff15972c103244611a2b1e8408c04f1dbe6", size = 225369, upload-time = "2026-10-30T15:09:36.204Z" },
+ { url = "https://files.pythonhosted.org/packages/88/66/3a320bfb718d9ae1eff15ba7790979c0e6fdec74e388430f426e6647d7f4/websockets-17.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:e6d2a185ec4ff0478089305abd833da3a9fda8803eb77325b89ebeae5585a3b7", size = 226268, upload-time = "2026-10-30T15:09:37.565Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/1f9cdeeaaa2d5e158b7a40f971e74b641b35f977674b46229a5bb14e270a/websockets-17.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9a4e95d56994a45ac76541853796720436ffe462957d0b1b89e87ff3d3b1627e", size = 226530, upload-time = "2026-10-30T15:09:39.171Z" },
+ { url = "https://files.pythonhosted.org/packages/da/28/506406ef09e6e168796f0187bdac0767f38a560e6785c4ee26e583aaaf76/websockets-17.4-cp315-cp315-win32.whl", hash = "sha256:b7ea61af293a153f2d67430038cf0efc10140caa623f2bdf5a9f5a457ff20f57", size = 218751, upload-time = "2026-10-30T15:09:40.748Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a6/818647a9a9b3ee8bf131e9ab4fa1f2db9c6539f08e7084dd57549e0bd49d/websockets-17.4-cp315-cp315-win_amd64.whl", hash = "sha256:8b45d40f07081b377f0b837ddbfba4a9ab4a89a7b569d3c095436564e767a27b", size = 219045, upload-time = "2026-10-30T15:09:42.251Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/b2/0338a9abec3df8556627af987352657ce1c8456f2060b54741570b174122/websockets-17.4-cp315-cp315-win_arm64.whl", hash = "sha256:29b615c2d5dd883f89c107a782681b73a7aa8b2bd894130310e45eb09b143913", size = 218957, upload-time = "2026-10-30T15:09:43.866Z" },
+ { url = "https://files.pythonhosted.org/packages/68/92/22e83d003cdbd22be5d35e28c260203e9fbb6c73b17a8e1509f4ad49e74d/websockets-17.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c568e72f5425a74616daeb824afe28da460cbeea036432f41c6af5380b83300d", size = 219056, upload-time = "2026-10-30T15:09:45.528Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/ca/071d38e7237f6ada4f577b74f7cbf5bc2ff7e1f9360ebd2837e74a22dfd4/websockets-17.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:bfc86dff091104eec770c29ea1831dbe191968a70aa2d8ae8eda3240d863970a", size = 216630, upload-time = "2026-10-30T15:09:46.951Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/76/22d4523f92d88b8f5ebfd3df7ab5e825146af100f7e2caeae7d359ae51ea/websockets-17.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5f06f59e19aa0770aaf651ce91c196fa620b35aed49a7e6c36e7335f3f83db7", size = 216841, upload-time = "2026-10-30T15:09:48.285Z" },
+ { url = "https://files.pythonhosted.org/packages/87/29/bc94be4a8bc19ef518e9efecd90a992f2991b7cfc6eb8a7430c4e2dc097c/websockets-17.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8f32a6cb0b101170abc2d8053af1473a2a29ef2a06d7130994ee8455a50f5dbf", size = 226992, upload-time = "2026-10-30T15:09:49.776Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/25/58f69b5fec064764ddf5b88045397433cdb028f5b17c82fbe047fd31fff7/websockets-17.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:72d0a731eccf482d5661c6711abfcd5921f3dbfbbb0789061e1918ef4bc89c70", size = 227258, upload-time = "2026-10-30T15:09:51.172Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7c/bc932a54c5f1bd78461574aea539fc562fd2de494738a2c0675c4de860be/websockets-17.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48c83103f82fce36191a9e3a8a80731d4340e2da544eae8c5aaaf6a45280e7d0", size = 228589, upload-time = "2026-10-30T15:09:52.628Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/92/57ec83066d0bfa8a9b9f594bc61951073c4a86e34c562a5292604118dab6/websockets-17.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c8052c3058c402d53602583da5ddcf1a15a63ab1efe0222f47c7be59944e6463", size = 230217, upload-time = "2026-10-30T15:09:54.058Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/fc/0f2fe533b5b4dc22ac704b3357f5c99fa60a3837218b3c6c6ad63a60120e/websockets-17.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0c22a9c6b2349204f56123dd0ca7779e989d68e336d0dc02ad823485d44925de", size = 229026, upload-time = "2026-10-30T15:09:55.727Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/cc/ed28307835106a9a2dbd1a5efc597e8befa3d0a5f2b46a4c1e466b2f4759/websockets-17.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0dcce83837328e6795225ead920d3aa8d867cc9d6922aa558b0e9111599fdbb9", size = 227904, upload-time = "2026-10-30T15:09:57.221Z" },
+ { url = "https://files.pythonhosted.org/packages/39/4d/24ad7deddc7b31c3e430acc1c00c7b384ae42f1f973e9cc01aa42d19628f/websockets-17.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b79ea31cf277c72d2307c6cb8bd32a2c151959e80a2f24fa85df620c5f2860c", size = 225253, upload-time = "2026-10-30T15:09:58.624Z" },
+ { url = "https://files.pythonhosted.org/packages/93/f6/9c92230ebfab42fee482b236c45c32ed34b24f7360080b400cce6cfcac11/websockets-17.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3d27153a0413696767b98b5d96173a2a7e27cbfca02000a6069cea7b977765a7", size = 228089, upload-time = "2026-10-30T15:10:00.308Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/7a/f523830e562da9b1d7a610174752753720c5e47e84b59b5fc47721d416bd/websockets-17.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:e0feb03a338f8f5e819ae23a73aabb7b46fd3b73f15f7df782dd12d99e2b02ad", size = 226743, upload-time = "2026-10-30T15:10:01.744Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e6/d1a56495cb7b1a0a4688145131be38cfd0c1342bde8125e136a6e6ee494e/websockets-17.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9ff89e36451a19775e7556d95ebfa4a0b24b84cc87565d6b5d0ec8ea82bf3f2c", size = 227237, upload-time = "2026-10-30T15:10:03.417Z" },
+ { url = "https://files.pythonhosted.org/packages/44/41/c4e53f7ad63838d7eba6e82c5d743827e91dafe5e48c88a58f7e90043945/websockets-17.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:ee6c4e0085bff5f6621bf9c53452584dcb91ebd993ce841e1f4061ac12b6b556", size = 228348, upload-time = "2026-10-30T15:10:05.149Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/01/f6f78c3edeb8ad2b3c35a8c4117ec2cf9346215afaed8155af1e7933818b/websockets-17.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1962f3bfde085af18878f1bb1aafb300c9a6ac33da886d0b0d97e33c9ce7a222", size = 226099, upload-time = "2026-10-30T15:10:06.588Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/01/d8c11740231b63d1aba6bad6b1d4e72e8384b26c5448801da238ebe82c92/websockets-17.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:42d7a6e818214336cf7af9a3aae5c6923f94f09fa07ac5aab98268ba2a98a6d5", size = 226956, upload-time = "2026-10-30T15:10:08.141Z" },
+ { url = "https://files.pythonhosted.org/packages/99/87/dbfd43ae01ea8fdb7478a1b3d58b32aca83bb9c4aa61ebe187d1e188ac8e/websockets-17.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c9f8fdb7bbefc4907c2bb3593f810f2b67a2d5329e1d38724a1aa3d763cf63eb", size = 227253, upload-time = "2026-10-30T15:10:09.533Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/8e/fbd968a75428063560ada02d564de8d6c49ca34ec29dfb78e5d900f4b0ef/websockets-17.4-cp315-cp315t-win32.whl", hash = "sha256:506623128d7a55b726a5a46e4c9a8eae4c9dd0f04ff60b041fe7df85b9d034fe", size = 218877, upload-time = "2026-10-30T15:10:10.925Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/53/8f37a3943517bba77fc86f7c4162c455018c5b6d760c5e9c9a11de9e37f7/websockets-17.4-cp315-cp315t-win_amd64.whl", hash = "sha256:6e6558efbb256b532c9f06a21ba42bcac729718e365c505819f181f25e5c0eca", size = 219195, upload-time = "2026-10-30T15:10:12.336Z" },
+ { url = "https://files.pythonhosted.org/packages/69/35/cf33cba7248fcd80cfe5911ffe67aa2637f50c2bfaada123b1e9d2aeca90/websockets-17.4-cp315-cp315t-win_arm64.whl", hash = "sha256:fcac44d917672ad6257ad64b80b5168306f168a78cab5c0612e4c699caed18cf", size = 219117, upload-time = "2026-10-30T15:10:13.771Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/27/9db924cb0dbf22eade78d98fbae77790f9bf6bc729e6ab29aef9d68e4156/websockets-17.4-py3-none-any.whl", hash = "sha256:c6bd2c9781915592b209160d0f017d4c7c82ad53075e6b0c24d2a89299e7f9e9", size = 212900, upload-time = "2026-10-30T15:10:34.482Z" },
+]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bf/60/bc7a980fc78837d6ef8f5940cca4cadc433364503a4c4d42e2a7a0de3231/widgetsnbextension-4.0.16.tar.gz", hash = "sha256:adeea0ae78f0856ee4945f413299801b82a0a01416303301f39a704282a37b73", size = 1111094, upload-time = "2026-08-18T08:52:55.859Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl", hash = "sha256:a31a8774885b96fe825462f5d6496166f0c7cae111195b6465c801d230eb5a4e", size = 2225148, upload-time = "2026-08-18T08:52:53.736Z" },
+]