pax_global_header 0000666 0000000 0000000 00000000064 15154651363 0014523 g ustar 00root root 0000000 0000000 52 comment=7188740143de900230f12ee5be9a41dd6154dd58
cogent3-citeable-7188740/ 0000775 0000000 0000000 00000000000 15154651363 0015037 5 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/.github/ 0000775 0000000 0000000 00000000000 15154651363 0016377 5 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/.github/dependabot.yml 0000664 0000000 0000000 00000000463 15154651363 0021232 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
time: "19:00"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
time: "19:00"
open-pull-requests-limit: 10
cogent3-citeable-7188740/.github/workflows/ 0000775 0000000 0000000 00000000000 15154651363 0020434 5 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/.github/workflows/linters.yml 0000664 0000000 0000000 00000001267 15154651363 0022645 0 ustar 00root root 0000000 0000000 name: Lint code using ruff
on:
push:
permissions:
contents: write
jobs:
linters:
runs-on: ubuntu-latest
if: github.repository == 'cogent3/citeable'
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Format code using ruff
run: uv run nox -s fmt
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: ${{ github.actor }}
author_email: ${{ github.actor }}@users.noreply.github.com
message: "STY: pre-commit linting with ruff"
add: "."
cogent3-citeable-7188740/.github/workflows/release.yml 0000664 0000000 0000000 00000004064 15154651363 0022603 0 ustar 00root root 0000000 0000000 name: Release
on: [workflow_dispatch]
jobs:
test:
name: "Test on Python ${{ matrix.python-version }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: "actions/checkout@v6"
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Run nox for ${{ matrix.python-version }}"
shell: bash
run: uv run nox -s "test-${{ matrix.python-version }}"
build:
name: Build wheel and sdist
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build sdist and wheel
run: uv build
- name: Upload sdist and wheel
uses: actions/upload-artifact@v7
with:
name: citeable-wheel-sdist
path: |
./dist/*.whl
./dist/*.tar.gz
release_test:
name: Release to Test PyPI
needs: build
environment: test_pypi
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Download sdist and wheel
uses: actions/download-artifact@v8
with:
name: citeable-wheel-sdist
path: ./dist
- name: Publish package distributions to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
release:
name: Release to PyPI
needs: release_test
environment: pypi
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Download sdist and wheel
uses: actions/download-artifact@v8
with:
name: citeable-wheel-sdist
path: ./dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
cogent3-citeable-7188740/.github/workflows/testing_develop.yml 0000664 0000000 0000000 00000003476 15154651363 0024364 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
pull_request:
# NOTE:
# if changing python versions, also update versions in
# - release.yml
# - noxfile.py
jobs:
tests:
name: "Python ${{ matrix.python-version }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.14"]
steps:
- uses: "actions/checkout@v6"
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Run nox for ${{ matrix.python-version }}"
shell: bash
run: |
cov="--cov-report lcov:lcov-${{matrix.os}}-${{matrix.python-version}}.lcov --cov-report term --cov-append --cov citeable"
uv run nox -s "test-${{ matrix.python-version }}" -- $cov
- name: Coveralls Parallel
uses: coverallsapp/github-action@v2
with:
parallel: true
github-token: ${{ secrets.github_token }}
flag-name: run-${{matrix.python-version}}-${{matrix.os}}
file: "tests/lcov-${{matrix.os}}-${{matrix.python-version}}.lcov"
type_check:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Run Type Checking"
run: uv run mypy src/citeable --strict
finish:
name: "Finish Coveralls"
needs: tests
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
cogent3-citeable-7188740/.gitignore 0000664 0000000 0000000 00000001102 15154651363 0017021 0 ustar 00root root 0000000 0000000 *.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
doc/_build
# docker stuff
.devcontainer/*
# Installer logs
pip-log.txt
# Unit test / coverage reports
.mypy*
**/.mypy_cache/*
.coverage*
.tox
.nox
coverage.xml
junit-*.xml
nosetests.xml
tests/draw_results
lcov*.info
.ruff_cache/*
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.idea/*
.DS_Store
__pycache__
*.code-workspace
*.wpr
*.wpu
.vscode/*
venv*
.venv*
working/
*.ipynb
*.ipynb_checkpoints/
# vi
.*.swp
cogent3-citeable-7188740/.hgignore 0000664 0000000 0000000 00000000646 15154651363 0016650 0 ustar 00root root 0000000 0000000 syntax:glob
.svn
*.pyc
*.pyo
*.so
*.o
*.DS_Store
*.tmproj
*.rej
*.orig
*.wpr
*.pdf
_build/*
build
*htmlcov*
*.idea
draw_results
*.coverage*
*egg-info*
*.wpu
.cache*
*taskpaper
*.ipynb
*.ipynb_checkpoints*
*.sublime*
*.patch
*.pytest_cache
*.tox
*.nox
*.vscode
*.code-workspace
coverage.xml
__pycache__
junit-*.xml
doc/draw*
dist/*
working/*
lcov*.info
.ruff_cache/*
.devcontainer/*
hg_old*zip
hg_old/*
venv*
.venv*
.mypy*
cogent3-citeable-7188740/.sourcery.yaml 0000664 0000000 0000000 00000000050 15154651363 0017647 0 ustar 00root root 0000000 0000000 rule_settings:
python_version: '3.11'
cogent3-citeable-7188740/LICENSE 0000664 0000000 0000000 00000002734 15154651363 0016052 0 ustar 00root root 0000000 0000000 BSD 3-Clause License
Copyright (c) 2026, Gavin Huttley
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
cogent3-citeable-7188740/README.md 0000664 0000000 0000000 00000032046 15154651363 0016323 0 ustar 00root root 0000000 0000000 # citeable 📚
*Structured BibTeX citations for developers of scientific software packages.*
[](https://github.com/cogent3/citeable/blob/main/LICENSE)
[](https://coveralls.io/github/GavinHuttley/citeable?branch=main)
[](https://badge.fury.io/py/citeable)

[](https://github.com/astral-sh/ruff)
[](https://github.com/cogent3/cogent3/actions/workflows/codeql.yml)
[](https://app.codacy.com/gh/cogent3/citeable/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
## Overview
`citeable` is a lightweight, zero-dependency, pure-Python library for defining structured bibliographic citations. The goal is to make it easy for package users to cite package developers. It is being used by [cogent3](https://github.com/cogent3/cogent3) and cogent3 plugin developers to declare citations that cogent3 can assemble into a BibTeX-compatible `.bib` file to ensure users cite their work. But it can be used for other projects too!
## Installation
```bash
pip install citeable
```
Requirements
Pure Python, no dependencies. Requires Python >= 3.11.
## Developer quick start
```python
from citeable import Article
cite = Article(
author=["Huttley, Gavin", "Caley, Katherine", "McArthur, Robert"],
title="diverse-seq: an application for alignment-free selecting and clustering biological sequences",
journal="Journal of Open Source Software",
year=2025,
volume=10,
number=110,
pages="7765",
doi="10.21105/joss.07765",
url="https://doi.org/10.21105/joss.07765",
)
# cite.key == 'Huttley.2025'
```
```bibtex
@article{Huttley.2025,
author = {Huttley, Gavin and Caley, Katherine and McArthur, Robert},
title = {diverse-seq: an application for alignment-free selecting and clustering biological sequences},
journal = {Journal of Open Source Software},
year = {2025},
volume = {10},
number = {110},
pages = {7765},
doi = {10.21105/joss.07765},
url = {https://doi.org/10.21105/joss.07765},
}
```
## Defining a citation
Constructing directly in Python (recommended)
Citations are constructed directly in Python source. Required fields are positional-or-keyword constructor arguments; optional fields are keyword-only with `None` defaults. `key` is always optional at construction -- it will be auto-generated if omitted (see [Key generation](#key-generation) below).
```python
from citeable import Article, Software
cite = Article(
author=["Huttley, Gavin", "Caley, Katherine", "McArthur, Robert"],
title="diverse-seq: an application for alignment-free selecting and clustering biological sequences",
journal="Journal of Open Source Software",
year=2025,
volume=10,
number=110,
pages="7765",
doi="10.21105/joss.07765",
)
# cite.key == "Huttley.2025"
tool_cite = Software(
author=["Smith, Jane"],
title="my-cogent3-plugin",
year=2024,
version="1.0.0",
url="https://github.com/jsmith/my-cogent3-plugin",
)
# tool_cite.key == "Smith.2024"
```
Validation is performed at construction time. A missing required field raises `ValueError` with a message identifying the field and entry type:
```
ValueError: Article requires 'volume'; received None
```
Parsing from a BibTeX string
`from_bibtex_string` accepts a raw BibTeX string containing a single record and returns the corresponding `citeable` object. This is the intended path for developers who already have a `.bib` entry in a reference manager -- paste the raw BibTeX string directly into Python source.
```python
from citeable import from_bibtex_string
cite = from_bibtex_string("""
@article{Huttley.2025,
doi = {10.21105/joss.07765},
url = {https://doi.org/10.21105/joss.07765},
year = {2025},
volume = {10},
number = {110},
pages = {7765},
author = {Huttley, Gavin and Caley, Katherine and McArthur, Robert},
title = {diverse-seq: an application for alignment-free selecting and clustering biological sequences},
journal = {Journal of Open Source Software},
}
""")
```
The cite key from the BibTeX string is preserved as the `key` value. Author names in `"First Last"` format are normalised to `"Last, First"` on parse.
**Round-trip scaffolding:** use `from_bibtex_string` + `repr()` to convert a BibTeX record into a clean Python constructor call, then paste that into your source:
```python
print(repr(cite))
```
```python
Article(
author=['Huttley, Gavin', 'Caley, Katherine', 'McArthur, Robert'],
title='diverse-seq: an application for alignment-free selecting and clustering biological sequences',
year=2025,
journal='Journal of Open Source Software',
volume=10,
pages='7765',
number=110,
doi='10.21105/joss.07765',
url='https://doi.org/10.21105/joss.07765',
)
```
## Supported entry types
| Class | BibTeX `@type` | Required fields (beyond common) |
|---|---|---|
| `Article` | `@article` | `journal`, `volume`, `pages` or `article_number` |
| `Book` | `@book` | `publisher` |
| `InProceedings` | `@inproceedings` | `booktitle` |
| `TechReport` | `@techreport` | `institution` |
| `Thesis` | `@phdthesis` / `@mastersthesis` | `school`, `thesis_type` |
| `Software` | `@software` | *(none)* |
| `Misc` | `@misc` | *(none)* |
All types share common fields: `author` (required), `title` (required), `year` (required), `doi`, `url`, `note`, `key`, `app`.
Field reference
### Fields common to all entry types
| Field | Required | Notes |
|---|---|---|
| `key` | No | Auto-generated if not supplied |
| `author` | Yes | List of strings in `"Surname, Given"` format |
| `title` | Yes | |
| `year` | Yes | Integer |
| `doi` | No | Recommended where available |
| `url` | No | |
| `note` | No | |
| `app` | No | Name of the cogent3 app. Not written to BibTeX output; excluded from equality and hashing. |
### `Article`
| Field | Required |
|---|---|
| `journal` | Yes |
| `volume` | Yes |
| `pages` | Yes (or `article_number`) |
| `article_number` | No |
| `number` | No -- issue number |
### `Book`
| Field | Required |
|---|---|
| `publisher` | Yes |
| `edition` | No |
| `editor` | No -- list of strings |
### `InProceedings`
| Field | Required |
|---|---|
| `booktitle` | Yes |
| `pages` | No |
| `publisher` | No |
| `editor` | No |
### `TechReport`
| Field | Required |
|---|---|
| `institution` | Yes |
| `number` | No -- report number |
### `Thesis`
| Field | Required | Notes |
|---|---|---|
| `school` | Yes | |
| `thesis_type` | Yes | `"phd"` or `"masters"` -- determines BibTeX entry type |
### `Software`
| Field | Required |
|---|---|
| `publisher` | No -- organisation or individual releasing the software |
| `version` | No -- strongly recommended |
| `license` | No |
### `Misc`
No additional required fields beyond the common set.
## Key generation
Keys are auto-generated from the first author's surname and the year, e.g. `"Huttley.2025"`:
1. Extract surname from the first author (before the first comma, or the last token)
2. Strip non-ASCII characters and spaces; title-case the result
3. Return `"{surname}.{year}"`
On collision, `assign_unique_keys` appends a lowercase letter suffix: `"Smith.2024.a"`, `"Smith.2024.b"`, etc.
A developer may supply an explicit `key` at construction time, in which case auto-generation is skipped. But note that the key attribute of a citation can be modified and cogent3 will do this if their are key conflicts.
## Working with collections
Assigning unique keys
Because citations come from multiple independent plugin developers, key collisions are expected. The function `assign_unique_keys` resolves collisions in-place across a deduplicated list:
```python
from citeable import assign_unique_keys
unique = assign_unique_keys(citations)
```
- Deduplication by value is performed first: if two objects compare as equal, only the first is retained
- Keys already unique in the deduplicated collection are left unchanged
- Collisions between distinct citations sharing a base key get a letter suffix: `"Smith.2024"` becomes `"Smith.2024.a"`, `"Smith.2024.b"`, etc.
- The function mutates surviving objects in-place and returns the deduplicated list
cogent3 calls `assign_unique_keys` when assembling a bibliography from a composed app, so plugin developers do not need to call it themselves.
Writing a .bib file
`write_bibtex` takes a list of citations and a file path, deduplicates the list, assigns unique keys, then writes the result as a valid `.bib` file:
```python
from citeable import write_bibtex
write_bibtex(citations, "bibliography.bib")
```
For cases where only the string is needed:
```python
unique = assign_unique_keys(citations)
bib_string = "\n\n".join(str(c) for c in unique)
```
## Using citeable with cogent3
The `define_app` decorator in cogent3 has an optional `cite` argument:
```python
@define_app(cite=Article(...))
class MyPlugin:
...
```
cogent3 collects citations across a composed app and expose a method (e.g. `app.bibliography()`) that returns a combined `.bib` string.
## Distribution guidance
Plugin developers **must** define their citation as a Python object in their package source. This guarantees it is present after `pip install` without any special `package_data` configuration or `MANIFEST.in` entries.
The recommended pattern is a dedicated `citations.py` in the plugin package:
```
my_plugin/
__init__.py
citations.py # citation objects defined here
app.py # @define_app(cite=MY_CITE) used here
```
`from_bibtex_string` is provided as a convenience constructor only. Either way, the result is a Python object embedded in source, not a runtime file read.
Developer setup (uv)
This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
### Initial setup
```bash
uv sync
```
This creates a `.venv` and installs the package in editable mode with all dev dependencies.
### Running tests
```bash
uv run pytest
```
### Running nox (multi-version test matrix)
```bash
uv run nox
```
Nox is configured to use uv as its virtualenv backend, so it will use uv to create per-session environments.
### Formatting
```bash
uv run nox -s fmt
```
This runs `ruff check --fix-only` followed by `ruff format`.
### Other common commands
```bash
uv run mypy src/citeable --strict # type checking
uv run ruff check . # linting
uv run cog -r README.md # regenerate cog blocks
```
## Contributing
Bug reports and pull requests are welcome at https://github.com/cogent3/citeable.
## Licence
BSD-3-Clause. See [LICENSE](https://github.com/cogent3/citeable/blob/main/LICENSE).
cogent3-citeable-7188740/noxfile.py 0000664 0000000 0000000 00000001256 15154651363 0017061 0 ustar 00root root 0000000 0000000 import os
import sys
import nox
# on python >= 3.12 this will improve speed of test coverage a lot
if sys.version_info >= (3, 12):
os.environ["COVERAGE_CORE"] = "sysmon"
nox.options.default_venv_backend = "uv"
_py_versions = range(11, 15)
@nox.session(python=False)
def fmt(session: nox.Session) -> None:
session.run("ruff", "check", "--fix-only", ".", external=True)
session.run("ruff", "format", ".", external=True)
@nox.session(python=[f"3.{v}" for v in _py_versions])
def test(session: nox.Session) -> None:
session.install("-e.[dev]")
session.chdir("tests")
session.run(
"pytest",
"-s",
"-x",
*session.posargs,
)
cogent3-citeable-7188740/pyproject.toml 0000664 0000000 0000000 00000002062 15154651363 0017753 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "citeable"
version = "2026.3.11b1"
description = "Structured BibTeX citations for developers of scientific software packages."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "BSD-3-Clause" }
dependencies = []
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License",
"Topic :: Software Development :: Libraries :: Python Modules",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
[dependency-groups]
dev = ["cogapp", "mypy", "nox", "pytest", "pytest-cov", "ruff"]
[tool.hatch.build.targets.wheel]
packages = ["src/citeable"]
[tool.mypy]
strict = true
[[tool.mypy.overrides]]
module = "tests.*"
ignore_errors = true
[tool.pytest.ini_options]
testpaths = ["tests"]
cogent3-citeable-7188740/ruff.toml 0000664 0000000 0000000 00000004630 15154651363 0016701 0 ustar 00root root 0000000 0000000 exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"working",
]
# Same as Black.
line-length = 88
indent-width = 4
target-version = "py311"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["ALL"]
# ICN001 not adhering to numpy as np convention due to
# name collision, where np is used as num params
# N801 CamelCase is not always appropriate
# PLR0913 number of arguments sometimes needs to be > 5
# FBT001 and FBT002, positional boolean arguments are allowed!
# PT011, I disagree about checking error messages as well
# as exception types as it doubles the "cost" of fixing typos
ignore = [
"COM812",
"EXE002",
"FA100",
"E501",
"D",
"N801",
"ICN001",
"PLR0913",
"FBT001",
"FBT002",
"PT011",
"S311", # Allow use of random
"PLR2004", # Magic number usage where appropriate
"PLC0415", # Allow delayed imports
"A002", # Allow shadowing builtins for domain field names (e.g. license)
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # asserts allowed in tests...
"INP001", # __init__.py files are not required
"ANN",
"N802",
"N803",
"S608", # sql injection unlikely
]
[format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "lf"
docstring-code-format = true
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
cogent3-citeable-7188740/src/ 0000775 0000000 0000000 00000000000 15154651363 0015626 5 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/src/citeable/ 0000775 0000000 0000000 00000000000 15154651363 0017376 5 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/src/citeable/__init__.py 0000664 0000000 0000000 00000001461 15154651363 0021511 0 ustar 00root root 0000000 0000000 """citeable — structured BibTeX citations for cogent3 plugins."""
from importlib import metadata as _metadata
from citeable._entries import (
Article,
Book,
Citation,
CitationBase,
InProceedings,
Misc,
Software,
TechReport,
Thesis,
)
from citeable._json import from_jsons, load_json, to_jsons, write_json
from citeable._keys import assign_unique_keys, write_bibtex
from citeable._parser import from_bibtex_string
__version__ = _metadata.version("citeable")
__all__ = [
"Article",
"Book",
"Citation",
"CitationBase",
"InProceedings",
"Misc",
"Software",
"TechReport",
"Thesis",
"__version__",
"assign_unique_keys",
"from_bibtex_string",
"from_jsons",
"load_json",
"to_jsons",
"write_bibtex",
"write_json",
]
cogent3-citeable-7188740/src/citeable/_entries.py 0000664 0000000 0000000 00000044477 15154651363 0021600 0 ustar 00root root 0000000 0000000 """Citation entry type classes for all supported BibTeX types."""
from __future__ import annotations
from abc import ABC, abstractmethod
from citeable._keys import generate_key
from citeable._validate import extract_surname, require_field, require_non_empty_authors
def _format_bibtex_field(name: str, value: str) -> str:
return f" {name:<10}= {{{value}}},"
def _author_str(authors: list[str]) -> str:
return " and ".join(authors)
def _author_summary(authors: list[str]) -> str:
surname = extract_surname(authors[0])
return f"{surname} et al." if len(authors) > 1 else surname
def _title_excerpt(title: str, max_len: int = 50) -> str:
return title if len(title) <= max_len else title[:max_len] + "\u2026"
def _content_fields(obj: object, exclude: set[str]) -> tuple[object, ...]:
"""Return a tuple of content field values for equality/hashing.
Lists are converted to tuples so the result is hashable.
"""
vals: list[object] = []
vals.extend(
tuple(v) if isinstance(v, list) else v
for k, v in sorted(vars(obj).items())
if k not in exclude
)
return tuple(vals)
_EXCLUDED: set[str] = {"key", "app"}
class CitationBase(ABC):
"""Abstract base class for all citation entry types."""
author: list[str]
title: str
year: int
doi: str | None
url: str | None
note: str | None
key: str
app: str | None
def _init_base(
self,
author: list[str],
title: str,
year: int,
*,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
"""Set common fields shared by all citation types."""
require_non_empty_authors(author, type(self).__name__)
self.author = author
self.title = title
self.year = year
self.doi = doi
self.url = url
self.note = note
self.key = key if key is not None else generate_key(author, year)
self.app = app
def __eq__(self, other: object) -> bool:
if self is other:
return True
if type(self) is not type(other):
return NotImplemented
return _content_fields(self, _EXCLUDED) == _content_fields(other, _EXCLUDED)
def __hash__(self) -> int:
return hash((type(self).__name__, _content_fields(self, _EXCLUDED)))
def summary(self) -> tuple[str, str]:
"""Return ``(app_name, citation_string)``."""
app_name = self.app if self.app is not None else ""
auth = _author_summary(self.author)
excerpt = _title_excerpt(self.title)
return (app_name, f"{auth} {self.year} {excerpt}")
def __repr__(self) -> str:
fields = self._repr_fields()
auto_key = generate_key(self.author, self.year)
if self.key != auto_key:
fields.insert(0, ("key", self.key))
parts = [f" {name}={value!r}," for name, value in fields]
body = "\n".join(parts)
return f"{type(self).__name__}(\n{body}\n)"
def _append_common_bibtex(self, lines: list[str]) -> None:
"""Append doi/url/note BibTeX fields if set."""
if self.doi is not None:
lines.append(_format_bibtex_field("doi", self.doi))
if self.url is not None:
lines.append(_format_bibtex_field("url", self.url))
if self.note is not None:
lines.append(_format_bibtex_field("note", self.note))
def _append_common_optional_repr(self, fields: list[tuple[str, object]]) -> None:
"""Append doi/url/note to repr field list if set."""
if self.doi is not None:
fields.append(("doi", self.doi))
if self.url is not None:
fields.append(("url", self.url))
if self.note is not None:
fields.append(("note", self.note))
def to_dict(self) -> dict[str, object]:
"""Return a JSON-serialisable dict including a ``"type"`` discriminator."""
return {"type": type(self).__name__, **vars(self)}
@classmethod
def from_dict(cls, data: dict[str, object]) -> CitationBase:
"""Reconstruct a citation from a dict produced by :meth:`to_dict`.
Raises ``ValueError`` if the ``"type"`` key is missing or unknown.
"""
data = dict(data) # shallow copy so we don't mutate the caller's dict
type_name = data.pop("type", None)
if type_name is None:
msg = "dict is missing required 'type' key"
raise ValueError(msg)
entry_cls = _ENTRY_TYPES.get(str(type_name))
if entry_cls is None:
msg = f"unknown citation type {type_name!r}"
raise ValueError(msg)
return entry_cls(**data)
@abstractmethod
def _repr_fields(self) -> list[tuple[str, object]]:
"""Return the list of ``(name, value)`` pairs for ``__repr__``."""
# Keep Citation as a public alias for the base class.
Citation = CitationBase
class Article(CitationBase):
"""An ``@article`` BibTeX entry."""
journal: str
volume: int | None
pages: str | None
article_number: str | None
number: int | None
def __init__(
self,
author: list[str],
title: str,
year: int,
journal: str,
*,
volume: int | None = None,
pages: str | None = None,
article_number: str | None = None,
number: int | None = None,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
require_field(journal, "journal", "Article")
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
self.journal = journal
self.volume = volume
self.pages = pages
self.article_number = article_number
self.number = number
def __str__(self) -> str:
lines = [
f"@article{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("journal", self.journal),
_format_bibtex_field("year", str(self.year)),
]
if self.volume is not None:
lines.append(_format_bibtex_field("volume", str(self.volume)))
if self.number is not None:
lines.append(_format_bibtex_field("number", str(self.number)))
if self.pages is not None:
lines.append(_format_bibtex_field("pages", self.pages))
if self.article_number is not None:
lines.append(_format_bibtex_field("article_number", self.article_number))
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
("journal", self.journal),
]
if self.volume is not None:
fields.append(("volume", self.volume))
if self.pages is not None:
fields.append(("pages", self.pages))
if self.article_number is not None:
fields.append(("article_number", self.article_number))
if self.number is not None:
fields.append(("number", self.number))
self._append_common_optional_repr(fields)
return fields
class Book(CitationBase):
"""A ``@book`` BibTeX entry."""
publisher: str
edition: str | None
editor: list[str] | None
def __init__(
self,
author: list[str],
title: str,
year: int,
publisher: str,
*,
edition: str | None = None,
editor: list[str] | None = None,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
require_field(publisher, "publisher", "Book")
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
self.publisher = publisher
self.edition = edition
self.editor = editor
def __str__(self) -> str:
lines = [
f"@book{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("publisher", self.publisher),
_format_bibtex_field("year", str(self.year)),
]
if self.edition is not None:
lines.append(_format_bibtex_field("edition", self.edition))
if self.editor is not None:
lines.append(_format_bibtex_field("editor", _author_str(self.editor)))
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
("publisher", self.publisher),
]
if self.edition is not None:
fields.append(("edition", self.edition))
if self.editor is not None:
fields.append(("editor", self.editor))
self._append_common_optional_repr(fields)
return fields
class InProceedings(CitationBase):
"""An ``@inproceedings`` BibTeX entry."""
booktitle: str
pages: str | None
publisher: str | None
editor: list[str] | None
def __init__(
self,
author: list[str],
title: str,
year: int,
booktitle: str,
*,
pages: str | None = None,
publisher: str | None = None,
editor: list[str] | None = None,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
require_field(booktitle, "booktitle", "InProceedings")
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
self.booktitle = booktitle
self.pages = pages
self.publisher = publisher
self.editor = editor
def __str__(self) -> str:
lines = [
f"@inproceedings{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("booktitle", self.booktitle),
_format_bibtex_field("year", str(self.year)),
]
if self.pages is not None:
lines.append(_format_bibtex_field("pages", self.pages))
if self.publisher is not None:
lines.append(_format_bibtex_field("publisher", self.publisher))
if self.editor is not None:
lines.append(_format_bibtex_field("editor", _author_str(self.editor)))
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
("booktitle", self.booktitle),
]
if self.pages is not None:
fields.append(("pages", self.pages))
if self.publisher is not None:
fields.append(("publisher", self.publisher))
if self.editor is not None:
fields.append(("editor", self.editor))
self._append_common_optional_repr(fields)
return fields
class TechReport(CitationBase):
"""A ``@techreport`` BibTeX entry."""
institution: str
number: str | None
def __init__(
self,
author: list[str],
title: str,
year: int,
institution: str,
*,
number: str | None = None,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
require_field(institution, "institution", "TechReport")
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
self.institution = institution
self.number = number
def __str__(self) -> str:
lines = [
f"@techreport{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("institution", self.institution),
_format_bibtex_field("year", str(self.year)),
]
if self.number is not None:
lines.append(_format_bibtex_field("number", self.number))
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
("institution", self.institution),
]
if self.number is not None:
fields.append(("number", self.number))
self._append_common_optional_repr(fields)
return fields
class Thesis(CitationBase):
"""A ``@phdthesis`` or ``@mastersthesis`` BibTeX entry."""
school: str
thesis_type: str
def __init__(
self,
author: list[str],
title: str,
year: int,
school: str,
thesis_type: str,
*,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
require_field(school, "school", "Thesis")
require_field(thesis_type, "thesis_type", "Thesis")
if thesis_type not in ("phd", "masters"):
msg = f"Thesis thesis_type must be 'phd' or 'masters'; got {thesis_type!r}"
raise ValueError(msg)
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
self.school = school
self.thesis_type = thesis_type
def __str__(self) -> str:
bib_type = "phdthesis" if self.thesis_type == "phd" else "mastersthesis"
lines = [
f"@{bib_type}{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("school", self.school),
_format_bibtex_field("year", str(self.year)),
]
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
("school", self.school),
("thesis_type", self.thesis_type),
]
self._append_common_optional_repr(fields)
return fields
class Software(CitationBase):
"""A ``@software`` BibTeX entry."""
publisher: str | None
version: str | None
license: str | None
def __init__(
self,
author: list[str],
title: str,
year: int,
*,
publisher: str | None = None,
version: str | None = None,
license: str | None = None,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
self.publisher = publisher
self.version = version
self.license = license
def __str__(self) -> str:
lines = [
f"@software{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("year", str(self.year)),
]
if self.publisher is not None:
lines.append(_format_bibtex_field("publisher", self.publisher))
if self.version is not None:
lines.append(_format_bibtex_field("version", self.version))
if self.license is not None:
lines.append(_format_bibtex_field("license", self.license))
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
]
if self.publisher is not None:
fields.append(("publisher", self.publisher))
if self.version is not None:
fields.append(("version", self.version))
if self.license is not None:
fields.append(("license", self.license))
self._append_common_optional_repr(fields)
return fields
class Misc(CitationBase):
"""A ``@misc`` BibTeX entry."""
def __init__(
self,
author: list[str],
title: str,
year: int,
*,
doi: str | None = None,
url: str | None = None,
note: str | None = None,
key: str | None = None,
app: str | None = None,
) -> None:
self._init_base(
author, title, year, doi=doi, url=url, note=note, key=key, app=app
)
def __str__(self) -> str:
lines = [
f"@misc{{{self.key},",
_format_bibtex_field("author", _author_str(self.author)),
_format_bibtex_field("title", self.title),
_format_bibtex_field("year", str(self.year)),
]
self._append_common_bibtex(lines)
lines.append("}")
return "\n".join(lines)
def _repr_fields(self) -> list[tuple[str, object]]:
fields: list[tuple[str, object]] = [
("author", self.author),
("title", self.title),
("year", self.year),
]
self._append_common_optional_repr(fields)
return fields
_ENTRY_TYPES: dict[str, type[CitationBase]] = {
"Article": Article,
"Book": Book,
"InProceedings": InProceedings,
"TechReport": TechReport,
"Thesis": Thesis,
"Software": Software,
"Misc": Misc,
}
cogent3-citeable-7188740/src/citeable/_json.py 0000664 0000000 0000000 00000001722 15154651363 0021062 0 ustar 00root root 0000000 0000000 """JSON serialisation helpers for citation collections."""
from __future__ import annotations
import json
import pathlib
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable
from citeable._entries import CitationBase
def to_jsons(citations: Iterable[CitationBase]) -> str:
"""Return a JSON string from an iterable of citations."""
return json.dumps([c.to_dict() for c in citations])
def from_jsons(data: str) -> list[CitationBase]:
"""Return a list of citations from a JSON string."""
return [CitationBase.from_dict(d) for d in json.loads(data)]
def write_json(*, citations: Iterable[CitationBase], path: str | pathlib.Path) -> None:
"""Write citations to a JSON file at *path*."""
pathlib.Path(path).write_text(to_jsons(citations))
def load_json(path: str | pathlib.Path) -> list[CitationBase]:
"""Read citations from a JSON file at *path*."""
return from_jsons(pathlib.Path(path).read_text())
cogent3-citeable-7188740/src/citeable/_keys.py 0000664 0000000 0000000 00000003513 15154651363 0021064 0 ustar 00root root 0000000 0000000 """Auto key generation, unique key assignment, and .bib file writing."""
from __future__ import annotations
import string
from pathlib import Path
from typing import TYPE_CHECKING
from citeable._validate import extract_surname
if TYPE_CHECKING:
from citeable._entries import CitationBase
def generate_key(author: list[str], year: int) -> str:
"""Generate a citation key from the first author surname and year.
Algorithm:
1. Extract surname from the first author (before comma, or last token).
2. Strip non-ASCII and spaces; preserve original case.
3. Return ``"{surname}.{year}"``.
"""
surname = extract_surname(author[0])
return f"{surname}.{year}"
def assign_unique_keys(citations: list[CitationBase]) -> list[CitationBase]:
"""Deduplicate *citations* by value and resolve key collisions in-place.
Returns the deduplicated list (mutated in-place for surviving objects).
"""
seen: dict[CitationBase, CitationBase] = {}
unique: list[CitationBase] = []
for c in citations:
if c not in seen:
seen[c] = c
unique.append(c)
key_groups: dict[str, list[CitationBase]] = {}
for c in unique:
key_groups.setdefault(c.key, []).append(c)
for key, group in key_groups.items():
if len(group) <= 1:
continue
if len(group) > 26:
msg = f"More than 26 collisions for key {key!r}"
raise ValueError(msg)
for i, c in enumerate(group):
c.key = f"{key}.{string.ascii_lowercase[i]}"
return unique
def write_bibtex(citations: list[CitationBase], path: str | Path) -> None:
"""Deduplicate, assign unique keys, and write a ``.bib`` file."""
unique = assign_unique_keys(citations)
content = "\n\n".join(str(c) for c in unique)
Path(path).write_text(content, encoding="utf-8")
cogent3-citeable-7188740/src/citeable/_parser.py 0000664 0000000 0000000 00000015222 15154651363 0021405 0 ustar 00root root 0000000 0000000 """BibTeX string parser — single-record ``from_bibtex_string``."""
from __future__ import annotations
import re
from collections.abc import Callable
from typing import Any
from citeable._entries import (
Article,
Book,
CitationBase,
InProceedings,
Misc,
Software,
TechReport,
Thesis,
)
_ENTRY_START_RE = re.compile(r"@(\w+)\s*\{")
_TYPE_MAP: dict[str, type[CitationBase]] = {
"article": Article,
"book": Book,
"inproceedings": InProceedings,
"techreport": TechReport,
"phdthesis": Thesis,
"mastersthesis": Thesis,
"software": Software,
"misc": Misc,
}
def _extract_entries(bibtex: str) -> list[tuple[str, str, str]]:
"""Return list of ``(entry_type, cite_key, body)`` tuples."""
results: list[tuple[str, str, str]] = []
for m in _ENTRY_START_RE.finditer(bibtex):
entry_type = m.group(1)
start = m.end()
depth = 1
pos = start
while pos < len(bibtex) and depth > 0:
if bibtex[pos] == "{":
depth += 1
elif bibtex[pos] == "}":
depth -= 1
pos += 1
body = bibtex[start : pos - 1]
comma_idx = body.find(",")
if comma_idx == -1:
cite_key = body.strip()
fields_body = ""
else:
cite_key = body[:comma_idx].strip()
fields_body = body[comma_idx + 1 :]
results.append((entry_type, cite_key, fields_body))
return results
def _normalise_author(name: str) -> str:
"""Normalise an author name to ``"Last, First"`` format."""
name = name.strip()
if "," in name:
return name
parts = name.split()
return name if len(parts) <= 1 else f"{parts[-1]}, {' '.join(parts[:-1])}"
def _parse_authors(raw: str) -> list[str]:
return [_normalise_author(a) for a in raw.split(" and ")]
_FIELD_START_RE = re.compile(r"^\s*(\w+)\s*=\s*", re.MULTILINE)
def _extract_value(raw: str) -> str:
"""Extract a field value from the text after ``=``.
Handles brace-delimited (with nesting), quoted, and bare values.
"""
raw = raw.strip()
if raw.startswith("{"):
depth = 0
for i, ch in enumerate(raw):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return raw[1:i]
elif raw.startswith('"'):
end = raw.index('"', 1)
return raw[1:end]
else:
return raw.rstrip(",").strip()
return raw
def _parse_fields(body: str) -> dict[str, str]:
"""Parse BibTeX fields supporting braced, quoted, and bare values."""
matches = list(_FIELD_START_RE.finditer(body))
fields: dict[str, str] = {}
for i, m in enumerate(matches):
name = m.group(1).lower()
val_start = m.end()
val_end = matches[i + 1].start() if i + 1 < len(matches) else len(body)
fields[name] = _extract_value(body[val_start:val_end])
return fields
def _common_kwargs(
fields: dict[str, str],
cite_key: str,
) -> dict[str, Any]:
kwargs: dict[str, Any] = {"key": cite_key}
if "author" in fields:
kwargs["author"] = _parse_authors(fields["author"])
if "title" in fields:
kwargs["title"] = fields["title"]
if "year" in fields:
kwargs["year"] = int(fields["year"])
for name in ("doi", "url", "note"):
if name in fields:
kwargs[name] = fields[name]
return kwargs
def _article_kwargs(fields: dict[str, str]) -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if "journal" in fields:
kwargs["journal"] = fields["journal"]
if "volume" in fields:
kwargs["volume"] = int(fields["volume"])
if "pages" in fields:
kwargs["pages"] = fields["pages"]
if "article_number" in fields:
kwargs["article_number"] = fields["article_number"]
if "number" in fields:
kwargs["number"] = int(fields["number"])
return kwargs
def _book_kwargs(fields: dict[str, str]) -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if "publisher" in fields:
kwargs["publisher"] = fields["publisher"]
if "edition" in fields:
kwargs["edition"] = fields["edition"]
if "editor" in fields:
kwargs["editor"] = _parse_authors(fields["editor"])
return kwargs
def _inproceedings_kwargs(fields: dict[str, str]) -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if "booktitle" in fields:
kwargs["booktitle"] = fields["booktitle"]
if "pages" in fields:
kwargs["pages"] = fields["pages"]
if "publisher" in fields:
kwargs["publisher"] = fields["publisher"]
if "editor" in fields:
kwargs["editor"] = _parse_authors(fields["editor"])
return kwargs
def _techreport_kwargs(fields: dict[str, str]) -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if "institution" in fields:
kwargs["institution"] = fields["institution"]
if "number" in fields:
kwargs["number"] = fields["number"]
return kwargs
def _thesis_kwargs(
fields: dict[str, str],
entry_type: str,
) -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if "school" in fields:
kwargs["school"] = fields["school"]
kwargs["thesis_type"] = "phd" if entry_type == "phdthesis" else "masters"
return kwargs
def _software_kwargs(fields: dict[str, str]) -> dict[str, Any]:
kwargs: dict[str, Any] = {
name: fields[name]
for name in ("publisher", "version", "license")
if name in fields
}
return kwargs
_KwargsFunc = Callable[[dict[str, str]], dict[str, Any]]
_TYPE_KWARGS: dict[type[CitationBase], _KwargsFunc] = {
Article: _article_kwargs,
Book: _book_kwargs,
InProceedings: _inproceedings_kwargs,
TechReport: _techreport_kwargs,
Software: _software_kwargs,
}
def from_bibtex_string(bibtex: str) -> CitationBase:
"""Parse a single BibTeX record and return the corresponding object."""
entries = _extract_entries(bibtex)
if not entries:
msg = "No BibTeX entry found in input string"
raise ValueError(msg)
if len(entries) > 1:
msg = "Multiple BibTeX entries found; only single-record strings are supported"
raise ValueError(msg)
entry_type_raw, cite_key, body = entries[0]
entry_type = entry_type_raw.lower()
if entry_type not in _TYPE_MAP:
msg = f"Unsupported BibTeX entry type: @{entry_type}"
raise ValueError(msg)
cls = _TYPE_MAP[entry_type]
fields = _parse_fields(body)
kwargs = _common_kwargs(fields, cite_key)
if cls is Thesis:
kwargs.update(_thesis_kwargs(fields, entry_type))
elif cls in _TYPE_KWARGS:
kwargs.update(_TYPE_KWARGS[cls](fields))
return cls(**kwargs)
cogent3-citeable-7188740/src/citeable/_validate.py 0000664 0000000 0000000 00000002057 15154651363 0021704 0 ustar 00root root 0000000 0000000 """Shared validation logic for citeable entry types."""
from __future__ import annotations
import re
def require_field(
value: object,
field_name: str,
entry_type: str,
) -> None:
"""Raise ``ValueError`` if *value* is ``None``."""
if value is None:
msg = f"{entry_type} requires {field_name!r}; received None"
raise ValueError(msg)
def require_non_empty_authors(authors: list[str], entry_type: str) -> None:
"""Raise ``ValueError`` if *authors* is empty."""
if not authors:
msg = f"{entry_type} requires at least one author"
raise ValueError(msg)
def extract_surname(name: str) -> str:
"""Extract surname from an author name string.
Handles both ``"Last, First"`` and ``"First Last"`` formats.
Returns the surname stripped of non-ASCII characters, preserving original case.
"""
if "," in name:
surname = name.split(",", maxsplit=1)[0].strip()
else:
surname = name.rsplit(maxsplit=1)[-1].strip()
surname = re.sub(r"[^A-Za-z]", "", surname)
return surname
cogent3-citeable-7188740/src/citeable/py.typed 0000664 0000000 0000000 00000000000 15154651363 0021063 0 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/tests/ 0000775 0000000 0000000 00000000000 15154651363 0016201 5 ustar 00root root 0000000 0000000 cogent3-citeable-7188740/tests/test_entries.py 0000664 0000000 0000000 00000024475 15154651363 0021277 0 ustar 00root root 0000000 0000000 """Tests for citation entry type construction and validation."""
import pytest
from citeable import (
Article,
Book,
InProceedings,
Misc,
Software,
TechReport,
Thesis,
)
# ── Article ──────────────────────────────────────────────────────────────
def test_article_construction():
a = Article(
author=["Huttley, Gavin", "Caley, Katherine"],
title="A paper",
year=2025,
journal="JOSS",
volume=10,
pages="7765",
)
assert a.author == ["Huttley, Gavin", "Caley, Katherine"]
assert a.journal == "JOSS"
assert a.volume == 10
assert a.pages == "7765"
assert a.key == "Huttley.2025"
def test_article_with_article_number():
a = Article(
author=["Smith, Jane"],
title="Something",
year=2024,
journal="Nature",
volume=1,
article_number="e123",
)
assert a.article_number == "e123"
assert a.pages is None
def test_article_advance_access():
a = Article(
author=["Smith, Jane"],
title="Something",
year=2024,
journal="Nature",
doi="10.1234/example",
)
assert a.volume is None
assert a.pages is None
assert a.article_number is None
def test_article_missing_journal():
with pytest.raises(ValueError, match="Article requires 'journal'"):
Article(
author=["Smith, Jane"],
title="Something",
year=2024,
journal=None, # type: ignore[arg-type]
volume=1,
pages="1-10",
)
def test_article_empty_authors():
with pytest.raises(ValueError, match="at least one author"):
Article(
author=[],
title="Something",
year=2024,
journal="Nature",
volume=1,
pages="1-10",
)
# ── Book ─────────────────────────────────────────────────────────────────
def test_book_construction():
b = Book(
author=["Knuth, Donald"],
title="The Art of Programming",
year=1997,
publisher="Addison-Wesley",
)
assert b.publisher == "Addison-Wesley"
assert b.key == "Knuth.1997"
def test_book_missing_publisher():
with pytest.raises(ValueError, match="Book requires 'publisher'"):
Book(
author=["Knuth, Donald"],
title="The Art",
year=1997,
publisher=None, # type: ignore[arg-type]
)
# ── InProceedings ────────────────────────────────────────────────────────
def test_inproceedings_construction():
ip = InProceedings(
author=["Doe, John"],
title="A Conference Paper",
year=2023,
booktitle="Proceedings of Foo",
)
assert ip.booktitle == "Proceedings of Foo"
assert ip.key == "Doe.2023"
def test_inproceedings_missing_booktitle():
with pytest.raises(ValueError, match="InProceedings requires 'booktitle'"):
InProceedings(
author=["Doe, John"],
title="Paper",
year=2023,
booktitle=None, # type: ignore[arg-type]
)
# ── TechReport ───────────────────────────────────────────────────────────
def test_techreport_construction():
tr = TechReport(
author=["Turing, Alan"],
title="On Computable Numbers",
year=1936,
institution="Cambridge",
)
assert tr.institution == "Cambridge"
assert tr.key == "Turing.1936"
def test_techreport_missing_institution():
with pytest.raises(ValueError, match="TechReport requires 'institution'"):
TechReport(
author=["Turing, Alan"],
title="A Report",
year=1936,
institution=None, # type: ignore[arg-type]
)
# ── Thesis ───────────────────────────────────────────────────────────────
def test_thesis_phd():
t = Thesis(
author=["Student, Alice"],
title="My Thesis",
year=2022,
school="MIT",
thesis_type="phd",
)
assert t.thesis_type == "phd"
assert t.school == "MIT"
def test_thesis_masters():
t = Thesis(
author=["Student, Bob"],
title="My Thesis",
year=2021,
school="Oxford",
thesis_type="masters",
)
assert t.thesis_type == "masters"
def test_thesis_invalid_type():
with pytest.raises(ValueError, match="'phd' or 'masters'"):
Thesis(
author=["Student, Alice"],
title="My Thesis",
year=2022,
school="MIT",
thesis_type="bachelor",
)
def test_thesis_missing_school():
with pytest.raises(ValueError, match="Thesis requires 'school'"):
Thesis(
author=["Student, Alice"],
title="My Thesis",
year=2022,
school=None, # type: ignore[arg-type]
thesis_type="phd",
)
# ── Software ─────────────────────────────────────────────────────────────
def test_software_construction():
s = Software(
author=["Dev, Jane"],
title="my-tool",
year=2024,
version="1.0.0",
url="https://github.com/example",
)
assert s.version == "1.0.0"
assert s.key == "Dev.2024"
def test_software_minimal():
s = Software(
author=["Dev, Jane"],
title="my-tool",
year=2024,
)
assert s.version is None
assert s.publisher is None
# ── Misc ─────────────────────────────────────────────────────────────────
def test_misc_construction():
m = Misc(
author=["Author, Some"],
title="A Misc Entry",
year=2020,
note="Some note",
)
assert m.note == "Some note"
assert m.key == "Author.2020"
# ── Equality and hashing ────────────────────────────────────────────────
def test_equality_same_content():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
b = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
assert a == b
assert hash(a) == hash(b)
def test_equality_excludes_key_and_app():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
key="custom.key",
app="app1",
)
b = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
key="other.key",
app="app2",
)
assert a == b
assert hash(a) == hash(b)
def test_identity_equality():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
b = a
assert a is b
assert a == b
def test_inequality_different_content():
a = Article(
author=["Smith, A"],
title="Paper 1",
year=2024,
journal="J",
volume=1,
pages="1",
)
b = Article(
author=["Smith, A"],
title="Paper 2",
year=2024,
journal="J",
volume=1,
pages="1",
)
assert a != b
def test_inequality_different_types():
a = Misc(author=["Smith, A"], title="Thing", year=2024)
b = Software(author=["Smith, A"], title="Thing", year=2024)
assert a != b
def test_set_deduplication():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
b = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
key="different",
)
assert len({a, b}) == 1
def test_author_order_matters():
a = Article(
author=["Smith, A", "Jones, B"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
b = Article(
author=["Jones, B", "Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
assert a != b
# ── key auto-generation ─────────────────────────────────────────────────
def test_key_auto_generated():
a = Article(
author=["Huttley, Gavin"],
title="Paper",
year=2025,
journal="J",
volume=1,
pages="1",
)
assert a.key == "Huttley.2025"
def test_key_explicit():
a = Article(
author=["Huttley, Gavin"],
title="Paper",
year=2025,
journal="J",
volume=1,
pages="1",
key="custom",
)
assert a.key == "custom"
def test_key_from_first_last_format():
a = Misc(author=["Gavin Huttley"], title="Paper", year=2025)
assert a.key == "Huttley.2025"
# ── app field ────────────────────────────────────────────────────────────
def test_app_field():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
app="my-plugin",
)
assert a.app == "my-plugin"
def test_app_default_none():
a = Misc(author=["Smith, A"], title="Paper", year=2024)
assert a.app is None
cogent3-citeable-7188740/tests/test_keys.py 0000664 0000000 0000000 00000007322 15154651363 0020571 0 ustar 00root root 0000000 0000000 """Tests for key generation and assign_unique_keys."""
import pytest
from citeable import Article, Misc, Software, assign_unique_keys
from citeable._keys import generate_key
from citeable._validate import extract_surname
# ── generate_key ─────────────────────────────────────────────────────────
def test_generate_key_comma_format():
assert generate_key(["Huttley, Gavin"], 2025) == "Huttley.2025"
def test_generate_key_space_format():
assert generate_key(["Gavin Huttley"], 2025) == "Huttley.2025"
def test_generate_key_strips_non_ascii():
assert generate_key(["Müller, Hans"], 2020) == "Mller.2020"
def test_generate_key_preserves_case():
assert generate_key(["van der berg, Jan"], 2020) == "vanderberg.2020"
def test_generate_key_camel_case_preserved():
assert generate_key(["McArthur, Robert"], 2021) == "McArthur.2021"
assert generate_key(["McDonald, Daniel"], 2022) == "McDonald.2022"
def test_extract_surname_preserves_case():
assert extract_surname("McArthur, Robert") == "McArthur"
assert extract_surname("McDonald, Daniel") == "McDonald"
# ── assign_unique_keys ──────────────────────────────────────────────────
def test_assign_unique_keys_no_collision():
a = Misc(author=["Smith, A"], title="Paper A", year=2024)
b = Misc(author=["Jones, B"], title="Paper B", year=2024)
result = assign_unique_keys([a, b])
assert len(result) == 2
assert a.key == "Smith.2024"
assert b.key == "Jones.2024"
def test_assign_unique_keys_collision():
a = Misc(author=["Smith, A"], title="Paper A", year=2024)
b = Misc(author=["Smith, B"], title="Paper B", year=2024)
result = assign_unique_keys([a, b])
assert len(result) == 2
assert a.key == "Smith.2024.a"
assert b.key == "Smith.2024.b"
def test_assign_unique_keys_deduplication():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
app="app1",
)
b = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
app="app2",
)
result = assign_unique_keys([a, b])
assert len(result) == 1
assert result[0] is a
def test_assign_unique_keys_preserves_order():
a = Misc(author=["Smith, A"], title="First", year=2024)
b = Misc(author=["Smith, B"], title="Second", year=2024)
c = Misc(author=["Smith, C"], title="Third", year=2024)
result = assign_unique_keys([a, b, c])
assert result == [a, b, c]
assert a.key == "Smith.2024.a"
assert b.key == "Smith.2024.b"
assert c.key == "Smith.2024.c"
def test_assign_unique_keys_returns_list():
a = Misc(author=["Smith, A"], title="Paper", year=2024)
result = assign_unique_keys([a])
assert isinstance(result, list)
assert result == [a]
def test_assign_unique_keys_empty():
result = assign_unique_keys([])
assert result == []
def test_assign_unique_keys_too_many_collisions():
citations = [
Misc(author=["Smith, A"], title=f"Paper {i}", year=2024) for i in range(27)
]
with pytest.raises(ValueError, match="More than 26 collisions"):
assign_unique_keys(citations)
def test_assign_unique_keys_mixed_types_same_key():
a = Misc(author=["Smith, A"], title="Misc Paper", year=2024)
b = Software(author=["Smith, B"], title="Software", year=2024)
result = assign_unique_keys([a, b])
assert len(result) == 2
assert a.key == "Smith.2024.a"
assert b.key == "Smith.2024.b"
cogent3-citeable-7188740/tests/test_parser.py 0000664 0000000 0000000 00000014533 15154651363 0021114 0 ustar 00root root 0000000 0000000 """Tests for from_bibtex_string parser."""
import pytest
from citeable import (
Article,
Book,
InProceedings,
Misc,
Software,
TechReport,
Thesis,
from_bibtex_string,
)
def test_parse_article():
cite = from_bibtex_string("""
@article{Huttley.2025,
doi = {10.21105/joss.07765},
url = {https://doi.org/10.21105/joss.07765},
year = {2025},
volume = {10},
number = {110},
pages = {7765},
author = {Huttley, Gavin and Caley, Katherine and McArthur, Robert},
title = {diverse-seq},
journal = {Journal of Open Source Software},
}
""")
assert isinstance(cite, Article)
assert cite.key == "Huttley.2025"
assert cite.author == ["Huttley, Gavin", "Caley, Katherine", "McArthur, Robert"]
assert cite.journal == "Journal of Open Source Software"
assert cite.volume == 10
assert cite.number == 110
assert cite.pages == "7765"
assert cite.doi == "10.21105/joss.07765"
def test_parse_software_1():
cite = from_bibtex_string("""@software{cogent3,
title = {{cogent3}: making sense of sequence},
author = {Huttley, Gavin and Caley, Katherine and Fotovat, Nabi and Ma, Stephen Ka-Wah and Koh, Moses and Morris, Richard and McArthur, Robert and McDonald, Daniel and Jaya, Fred and Maxwell, Peter and Martini, James and La, Thomas and Lang, Yapeng},
year = 2025,
month = jul,
doi = {10.5281/zenodo.16519079},
urldate = {2025-08-02},
howpublished = {Zenodo}
}
""")
assert isinstance(cite, Software)
def test_parse_book():
cite = from_bibtex_string("""
@book{Knuth.1997,
author = {Knuth, Donald},
title = {The Art of Computer Programming},
publisher = {Addison-Wesley},
year = {1997},
edition = {3rd},
}
""")
assert isinstance(cite, Book)
assert cite.publisher == "Addison-Wesley"
assert cite.edition == "3rd"
def test_parse_inproceedings():
cite = from_bibtex_string("""
@inproceedings{Doe.2023,
author = {Doe, John},
title = {A Paper},
booktitle = {Proceedings of Foo},
year = {2023},
pages = {1--10},
}
""")
assert isinstance(cite, InProceedings)
assert cite.booktitle == "Proceedings of Foo"
assert cite.pages == "1--10"
def test_parse_techreport():
cite = from_bibtex_string("""
@techreport{Turing.1936,
author = {Turing, Alan},
title = {On Computable Numbers},
institution = {Cambridge},
year = {1936},
number = {TR-42},
}
""")
assert isinstance(cite, TechReport)
assert cite.institution == "Cambridge"
assert cite.number == "TR-42"
def test_parse_phdthesis():
cite = from_bibtex_string("""
@phdthesis{Student.2022,
author = {Student, Alice},
title = {My Thesis},
school = {MIT},
year = {2022},
}
""")
assert isinstance(cite, Thesis)
assert cite.thesis_type == "phd"
assert cite.school == "MIT"
def test_parse_mastersthesis():
cite = from_bibtex_string("""
@mastersthesis{Student.2021,
author = {Student, Bob},
title = {My Thesis},
school = {Oxford},
year = {2021},
}
""")
assert isinstance(cite, Thesis)
assert cite.thesis_type == "masters"
def test_parse_software():
cite = from_bibtex_string("""
@software{Dev.2024,
author = {Dev, Jane},
title = {my-tool},
year = {2024},
version = {1.0.0},
url = {https://github.com/example},
}
""")
assert isinstance(cite, Software)
assert cite.version == "1.0.0"
def test_parse_misc():
cite = from_bibtex_string("""
@misc{Author.2020,
author = {Author, Some},
title = {A Misc Entry},
year = {2020},
note = {Some note},
}
""")
assert isinstance(cite, Misc)
assert cite.note == "Some note"
def test_parse_author_normalisation():
cite = from_bibtex_string("""
@misc{Smith.2024,
author = {John Smith and Jane Doe},
title = {A Paper},
year = {2024},
}
""")
assert cite.author == ["Smith, John", "Doe, Jane"]
def test_parse_single_name_author():
"""Single-name authors (e.g. mononyms) are kept as-is."""
cite = from_bibtex_string("""
@misc{Aristotle.2024,
author = {Aristotle},
title = {Metaphysics},
year = {2024},
}
""")
assert cite.author == ["Aristotle"]
def test_parse_single_name_among_multiple_authors():
cite = from_bibtex_string("""
@misc{Plato.2024,
author = {Plato and Jane Smith},
title = {Dialogues},
year = {2024},
}
""")
assert cite.author == ["Plato", "Smith, Jane"]
def test_parse_preserves_cite_key():
cite = from_bibtex_string("""
@misc{my_custom_key,
author = {Smith, John},
title = {A Paper},
year = {2024},
}
""")
assert cite.key == "my_custom_key"
def test_parse_article_with_article_number():
cite = from_bibtex_string("""
@article{Smith.2024,
author = {Smith, John},
title = {A Paper},
journal = {Nature},
year = {2024},
volume = {1},
article_number = {e42},
}
""")
assert isinstance(cite, Article)
assert cite.article_number == "e42"
def test_parse_book_with_editor():
cite = from_bibtex_string("""
@book{Knuth.1997,
author = {Knuth, Donald},
title = {TAOCP},
publisher = {Addison-Wesley},
year = {1997},
editor = {Smith, Jane and Doe, John},
}
""")
assert isinstance(cite, Book)
assert cite.editor == ["Smith, Jane", "Doe, John"]
def test_parse_inproceedings_with_publisher_and_editor():
cite = from_bibtex_string("""
@inproceedings{Doe.2023,
author = {Doe, John},
title = {A Paper},
booktitle = {Proceedings of Foo},
year = {2023},
publisher = {ACM},
editor = {Chair, Ed},
}
""")
assert isinstance(cite, InProceedings)
assert cite.publisher == "ACM"
assert cite.editor == ["Chair, Ed"]
def test_parse_unsupported_type():
with pytest.raises(ValueError, match="Unsupported BibTeX entry type"):
from_bibtex_string("""
@proceedings{Foo.2024,
author = {Smith, John},
title = {A Paper},
year = {2024},
}
""")
def test_parse_no_entry():
with pytest.raises(ValueError, match="No BibTeX entry found"):
from_bibtex_string("not a bibtex string")
def test_parse_multiple_entries():
with pytest.raises(ValueError, match="Multiple BibTeX entries"):
from_bibtex_string("""
@misc{A.2024,
author = {A, B},
title = {First},
year = {2024},
}
@misc{C.2024,
author = {C, D},
title = {Second},
year = {2024},
}
""")
cogent3-citeable-7188740/tests/test_serialise.py 0000664 0000000 0000000 00000032463 15154651363 0021602 0 ustar 00root root 0000000 0000000 """Tests for __str__, __repr__, summary(), and JSON serialisation."""
import pathlib
import pytest
from citeable import (
Article,
Book,
CitationBase,
InProceedings,
Misc,
Software,
TechReport,
Thesis,
from_jsons,
load_json,
to_jsons,
write_json,
)
# ── __str__ (BibTeX output) ─────────────────────────────────────────────
def test_article_str():
a = Article(
author=["Huttley, Gavin", "Caley, Katherine", "McArthur, Robert"],
title="diverse-seq",
journal="Journal of Open Source Software",
year=2025,
volume=10,
number=110,
pages="7765",
doi="10.21105/joss.07765",
url="https://doi.org/10.21105/joss.07765",
)
result = str(a)
assert result.startswith("@article{Huttley.2025,")
assert "author" in result
assert "Huttley, Gavin and Caley, Katherine and McArthur, Robert" in result
assert "journal" in result
assert "volume" in result
assert "number" in result
assert "pages" in result
assert "doi" in result
assert "url" in result
assert result.endswith("}")
def test_thesis_str_phd():
t = Thesis(
author=["Student, Alice"],
title="My Thesis",
year=2022,
school="MIT",
thesis_type="phd",
)
result = str(t)
assert result.startswith("@phdthesis{")
def test_thesis_str_masters():
t = Thesis(
author=["Student, Bob"],
title="My Thesis",
year=2021,
school="Oxford",
thesis_type="masters",
)
result = str(t)
assert result.startswith("@mastersthesis{")
def test_software_str():
s = Software(
author=["Dev, Jane"],
title="my-tool",
year=2024,
version="1.0.0",
)
result = str(s)
assert result.startswith("@software{")
assert "version" in result
def test_article_str_with_article_number():
a = Article(
author=["Smith, A"],
title="Paper",
journal="J",
year=2024,
volume=1,
article_number="e123",
note="A note",
)
result = str(a)
assert "article_number" in result
assert "e123" in result
assert "note" in result
def test_book_str_all_optional_fields():
b = Book(
author=["Knuth, Donald"],
title="TAOCP",
publisher="Addison-Wesley",
year=1997,
edition="3rd",
editor=["Smith, Jane"],
doi="10.1234/book",
url="https://example.com",
note="Classic text",
)
result = str(b)
assert result.startswith("@book{")
assert "edition" in result
assert "editor" in result
assert "Smith, Jane" in result
assert "doi" in result
assert "url" in result
assert "note" in result
def test_inproceedings_str_all_optional_fields():
ip = InProceedings(
author=["Doe, John"],
title="A Paper",
booktitle="Proceedings of Foo",
year=2023,
pages="1--10",
publisher="ACM",
editor=["Chair, Ed"],
doi="10.1234/conf",
url="https://example.com",
note="Oral presentation",
)
result = str(ip)
assert result.startswith("@inproceedings{")
assert "pages" in result
assert "publisher" in result
assert "editor" in result
assert "doi" in result
assert "url" in result
assert "note" in result
def test_techreport_str_all_optional_fields():
tr = TechReport(
author=["Turing, Alan"],
title="On Computable Numbers",
institution="Cambridge",
year=1936,
number="TR-42",
doi="10.1234/tr",
url="https://example.com",
note="Historic",
)
result = str(tr)
assert result.startswith("@techreport{")
assert "number" in result
assert "doi" in result
assert "url" in result
assert "note" in result
def test_software_str_all_optional_fields():
s = Software(
author=["Dev, Jane"],
title="my-tool",
year=2024,
publisher="GitHub",
version="1.0.0",
license="MIT",
doi="10.1234/sw",
url="https://github.com/example",
note="Beta release",
)
result = str(s)
assert result.startswith("@software{")
assert "publisher" in result
assert "version" in result
assert "license" in result
assert "doi" in result
assert "note" in result
def test_misc_str_omits_none_fields():
m = Misc(
author=["Smith, A"],
title="Something",
year=2024,
)
result = str(m)
assert "doi" not in result
assert "url" not in result
assert "note" not in result
# ── __repr__ ─────────────────────────────────────────────────────────────
def test_article_repr_omits_auto_key():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
r = repr(a)
assert r.startswith("Article(")
assert "key=" not in r
def test_article_repr_includes_explicit_key():
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
key="custom.key",
)
r = repr(a)
assert "key='custom.key'" in r
def test_repr_round_trip():
a = Article(
author=["Huttley, Gavin", "Caley, Katherine"],
title="diverse-seq",
journal="JOSS",
year=2025,
volume=10,
pages="7765",
doi="10.21105/joss.07765",
)
r = repr(a)
assert "Article(" in r
assert "author=" in r
assert "title=" in r
assert "journal=" in r
def test_article_repr_with_article_number_and_number():
a = Article(
author=["Smith, A"],
title="Paper",
journal="J",
year=2024,
volume=1,
article_number="e42",
number=3,
url="https://example.com",
note="See also v2",
)
r = repr(a)
assert "article_number='e42'" in r
assert "number=3" in r
assert "url=" in r
assert "note=" in r
def test_book_repr_all_optional_fields():
b = Book(
author=["Knuth, Donald"],
title="TAOCP",
publisher="Addison-Wesley",
year=1997,
edition="3rd",
editor=["Smith, Jane"],
doi="10.1234/book",
)
r = repr(b)
assert r.startswith("Book(")
assert "edition=" in r
assert "editor=" in r
assert "doi=" in r
def test_inproceedings_repr_all_optional_fields():
ip = InProceedings(
author=["Doe, John"],
title="A Paper",
booktitle="Proceedings of Foo",
year=2023,
pages="1--10",
publisher="ACM",
editor=["Chair, Ed"],
)
r = repr(ip)
assert r.startswith("InProceedings(")
assert "pages=" in r
assert "publisher=" in r
assert "editor=" in r
def test_techreport_repr_with_number():
tr = TechReport(
author=["Turing, Alan"],
title="On Computable Numbers",
institution="Cambridge",
year=1936,
number="TR-42",
)
r = repr(tr)
assert r.startswith("TechReport(")
assert "number=" in r
def test_thesis_repr_with_optional_fields():
t = Thesis(
author=["Student, Alice"],
title="My Thesis",
year=2022,
school="MIT",
thesis_type="phd",
doi="10.1234/thesis",
url="https://example.com",
note="Summa cum laude",
)
r = repr(t)
assert r.startswith("Thesis(")
assert "doi=" in r
assert "url=" in r
assert "note=" in r
def test_software_repr_all_optional_fields():
s = Software(
author=["Dev, Jane"],
title="my-tool",
year=2024,
publisher="GitHub",
version="1.0.0",
license="MIT",
)
r = repr(s)
assert r.startswith("Software(")
assert "publisher=" in r
assert "version=" in r
assert "license=" in r
def test_repr_omits_none_optional_fields():
m = Misc(
author=["Smith, A"],
title="Thing",
year=2024,
)
r = repr(m)
assert "doi=" not in r
assert "url=" not in r
assert "note=" not in r
# ── summary() ────────────────────────────────────────────────────────────
def test_summary_multiple_authors():
a = Article(
author=["Huttley, Gavin", "Caley, Katherine", "McArthur, Robert"],
title="diverse-seq: an application for alignment-free selecting and clustering biological sequences",
journal="JOSS",
year=2025,
volume=10,
pages="7765",
app="diverse-seq",
)
app_name, citation_str = a.summary()
assert app_name == "diverse-seq"
assert citation_str.startswith("Huttley et al. 2025")
assert "\u2026" in citation_str
def test_summary_single_author():
s = Software(
author=["Smith, Jane"],
title="my-cogent3-plugin",
year=2024,
app="my-plugin",
)
app_name, citation_str = s.summary()
assert app_name == "my-plugin"
assert citation_str.startswith("Smith 2024")
def test_summary_no_app():
m = Misc(
author=["Smith, A"],
title="Something",
year=2024,
)
app_name, _citation_str = m.summary()
assert app_name == ""
def test_summary_long_title_truncated():
m = Misc(
author=["Smith, A"],
title="A" * 60,
year=2024,
)
_, citation_str = m.summary()
assert citation_str.endswith("\u2026")
def test_summary_short_title_not_truncated():
m = Misc(
author=["Smith, A"],
title="Short title",
year=2024,
)
_, citation_str = m.summary()
assert "\u2026" not in citation_str
# ── JSON round-trip ──────────────────────────────────────────────────────
def _make_all_types() -> list[CitationBase]:
"""Return one instance of each entry type with distinctive fields."""
return [
Article(
author=["Huttley, Gavin"],
title="diverse-seq",
year=2025,
journal="JOSS",
volume=10,
pages="7765",
key="custom.key",
app="diverse-seq",
),
Book(
author=["Knuth, Donald"],
title="TAOCP",
year=1997,
publisher="Addison-Wesley",
edition="3rd",
),
InProceedings(
author=["Doe, John"],
title="A Paper",
year=2023,
booktitle="Proceedings",
pages="1--10",
),
TechReport(
author=["Turing, Alan"],
title="On Computable Numbers",
year=1936,
institution="Cambridge",
number="TR-42",
),
Thesis(
author=["Student, Alice"],
title="My Thesis",
year=2022,
school="MIT",
thesis_type="phd",
),
Software(
author=["Dev, Jane"],
title="my-tool",
year=2024,
version="1.0.0",
),
Misc(
author=["Smith, A"],
title="Something",
year=2024,
doi="10.1234/misc",
),
]
@pytest.mark.parametrize("citation", _make_all_types(), ids=lambda c: type(c).__name__)
def test_to_dict_from_dict_round_trip(citation: CitationBase) -> None:
d = citation.to_dict()
restored = CitationBase.from_dict(d)
assert restored == citation
assert restored.key == citation.key
assert restored.app == citation.app
@pytest.mark.parametrize("citation", _make_all_types(), ids=lambda c: type(c).__name__)
def test_to_dict_contains_type(citation: CitationBase) -> None:
d = citation.to_dict()
assert "type" in d
assert d["type"] == type(citation).__name__
def test_from_dict_unknown_type() -> None:
with pytest.raises(ValueError, match="unknown citation type"):
CitationBase.from_dict(
{"type": "Nonexistent", "author": ["A"], "title": "T", "year": 2024}
)
def test_from_dict_missing_type() -> None:
with pytest.raises(ValueError, match="missing required 'type' key"):
CitationBase.from_dict({"author": ["A"], "title": "T", "year": 2024})
def test_to_jsons_from_jsons_mixed_types() -> None:
originals = _make_all_types()
json_str = to_jsons(originals)
restored = from_jsons(json_str)
assert len(restored) == len(originals)
for orig, rest in zip(originals, restored, strict=True):
assert type(rest) is type(orig)
assert rest == orig
assert rest.key == orig.key
assert rest.app == orig.app
def test_write_json_load_json(tmp_path: pathlib.Path) -> None:
originals = _make_all_types()
path = tmp_path / "citations.json"
write_json(citations=originals, path=path)
assert path.exists()
restored = load_json(path)
assert len(restored) == len(originals)
for orig, rest in zip(originals, restored, strict=True):
assert type(rest) is type(orig)
assert rest == orig
assert rest.key == orig.key
assert rest.app == orig.app
cogent3-citeable-7188740/tests/test_write.py 0000664 0000000 0000000 00000004377 15154651363 0020757 0 ustar 00root root 0000000 0000000 """Tests for write_bibtex."""
from pathlib import Path
import pytest
from citeable import Article, Misc, write_bibtex
def test_write_bibtex_creates_file(tmp_path):
a = Misc(author=["Smith, A"], title="Paper A", year=2024)
b = Misc(author=["Jones, B"], title="Paper B", year=2024)
path = tmp_path / "refs.bib"
write_bibtex([a, b], path)
content = path.read_text(encoding="utf-8")
assert "@misc{Smith.2024," in content
assert "@misc{Jones.2024," in content
assert "\n\n" in content
def test_write_bibtex_deduplicates(tmp_path):
a = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
b = Article(
author=["Smith, A"],
title="Paper",
year=2024,
journal="J",
volume=1,
pages="1",
)
path = tmp_path / "refs.bib"
write_bibtex([a, b], path)
content = path.read_text(encoding="utf-8")
assert content.count("@article{") == 1
def test_write_bibtex_resolves_collisions(tmp_path):
a = Misc(author=["Smith, A"], title="Paper A", year=2024)
b = Misc(author=["Smith, B"], title="Paper B", year=2024)
path = tmp_path / "refs.bib"
write_bibtex([a, b], path)
content = path.read_text(encoding="utf-8")
assert "Smith.2024.a" in content
assert "Smith.2024.b" in content
def test_write_bibtex_empty_list(tmp_path):
path = tmp_path / "refs.bib"
write_bibtex([], path)
content = path.read_text(encoding="utf-8")
assert content == ""
def test_write_bibtex_str_path(tmp_path):
a = Misc(author=["Smith, A"], title="Paper", year=2024)
path = str(tmp_path / "refs.bib")
write_bibtex([a], path)
assert Path(path).exists()
def test_write_bibtex_missing_parent():
with pytest.raises(FileNotFoundError):
write_bibtex(
[Misc(author=["Smith, A"], title="Paper", year=2024)],
"/nonexistent/dir/refs.bib",
)
def test_write_bibtex_utf8(tmp_path):
a = Misc(
author=["Müller, Hans"],
title="Über etwas",
year=2020,
)
path = tmp_path / "refs.bib"
write_bibtex([a], path)
content = path.read_text(encoding="utf-8")
assert "Müller" in content
assert "Über" in content
cogent3-citeable-7188740/uv.lock 0000664 0000000 0000000 00000275034 15154651363 0016356 0 ustar 00root root 0000000 0000000 version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "argcomplete"
version = "3.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" },
]
[[package]]
name = "attrs"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "citeable"
version = "2026.3.11b1"
source = { editable = "." }
[package.dev-dependencies]
dev = [
{ name = "cogapp" },
{ name = "mypy" },
{ name = "nox" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "cogapp" },
{ name = "mypy" },
{ name = "nox" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
[[package]]
name = "cogapp"
version = "3.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/64/ea/4ffa8095e0b675e9961cbdcad002c09d35d4ab76ff99d61a014e9e6bcd53/cogapp-3.6.0.tar.gz", hash = "sha256:ec2a9170bfa644bf0d91996fdb5576c13d7e5e848bb2378a6f92727b48f92604", size = 59742, upload-time = "2025-09-21T15:54:13.553Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/1c/23b5110b7cb305f0fe3a5d6809d6804c1cb57d57b59516dbb5e24aaf95fc/cogapp-3.6.0-py3-none-any.whl", hash = "sha256:6ea11c63221f92978d37d8d76208eadeae2a6e4641d323f165738a5464a19d32", size = 30550, upload-time = "2025-09-21T15:54:12.205Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
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 = "colorlog"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" },
]
[[package]]
name = "coverage"
version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
{ url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
{ url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
{ url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
{ url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
{ url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
{ url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
{ url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
{ url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
{ url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
{ url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "dependency-groups"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" },
]
[[package]]
name = "distlib"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "filelock"
version = "3.24.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" },
]
[[package]]
name = "humanize"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" },
]
[[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 = "librt"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" },
{ url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" },
{ url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" },
{ url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" },
{ url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" },
{ url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" },
{ url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" },
{ url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" },
{ url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" },
{ url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" },
{ url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" },
{ url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
{ url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
{ url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
{ url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
{ url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
{ url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
{ url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
{ url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
{ url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
{ url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
{ url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
{ url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
{ url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
{ url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
{ url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
{ url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
{ url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
{ url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
{ url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" },
{ url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" },
{ url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" },
{ url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" },
{ url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" },
{ url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" },
{ url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" },
{ url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" },
{ url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" },
{ url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" },
{ url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" },
{ url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" },
{ url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" },
{ url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" },
{ url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" },
{ url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" },
{ url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" },
{ url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" },
{ url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" },
{ url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" },
{ url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
]
[[package]]
name = "mypy"
version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
{ url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
{ url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
{ url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "nox"
version = "2026.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "argcomplete" },
{ name = "attrs" },
{ name = "colorlog" },
{ name = "dependency-groups" },
{ name = "humanize" },
{ name = "packaging" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/8e/55a9679b31f1efc48facedd2448eb53c7f1e647fb592aa1403c9dd7a4590/nox-2026.2.9.tar.gz", hash = "sha256:1bc8a202ee8cd69be7aaada63b2a7019126899a06fc930a7aee75585bf8ee41b", size = 4031165, upload-time = "2026-02-10T04:38:58.878Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/58/0d5e5a044f1868bdc45f38afdc2d90ff9867ce398b4e8fa9e666bfc9bfba/nox-2026.2.9-py3-none-any.whl", hash = "sha256:1b7143bc8ecdf25f2353201326152c5303ae4ae56ca097b1fb6179ad75164c47", size = 74615, upload-time = "2026-02-10T04:38:57.266Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathspec"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]]
name = "platformdirs"
version = "4.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" },
]
[[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 = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "ruff"
version = "0.15.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
]
[[package]]
name = "tomli"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
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 = "virtualenv"
version = "20.39.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/54/809199edc537dbace273495ac0884d13df26436e910a5ed4d0ec0a69806b/virtualenv-20.39.0.tar.gz", hash = "sha256:a15f0cebd00d50074fd336a169d53422436a12dfe15149efec7072cfe817df8b", size = 5869141, upload-time = "2026-02-23T18:09:13.349Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/b4/8268da45f26f4fe84f6eae80a6ca1485ffb490a926afecff75fc48f61979/virtualenv-20.39.0-py3-none-any.whl", hash = "sha256:44888bba3775990a152ea1f73f8e5f566d49f11bbd1de61d426fd7732770043e", size = 5839121, upload-time = "2026-02-23T18:09:11.173Z" },
]