diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..12d1589
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,33 @@
+name: Build
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build_and_publish:
+ uses: tehw0lf/workflows/.github/workflows/build-test-publish.yml@main
+ permissions:
+ id-token: write
+ attestations: write
+ actions: write
+ contents: write
+ packages: write
+ security-events: write
+ pull-requests: write
+ with:
+ tool: uv
+ install: sync
+ lint: run ruff check && uv run ruff format --check && uv run mypy
+ # Fetches the conformance suite pinned in conformance.lock, then runs
+ # every test on the oldest and the newest supported Python: the
+ # orchestrator installs one version, so 3.11 would otherwise go
+ # untested and a newer-only construct would ship unnoticed.
+ test: run ./scripts/fetch-conformance.sh && uv run --python 3.11 pytest && uv run --python 3.14 pytest
+ build_branch: build
+ build_main: build
+ artifact_path: dist
+ # No PyPI yet: publishing comes with the API provider, once Trusted
+ # Publishing is configured on pypi.org.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c95894f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,39 @@
+# CLAUDE.md
+
+Python port of YaFT (PyPI `yaft`, import `yaft`). The normative rules are
+`yaft-conformance/SPEC.md` (fetched to `tests/suite/SPEC.md`); the TypeScript,
+Java and Go ports are references for behaviour, not for API shape.
+
+## Layout
+
+- `src/yaft/model.py` — `Feature` (frozen dataclass, snake_case fields)
+- `src/yaft/evaluate.py` — `Clock`, `evaluate`, `parse_timestamp` (R3–R13,
+ R27, R28). `datetime` does the calendar checks; only the offset minutes are
+ checked by hand. The pattern needs `re.ASCII` and `fullmatch`.
+- `src/yaft/mapping.py` — response normalisation (R22–R25, R29, R30). A key or
+ value that is not a string is not set: no coercion, JSON booleans belong in
+ the boolean shape.
+- `src/yaft/providers.py` — `FeatureProvider` protocol, local providers;
+ `LocalFeatureProvider.load` is the all-or-nothing refresh (R30)
+- `src/yaft/toggle.py` — `feature_toggle`, `set_provider`; functions per call,
+ classes once, empty shell for an off class without fallback (R14–R19)
+- `tests/conformance/` — the suite adapter; unknown case values and unknown
+ case-file format versions must fail, never be skipped
+- `tests/test_*.py` — what the suite cannot see (generators, static/class
+ methods, the shell, Python's parsing traps)
+
+## Constraints
+
+- No runtime dependencies.
+- `requires-python = ">=3.11"`; CI runs the tests on 3.11 and 3.14.
+- Bump the patch version in `pyproject.toml` on every PR and run `uv lock`.
+- Mutation-check new rules: `PYTHONDONTWRITEBYTECODE=1`, or a same-length edit
+ within the same second keeps running the stale `.pyc`.
+
+## Pre-commit validation
+
+```bash
+uv sync && uv run ruff check && uv run ruff format --check && uv run mypy \
+ && ./scripts/fetch-conformance.sh && uv run --python 3.11 pytest \
+ && uv run --python 3.14 pytest && uv build
+```
diff --git a/README.md b/README.md
index d670472..2c6a9a4 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,160 @@
-

+# YaFT for Python
-# yaft-python
+
+

+
-YaFT (Yet another Feature Toggle) for Python: decorators for functions,
-methods and classes, evaluated by the same rules as every other YaFT port and
-checked against the shared
-[conformance suite](https://github.com/tehw0lf/yaft-conformance).
+---
-Work in progress.
+Feature toggles for functions, methods and classes in Python, following the
+same rules as [`@tehw0lf/yaft`](https://github.com/tehw0lf/yaft-ts),
+[`de.tehwolf:yaft`](https://github.com/tehw0lf/yaft-java) and
+[`yaft-go`](https://github.com/tehw0lf/yaft-go). It passes every case of
+[yaft-conformance](https://github.com/tehw0lf/yaft-conformance), is fully
+typed, and has no runtime dependencies. Python 3.11 or later.
+
+---
+
+## Installation
+
+Not on PyPI yet; that comes with the API provider. Until then:
+
+```bash
+pip install git+https://github.com/tehw0lf/yaft-python
+```
+
+```python
+from yaft import feature_toggle, set_provider
+```
+
+## Providers
+
+Set one provider at startup:
+
+```python
+from yaft import Feature, LocalFeatureProvider, set_provider
+
+set_provider(LocalFeatureProvider({"newCheckout": Feature(key="newCheckout", value="true")}))
+```
+
+| Provider | Data | Time bounds |
+|---|---|---|
+| `LocalFeatureProvider` | full `Feature` records | yes: `active_at`, `disabled_at` |
+| `LocalBooleanProvider` | `{"myToggle": True}` | none, by design |
+
+Both read a JSON file with `from_file(path)`. `LocalFeatureProvider` takes a
+YaFT backend response (`{"toggles": [...]}`, in either field spelling) or
+features keyed by name, the format yaft-ts reads:
+
+```json
+{
+ "newCheckout": { "value": "true", "activeAt": "2026-10-01T00:00:00Z", "disabledAt": "" }
+}
+```
+
+A missing or unreadable file raises instead of starting with everything off.
+`load(response)` replaces the data with a newer backend response, all or
+nothing: a body that is not a toggle group raises `ValueError` and the old
+data stays.
+
+Any object with an `is_enabled(key: str) -> bool` method is a provider, for
+example one that reads environment variables.
+
+### Evaluation rules
+
+A feature is on when all of these hold:
+
+- `value` is exactly `"true"`. `"True"`, `"1"` and `""` are off.
+- `active_at` has been reached, if set. At exactly `active_at` it is on.
+- `disabled_at` has not been reached, if set. At exactly `disabled_at` it is off.
+
+A missing feature is off. Only RFC 3339 with an offset is a valid bound
+(`2026-09-18T15:00:00Z`, `2026-09-18T17:00:00+02:00`); an unset or invalid one
+is ignored with a warning on the `yaft` logger, never raised. The clock is a
+parameter, so tests can pin the time:
+
+```python
+from datetime import UTC, datetime
+
+provider = LocalFeatureProvider(data, clock=lambda: datetime(2026, 9, 18, 12, tzinfo=UTC))
+```
+
+The boolean shape has no time logic, and only the boolean `True` is on. A
+value that is not a boolean, such as the string `"true"`, is dropped when the
+data is loaded.
+
+## Toggling code
+
+### Functions and methods: evaluated on every call
+
+```python
+class Checkout:
+ def classic_total(self, cart: list[int]) -> int:
+ return sum(cart)
+
+ @feature_toggle("newPricing", fallback=classic_total)
+ def total(self, cart: list[int]) -> int:
+ return sum(cart) - self.discount(cart)
+```
+
+Off, the fallback runs with the same arguments and the same `self`. Without a
+fallback, an off call returns *nothing*:
+
+| Decorated | Off, without a fallback |
+|---|---|
+| function or method | `None` |
+| `async def` | a coroutine that resolves to `None`, so `await` still works |
+| generator | an empty iterator, so a `for` loop still works |
+| async generator | an empty async iterator |
+
+A synchronous fallback on an `async def` is fine; its result is returned from
+the coroutine. `staticmethod` and `classmethod` can be decorated too, with
+`@feature_toggle` on the outside.
+
+### Classes: decided once
+
+```python
+@feature_toggle("newCheckout", fallback=ClassicCheckout)
+class NewCheckout: ...
+```
+
+The toggle is read **once**, when the class is decorated; changing it later
+does not swap the class. Without a fallback, an off class becomes an *empty
+shell*: it takes any constructor arguments, and every method, inherited ones
+included, returns nothing of its own kind (see the table above). Properties
+read as `None`. `__call__` and the context manager methods are kept; other
+dunder methods and data attributes are not.
+
+### When a toggle is read
+
+| | Evaluated |
+|---|---|
+| class | **once**, when decorated |
+| function or method | on **every call** |
+| `async def`, generator | on every call, when the coroutine or generator starts |
+
+### Fail fast
+
+`feature_toggle` raises `ProviderNotSetError` ("FeatureToggleProvider not
+set") when it is applied, not on the first call, if no provider is set. A
+fallback of the wrong kind, such as a class for a function, raises `TypeError`
+at the same point. A misconfiguration shows at startup.
+
+## Conformance
+
+`conformance.lock` pins a release of the suite by version and checksum.
+`scripts/fetch-conformance.sh` fetches and verifies it into `tests/suite`, and
+`pytest` then runs every case next to this package's own tests.
+
+## Development
+
+```bash
+uv sync
+uv run ruff check && uv run ruff format --check && uv run mypy
+./scripts/fetch-conformance.sh && uv run --python 3.11 pytest && uv run --python 3.14 pytest
+uv build
+```
+
+## License
+
+MIT
diff --git a/conformance.lock b/conformance.lock
new file mode 100644
index 0000000..85a787e
--- /dev/null
+++ b/conformance.lock
@@ -0,0 +1,2 @@
+version=v5.0.0
+sha256=47b6adaac579aa7963f1fe1679676445c09b7f74c6812136ee35eeeeb4a7f3bc
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..0084996
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,52 @@
+[project]
+name = "yaft"
+version = "0.1.0"
+description = "Yet another Feature Toggle: decorators for functions, methods and classes"
+readme = "README.md"
+license = "MIT"
+license-files = ["LICENSE"]
+authors = [{ name = "tehw0lf", email = "tehwolf@protonmail.com" }]
+requires-python = ">=3.11"
+# No runtime dependencies, on purpose: a feature toggle library should not pull
+# a dependency tree into every application that uses it.
+dependencies = []
+keywords = ["feature", "toggle", "feature-toggle", "feature-flag", "yaft"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Typing :: Typed",
+]
+
+[project.urls]
+Homepage = "https://github.com/tehw0lf/yaft-python"
+Repository = "https://github.com/tehw0lf/yaft-python"
+Issues = "https://github.com/tehw0lf/yaft-python/issues"
+
+[dependency-groups]
+dev = ["mypy", "pytest", "ruff"]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.ruff]
+target-version = "py311"
+line-length = 100
+
+[tool.ruff.lint]
+select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF", "PT"]
+
+[tool.mypy]
+strict = true
+python_version = "3.11"
+files = ["src", "tests"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+addopts = "-ra --strict-markers"
diff --git a/scripts/fetch-conformance.sh b/scripts/fetch-conformance.sh
new file mode 100755
index 0000000..2d7073d
--- /dev/null
+++ b/scripts/fetch-conformance.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+#
+# Fetches the YaFT conformance suite pinned in conformance.lock.
+#
+# Copy this into a port, next to a conformance.lock of the form:
+#
+# version=v1.0.0
+# sha256=
+#
+# The checksum is not optional. A Git tag can be moved; verifying the asset is
+# what stops a port's tests from changing without a diff.
+#
+# Usage: scripts/fetch-conformance.sh [target-dir] (default: tests/suite)
+#
+# Set the default to wherever the port's adapter reads the cases from. Ports
+# differ -- yaft-ts uses src/test/conformance -- and a default that does not
+# match leaves the next test run reporting missing cases.
+
+set -euo pipefail
+
+REPO="tehw0lf/yaft-conformance"
+LOCK="${LOCK:-conformance.lock}"
+TARGET="${1:-tests/suite}"
+
+if [[ ! -f "$LOCK" ]]; then
+ echo "error: $LOCK not found; run from the port's root" >&2
+ exit 1
+fi
+
+# || true: without it, a missing line makes grep fail, and set -e would end
+# the script before the message below could say why.
+version="$(grep -E '^version=' "$LOCK" | cut -d= -f2- || true)"
+expected="$(grep -E '^sha256=' "$LOCK" | cut -d= -f2- || true)"
+
+if [[ -z "$version" || -z "$expected" ]]; then
+ echo "error: $LOCK needs both version= and sha256=" >&2
+ exit 1
+fi
+
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+
+url="https://github.com/${REPO}/releases/download/${version}/cases.tar.gz"
+echo "fetching conformance suite ${version}"
+curl --fail --location --silent --show-error --output "$tmp/cases.tar.gz" "$url"
+
+# sha256sum is GNU coreutils and is not present on macOS; shasum ships with
+# both. Preferring sha256sum keeps Linux CI on the faster binary.
+if command -v sha256sum >/dev/null 2>&1; then
+ actual="$(sha256sum "$tmp/cases.tar.gz" | cut -d' ' -f1)"
+elif command -v shasum >/dev/null 2>&1; then
+ actual="$(shasum -a 256 "$tmp/cases.tar.gz" | cut -d' ' -f1)"
+else
+ echo "error: neither sha256sum nor shasum found; cannot verify the download" >&2
+ exit 1
+fi
+if [[ "$actual" != "$expected" ]]; then
+ echo "error: checksum mismatch for cases.tar.gz" >&2
+ echo " expected $expected" >&2
+ echo " actual $actual" >&2
+ echo "The tag may have been moved. Do not update the lock without reading the diff." >&2
+ exit 1
+fi
+
+rm -rf "$TARGET"
+mkdir -p "$TARGET"
+tar --extract --gzip --file "$tmp/cases.tar.gz" --directory "$TARGET"
+
+echo "conformance suite ${version} unpacked into ${TARGET}"
diff --git a/src/yaft/__init__.py b/src/yaft/__init__.py
new file mode 100644
index 0000000..f3d68fa
--- /dev/null
+++ b/src/yaft/__init__.py
@@ -0,0 +1,46 @@
+"""YaFT - Yet another Feature Toggle.
+
+Decorators that switch functions, methods and classes on and off, evaluated by
+the rules every YaFT port shares (https://github.com/tehw0lf/yaft-conformance)::
+
+ from yaft import LocalFeatureProvider, feature_toggle, set_provider
+
+ set_provider(LocalFeatureProvider.from_file("features.json"))
+
+ class Checkout:
+ @feature_toggle("new-pricing", fallback=lambda self, cart: sum(cart))
+ def total(self, cart: list[int]) -> int: ...
+"""
+
+from importlib.metadata import PackageNotFoundError, version
+
+from .evaluate import Clock, evaluate, parse_timestamp, system_clock
+from .mapping import normalise_booleans, normalise_collection, normalise_feature, normalise_group
+from .model import Feature
+from .providers import FeatureProvider, LocalBooleanProvider, LocalFeatureProvider
+from .toggle import ProviderNotSetError, feature_toggle, get_provider, set_provider
+
+try:
+ __version__ = version("yaft")
+except PackageNotFoundError: # pragma: no cover - only without an installed distribution
+ __version__ = "0+unknown"
+
+__all__ = [
+ "Clock",
+ "Feature",
+ "FeatureProvider",
+ "LocalBooleanProvider",
+ "LocalFeatureProvider",
+ "ProviderNotSetError",
+ "__version__",
+ "evaluate",
+ "feature_toggle",
+ "get_provider",
+ "normalise_booleans",
+ "normalise_collection",
+ "normalise_feature",
+ "normalise_group",
+ "parse_timestamp",
+ "set_provider",
+ "system_clock",
+]
diff --git a/src/yaft/evaluate.py b/src/yaft/evaluate.py
new file mode 100644
index 0000000..439025a
--- /dev/null
+++ b/src/yaft/evaluate.py
@@ -0,0 +1,121 @@
+"""The evaluation rules, in one place (SPEC sections 2 and 3).
+
+Providers call :func:`evaluate` instead of implementing the rules themselves
+(R20), so every provider -- and every port that mirrors this module -- gives
+the same answer for the same feature at the same instant.
+"""
+
+import logging
+import re
+from collections.abc import Callable
+from datetime import UTC, datetime, timedelta, timezone
+
+from .model import Feature
+
+log = logging.getLogger("yaft")
+
+Clock = Callable[[], datetime]
+"""A source of the current time, as a timezone-aware ``datetime``.
+
+Everything that evaluates takes one instead of reading the system time, so
+tests -- and the conformance suite, which brings its own ``now`` per case --
+can evaluate against a fixed instant (R9).
+"""
+
+
+def system_clock() -> datetime:
+ """The default clock: the system time, in UTC."""
+ return datetime.now(UTC)
+
+
+# RFC 3339 with an explicit offset, and nothing else (R10). re.ASCII matters:
+# without it \d also matches digits of other scripts, which int() then reads.
+_RFC3339 = re.compile(
+ r"(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?"
+ r"(?:([Zz])|([+-])(\d{2}):(\d{2}))",
+ re.ASCII,
+)
+
+
+def parse_timestamp(value: object) -> datetime | None:
+ """Parses an RFC 3339 timestamp with an offset into an aware ``datetime``.
+
+ Returns ``None`` for anything unset, malformed or in another format;
+ callers treat that as no bound, never as an error (R8). An invalid value
+ is logged as a warning but never raises, so a bad timestamp in the backend
+ cannot take an application down.
+
+ ``datetime`` does the calendar checks: it rejects an impossible date or
+ time instead of rolling it over (R11), and a leap second with it (R12).
+ Fractional seconds are truncated to milliseconds (R27); offsets up to
+ ``±23:59`` are applied (R13, R28).
+ """
+ if value is None or value == "":
+ return None
+ if not isinstance(value, str):
+ log.warning("YaFT: ignoring %r, expected an RFC 3339 string", value)
+ return None
+
+ match = _RFC3339.fullmatch(value)
+ if match is None:
+ log.warning(
+ 'YaFT: ignoring "%s", expected RFC 3339 with an offset (e.g. 2026-09-18T15:00:00Z)',
+ value,
+ )
+ return None
+
+ year, month, day, hour, minute, second = (int(match.group(i)) for i in range(1, 7))
+ fraction, zulu, sign, offset_hours, offset_minutes = match.group(7, 8, 9, 10, 11)
+
+ # timedelta would carry minute 60 into the hour, so it is checked here;
+ # timezone() itself rejects 24 hours and more.
+ if not zulu and int(offset_minutes) > 59:
+ log.warning('YaFT: ignoring "%s", not a valid offset', value)
+ return None
+ offset = timedelta(hours=int(offset_hours or 0), minutes=int(offset_minutes or 0))
+
+ try:
+ return datetime(
+ year,
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ # Truncated, not rounded: .9999 is .999, never the next second (R27).
+ int((fraction or "")[:3].ljust(3, "0")) * 1000,
+ tzinfo=UTC if zulu else timezone(-offset if sign == "-" else offset),
+ )
+ except ValueError:
+ log.warning('YaFT: ignoring "%s", not a valid date, time or offset', value)
+ return None
+
+
+def evaluate(feature: Feature | None, now: datetime) -> bool:
+ """Decides whether ``feature`` is on at the instant ``now``.
+
+ - A missing feature is off (R3).
+ - Only the exact string ``"true"`` is on (R4).
+ - The window is ``[active_at, disabled_at)``: on at exactly ``active_at``,
+ off at exactly ``disabled_at`` (R5, R6).
+ - ``active_at`` after ``disabled_at`` is not special-cased; the window is
+ simply never open (R7).
+ - An unset or unparseable bound is ignored (R8).
+
+ ``now`` must be timezone-aware; a naive ``datetime`` raises ``ValueError``,
+ because guessing a timezone would flip the same feature at a different time
+ on every machine. It is truncated to milliseconds, like the bounds.
+ """
+ if now.utcoffset() is None:
+ raise ValueError("now must be timezone-aware, e.g. datetime.now(UTC)")
+ if feature is None or feature.value != "true":
+ return False
+
+ now = now.replace(microsecond=now.microsecond // 1000 * 1000)
+
+ active_at = parse_timestamp(feature.active_at)
+ if active_at is not None and now < active_at:
+ return False
+
+ disabled_at = parse_timestamp(feature.disabled_at)
+ return not (disabled_at is not None and now >= disabled_at)
diff --git a/src/yaft/mapping.py b/src/yaft/mapping.py
new file mode 100644
index 0000000..fdd69ec
--- /dev/null
+++ b/src/yaft/mapping.py
@@ -0,0 +1,116 @@
+"""Turns a backend response or a local file into provider data (SPEC section 6).
+
+This is the single definition of the mapping rules, the way
+:func:`yaft.evaluate` is the single definition of the evaluation rules.
+"""
+
+from collections.abc import Mapping
+from typing import Any
+
+from .model import Feature
+
+
+def _field(raw: Mapping[str, Any], lower: str, upper: str) -> Any:
+ """Reads a field by presence, not by truthiness (R23).
+
+ ``raw.get("value") or raw.get("Value")`` is wrong: a present but empty
+ value falls through to the other spelling, so a feature stored as ``""``
+ reads as whatever the capitalised field holds, and an off feature reports
+ on. The same trap applies to ``tags: []``.
+ """
+ if lower in raw:
+ return raw[lower]
+ if upper in raw:
+ return raw[upper]
+ return None
+
+
+def _text(value: Any) -> str:
+ """A string field, or ``""`` for anything that is not a string.
+
+ ``value`` is a string by definition (R1); a JSON boolean belongs in the
+ boolean shape, not here. So ``"value": true`` is not coerced to ``"true"``
+ -- it is not set, and the feature is off. A key that is not a string is
+ likewise no key, and the entry is skipped (R25). The backend sends ``null``
+ for an unset bound and fixtures ``""``; both are no bound (R24).
+ """
+ return value if isinstance(value, str) else ""
+
+
+def normalise_feature(raw: Mapping[str, Any]) -> Feature:
+ """Normalises one entry into a :class:`Feature`, in either field spelling (R22a)."""
+ tags = _field(raw, "tags", "Tags")
+ return Feature(
+ key=_text(_field(raw, "key", "Key")),
+ value=_text(_field(raw, "value", "Value")),
+ active_at=_text(_field(raw, "activeAt", "ActiveAt")),
+ disabled_at=_text(_field(raw, "disabledAt", "DisabledAt")),
+ # Filtered rather than trusted: a mixed array would otherwise hand
+ # callers a non-string through a field typed as strings.
+ tags=tuple(tag for tag in tags if isinstance(tag, str)) if isinstance(tags, list) else (),
+ )
+
+
+def _collection(body: Mapping[str, Any]) -> list[Any] | None:
+ for name in ("toggles", "value"):
+ entries = body.get(name)
+ if isinstance(entries, list):
+ return entries
+ return None
+
+
+def normalise_collection(response: object) -> dict[str, Feature]:
+ """Normalises a whole response into features keyed by their key.
+
+ Three envelopes are accepted, because the backend uses all three (R22):
+ ``{"toggles": [...]}`` for a UUID group, ``{"value": [...]}`` for the same
+ thing under another name, and a flat object for a single toggle. An entry
+ without a usable key is skipped rather than stored under ``""`` (R25).
+ """
+ if not isinstance(response, Mapping):
+ return {}
+
+ collection = _collection(response)
+ entries = collection if collection is not None else [response]
+
+ data: dict[str, Feature] = {}
+ for entry in entries:
+ if isinstance(entry, Mapping):
+ feature = normalise_feature(entry)
+ if feature.key != "":
+ data[feature.key] = feature
+ return data
+
+
+def normalise_group(response: object) -> dict[str, Feature] | None:
+ """Normalises a response, but only if it is recognisably a toggle group (R30).
+
+ A group is a collection envelope -- an empty one is a valid empty group --
+ or a single toggle. Returns ``None`` for anything else: ``None``, a list,
+ a proxy's error object, or a collection none of whose entries is usable.
+ :func:`normalise_collection` turns all of those into ``{}``, and a provider
+ storing that would switch every feature off without an error.
+ """
+ if not isinstance(response, Mapping):
+ return None
+ data = normalise_collection(response)
+ if data:
+ return data
+ collection = _collection(response)
+ return data if collection is not None and len(collection) == 0 else None
+
+
+def normalise_booleans(response: object) -> dict[str, bool]:
+ """Normalises a boolean-shape payload, ``{"myToggle": true}``.
+
+ Only real booleans are kept (R29). Anything else is dropped, so its key
+ reads as missing and therefore off. Keeping it and testing its truthiness
+ would turn ``"false"`` on, since a non-empty string is truthy.
+ """
+ if not isinstance(response, Mapping):
+ return {}
+ return {
+ key: value
+ for key, value in response.items()
+ if isinstance(key, str) and isinstance(value, bool)
+ }
diff --git a/src/yaft/model.py b/src/yaft/model.py
new file mode 100644
index 0000000..83ee8f3
--- /dev/null
+++ b/src/yaft/model.py
@@ -0,0 +1,20 @@
+"""The feature record every provider of the feature shape holds (SPEC section 1)."""
+
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True, slots=True)
+class Feature:
+ """One feature toggle.
+
+ ``value`` is a string, not a boolean: the backend stores it as one, and only
+ the exact string ``"true"`` is on (R1, R4). ``active_at`` and
+ ``disabled_at`` are RFC 3339 timestamps with an offset, or ``""`` for no
+ bound (R2).
+ """
+
+ key: str
+ value: str
+ active_at: str = ""
+ disabled_at: str = ""
+ tags: tuple[str, ...] = ()
diff --git a/src/yaft/providers.py b/src/yaft/providers.py
new file mode 100644
index 0000000..619b6aa
--- /dev/null
+++ b/src/yaft/providers.py
@@ -0,0 +1,140 @@
+"""Providers: where feature data comes from, and how a key is answered (SPEC section 5)."""
+
+import json
+import os
+from collections.abc import Mapping
+from types import MappingProxyType
+from typing import Any, Protocol, Self, runtime_checkable
+
+from .evaluate import Clock, evaluate, system_clock
+from .mapping import normalise_booleans, normalise_feature, normalise_group
+from .model import Feature
+
+
+@runtime_checkable
+class FeatureProvider(Protocol):
+ """Anything that answers whether a feature is on.
+
+ A feature-shape provider holds :class:`Feature` records and delegates to
+ :func:`yaft.evaluate` (R20); a boolean-shape provider holds plain booleans
+ and has no time logic (R21). Any object with ``is_enabled`` will do, for
+ example one backed by environment variables.
+ """
+
+ def is_enabled(self, key: str) -> bool: ...
+
+
+class LocalFeatureProvider:
+ """A feature-shape provider over data held in memory.
+
+ Time bounds are evaluated against ``clock`` on every call. Replacing the
+ data swaps a reference, so readers on other threads see either the old or
+ the new data, never a mix.
+ """
+
+ def __init__(self, data: Mapping[str, Feature] | None = None, clock: Clock = system_clock):
+ self._clock = clock
+ self._data: Mapping[str, Feature] = MappingProxyType(dict(data or {}))
+
+ @classmethod
+ def from_response(cls, response: object, clock: Clock = system_clock) -> Self:
+ """Builds a provider from a parsed backend response (R22).
+
+ Raises ``ValueError`` if ``response`` is not a toggle group (R30).
+ """
+ provider = cls(clock=clock)
+ provider.load(response)
+ return provider
+
+ @classmethod
+ def from_file(cls, path: str | os.PathLike[str], clock: Clock = system_clock) -> Self:
+ """Reads a JSON file: a backend response, or features keyed by name.
+
+ The keyed form is what yaft-ts reads from local files::
+
+ {"myToggle": {"value": "true", "activeAt": "", "disabledAt": ""}}
+
+ A missing or unreadable file raises, rather than starting with every
+ feature off.
+ """
+ with open(path, encoding="utf-8") as file:
+ content = json.load(file)
+ group = normalise_group(content)
+ if group is None:
+ group = _keyed_features(content)
+ return cls(group, clock)
+
+ @property
+ def data(self) -> Mapping[str, Feature]:
+ """The current features by key, read-only."""
+ return self._data
+
+ def replace(self, data: Mapping[str, Feature]) -> None:
+ """Swaps the data, for example after reloading it. The mapping is copied."""
+ self._data = MappingProxyType(dict(data))
+
+ def load(self, response: object) -> None:
+ """Replaces the data with the group in a parsed backend response.
+
+ The refresh succeeds or fails as a whole (R30). A group -- even an
+ empty one -- replaces the data completely, so a toggle missing from it
+ is off. Anything else raises ``ValueError`` and leaves the data as it
+ was: storing a proxy's error page as "no toggles" would switch every
+ feature off without an error.
+ """
+ group = normalise_group(response)
+ if group is None:
+ raise ValueError(f"not a toggle group: {_preview(response)}")
+ self.replace(group)
+
+ def is_enabled(self, key: str) -> bool:
+ """Evaluates the feature stored under ``key``; a missing key is off."""
+ return evaluate(self._data.get(key), self._clock())
+
+
+class LocalBooleanProvider:
+ """A boolean-shape provider: ``{"myToggle": true}``, no time logic (R21).
+
+ Only the boolean ``True`` is on. Anything that is not a boolean is dropped
+ when the data is set, so its key reads as missing and therefore off (R29).
+ """
+
+ def __init__(self, data: Mapping[str, Any] | None = None):
+ self._data: Mapping[str, bool] = MappingProxyType(normalise_booleans(data or {}))
+
+ @classmethod
+ def from_file(cls, path: str | os.PathLike[str]) -> Self:
+ """Reads a JSON object of booleans. A missing or unreadable file raises."""
+ with open(path, encoding="utf-8") as file:
+ content = json.load(file)
+ if not isinstance(content, Mapping):
+ raise ValueError(f"{os.fspath(path)}: expected a JSON object, got {_preview(content)}")
+ return cls(content)
+
+ @property
+ def data(self) -> Mapping[str, bool]:
+ """The current booleans by key, read-only."""
+ return self._data
+
+ def replace(self, data: Mapping[str, Any]) -> None:
+ """Swaps the data; entries that are not booleans are dropped (R29)."""
+ self._data = MappingProxyType(normalise_booleans(data))
+
+ def is_enabled(self, key: str) -> bool:
+ return self._data.get(key) is True
+
+
+def _keyed_features(content: object) -> dict[str, Feature]:
+ """Reads ``{"name": {feature}}``; each feature is stored under its name."""
+ if not isinstance(content, Mapping):
+ raise ValueError(f"not a toggle group or features keyed by name: {_preview(content)}")
+ return {
+ name: normalise_feature({**raw, "key": name})
+ for name, raw in content.items()
+ if isinstance(name, str) and name != "" and isinstance(raw, Mapping)
+ }
+
+
+def _preview(value: object) -> str:
+ text = repr(value)
+ return text if len(text) <= 80 else text[:77] + "..."
diff --git a/src/yaft/py.typed b/src/yaft/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/yaft/toggle.py b/src/yaft/toggle.py
new file mode 100644
index 0000000..1fe357c
--- /dev/null
+++ b/src/yaft/toggle.py
@@ -0,0 +1,238 @@
+"""The ``feature_toggle`` decorator (SPEC section 4).
+
+When a toggle is evaluated is observable, so it is fixed:
+
+- a **class** is decided once, when it is decorated (R14);
+- a **function or method** is decided on every call (R15).
+"""
+
+import functools
+import inspect
+from collections.abc import AsyncIterator, Callable, Generator, Iterator
+from typing import Any, TypeVar
+
+from .providers import FeatureProvider
+
+_T = TypeVar("_T")
+
+_provider: FeatureProvider | None = None
+
+
+class ProviderNotSetError(RuntimeError):
+ """Raised when a toggle is used before :func:`set_provider` (R16)."""
+
+ def __init__(self) -> None:
+ super().__init__("FeatureToggleProvider not set")
+
+
+def set_provider(provider: FeatureProvider | None) -> None:
+ """Installs the provider every toggle asks; ``None`` removes it.
+
+ A decorated method asks whichever provider is installed when it is called,
+ so replacing the provider takes effect on the next call.
+ """
+ global _provider
+ _provider = provider
+
+
+def get_provider() -> FeatureProvider | None:
+ """The installed provider, or ``None``."""
+ return _provider
+
+
+def _is_enabled(key: str) -> bool:
+ provider = _provider
+ if provider is None:
+ raise ProviderNotSetError
+ return provider.is_enabled(key)
+
+
+def feature_toggle(key: str, fallback: Any = None) -> Callable[[_T], _T]:
+ """Switches a function, method or class on and off by the toggle ``key``.
+
+ Off, a function or method runs ``fallback`` with the same arguments and the
+ same ``self`` (R19), or returns *nothing* without one: ``None``, a
+ coroutine resolving to ``None`` for an ``async def`` (R18), an empty
+ iterator for a generator. Off, a class is replaced by ``fallback``, or by
+ an empty shell whose methods all return *nothing* (R17).
+
+ Raises :class:`ProviderNotSetError` right here, at decoration time, if no
+ provider is installed (R16), and ``TypeError`` for a fallback of the wrong
+ kind -- a startup misconfiguration should not wait for the first call.
+ """
+ if _provider is None:
+ raise ProviderNotSetError
+
+ def decorate(target: _T) -> _T:
+ if isinstance(target, type):
+ result: Any = _toggle_class(target, key, fallback)
+ elif isinstance(target, staticmethod | classmethod):
+ result = type(target)(_toggle_function(target.__func__, key, fallback))
+ elif callable(target):
+ result = _toggle_function(target, key, fallback)
+ else:
+ raise TypeError(f"feature_toggle({key!r}) cannot decorate {target!r}")
+ return result # type: ignore[no-any-return]
+
+ return decorate
+
+
+def _toggle_class(cls: type, key: str, fallback: Any) -> type:
+ if fallback is not None and not isinstance(fallback, type):
+ raise TypeError(f"feature_toggle({key!r}): a class's fallback must be a class")
+ if _is_enabled(key):
+ return cls
+ return fallback if fallback is not None else _empty_shell(cls)
+
+
+def _toggle_function(fn: Callable[..., Any], key: str, fallback: Any) -> Callable[..., Any]:
+ if fallback is not None and (isinstance(fallback, type) or not callable(fallback)):
+ raise TypeError(f"feature_toggle({key!r}): a function's fallback must be a function")
+
+ if inspect.iscoroutinefunction(fn):
+ # An async def wrapper keeps inspect.iscoroutinefunction true, which
+ # frameworks such as FastAPI check. The toggle is read when the
+ # coroutine starts, which for `await f()` is the call.
+ @functools.wraps(fn)
+ async def toggled_coroutine(*args: Any, **kwargs: Any) -> Any:
+ if _is_enabled(key):
+ return await fn(*args, **kwargs)
+ if fallback is None:
+ return None
+ # A synchronous fallback is fine too; awaiting only what is awaitable
+ # keeps the promise the signature makes either way.
+ result = fallback(*args, **kwargs)
+ return await result if inspect.isawaitable(result) else result
+
+ return toggled_coroutine
+
+ if inspect.isgeneratorfunction(fn):
+
+ @functools.wraps(fn)
+ def toggled_generator(*args: Any, **kwargs: Any) -> Generator[Any, Any, Any]:
+ if _is_enabled(key):
+ return (yield from fn(*args, **kwargs))
+ result = fallback(*args, **kwargs) if fallback is not None else None
+ if result is not None:
+ return (yield from result)
+ return None
+
+ return toggled_generator
+
+ if inspect.isasyncgenfunction(fn):
+
+ @functools.wraps(fn)
+ async def toggled_async_generator(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
+ source = (
+ fn(*args, **kwargs)
+ if _is_enabled(key)
+ else fallback(*args, **kwargs)
+ if fallback is not None
+ else None
+ )
+ if source is not None:
+ async for item in source:
+ yield item
+
+ return toggled_async_generator
+
+ @functools.wraps(fn)
+ def toggled(*args: Any, **kwargs: Any) -> Any:
+ if _is_enabled(key):
+ return fn(*args, **kwargs)
+ return fallback(*args, **kwargs) if fallback is not None else None
+
+ return toggled
+
+
+# Dunder methods are left out of the shell: returning None from __len__ or
+# __eq__ breaks the caller. These few are safe to answer with nothing, and
+# without them a shell could not stand in for a callable or a context manager.
+_SHELL_DUNDERS = frozenset({"__call__", "__enter__", "__exit__", "__aenter__", "__aexit__"})
+
+
+def _empty_shell(cls: type) -> type:
+ """A class with the methods of ``cls``, every one returning nothing (R17).
+
+ It takes any constructor arguments, so it can be created wherever ``cls``
+ was. Inherited methods are included; data attributes are not.
+ """
+
+ def accept_anything(self: object, *args: Any, **kwargs: Any) -> None:
+ pass
+
+ namespace: dict[str, Any] = {
+ "__module__": cls.__module__,
+ "__qualname__": cls.__qualname__,
+ "__doc__": cls.__doc__,
+ "__init__": accept_anything,
+ }
+ # Base classes first, so a subclass's definition wins.
+ for klass in reversed(cls.__mro__[:-1]):
+ for name, attr in vars(klass).items():
+ if name.startswith("__") and name.endswith("__") and name not in _SHELL_DUNDERS:
+ continue
+ stub = _stub(attr)
+ if stub is not None:
+ namespace[name] = stub
+ else:
+ namespace.pop(name, None)
+ return type(cls.__name__, (), namespace)
+
+
+def _stub(attr: Any) -> Any:
+ if isinstance(attr, staticmethod):
+ return staticmethod(_nothing_like(attr.__func__))
+ if isinstance(attr, classmethod):
+ return classmethod(_nothing_like(attr.__func__))
+ if isinstance(attr, property | functools.cached_property):
+ # Assigning to it must not fail where the original accepted it.
+ writable = isinstance(attr, functools.cached_property) or attr.fset is not None
+ return property(_nothing, _ignore if writable else None)
+ if inspect.isfunction(attr):
+ return _nothing_like(attr)
+ return None
+
+
+def _nothing(*args: Any, **kwargs: Any) -> None:
+ return None
+
+
+def _ignore(*args: Any, **kwargs: Any) -> None:
+ pass
+
+
+def _nothing_like(fn: Callable[..., Any]) -> Callable[..., Any]:
+ """A function of the same kind as ``fn`` that returns nothing."""
+ if inspect.iscoroutinefunction(fn):
+
+ @functools.wraps(fn)
+ async def nothing_coroutine(*args: Any, **kwargs: Any) -> None:
+ return None
+
+ return nothing_coroutine
+
+ if inspect.isgeneratorfunction(fn):
+
+ @functools.wraps(fn)
+ def nothing_generator(*args: Any, **kwargs: Any) -> Iterator[Any]:
+ yield from ()
+
+ return nothing_generator
+
+ if inspect.isasyncgenfunction(fn):
+
+ @functools.wraps(fn)
+ async def nothing_async_generator(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
+ return
+ yield
+
+ return nothing_async_generator
+
+ # A new function each time: functools.wraps mutates the function it is
+ # given, so wrapping the shared _nothing would rename every stub.
+ @functools.wraps(fn)
+ def nothing(*args: Any, **kwargs: Any) -> None:
+ return None
+
+ return nothing
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conformance/__init__.py b/tests/conformance/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conformance/cases.py b/tests/conformance/cases.py
new file mode 100644
index 0000000..29a1eab
--- /dev/null
+++ b/tests/conformance/cases.py
@@ -0,0 +1,71 @@
+"""Loads the conformance case files.
+
+The cases live in ``tests/suite``, fetched by ``scripts/fetch-conformance.sh``
+from the version pinned in ``conformance.lock``. They are not checked in: the
+lock file plus its checksum is what makes a suite bump a reviewable one-line
+diff, and a committed copy would drift from the tag it claims to be.
+"""
+
+import json
+from pathlib import Path
+from typing import Any, NoReturn
+
+from yaft import Feature
+
+CASES_DIR = Path(__file__).parent.parent / "suite" / "cases"
+
+# The case-file format versions this adapter implements. A file in any other
+# format may carry a field this adapter never reads, which would leave a rule
+# silently unenforced, so it is rejected instead.
+FORMATS = {"evaluation": 1, "decorator": 1, "mapping": 4}
+
+Case = dict[str, Any]
+
+
+def load(suite: str) -> list[Case]:
+ path = CASES_DIR / f"{suite}.json"
+ if not path.exists():
+ raise FileNotFoundError(
+ f"Conformance cases missing at {path}.\n"
+ "Run scripts/fetch-conformance.sh before the tests; CI does this first."
+ )
+ file = json.loads(path.read_text(encoding="utf-8"))
+ if file["suite"] != suite:
+ raise ValueError(f'{path} declares suite "{file["suite"]}", expected "{suite}"')
+ if file["version"] != FORMATS[suite]:
+ raise ValueError(
+ f"{path} is format version {file['version']}, but this adapter implements "
+ f"version {FORMATS[suite]}. Extend the adapter to the new format."
+ )
+ cases: list[Case] = file["cases"]
+ return cases
+
+
+def title(case: Case) -> str:
+ """Names a case for the test output, so a failure points at a rule."""
+ return f"{case['name']} [{', '.join(case['rules'])}]"
+
+
+def unsupported(kind: str, value: object, case: Case) -> NoReturn:
+ """Rejects a value the adapter does not know how to handle.
+
+ A case whose ``target``, ``toggle`` or ``expected`` this adapter has never
+ seen is a rule nothing enforces here. Skipping it would leave the suite
+ looking green while a requirement goes unchecked.
+ """
+ raise AssertionError(
+ f'Case "{case["name"]}" uses {kind} "{value}", which this adapter does not '
+ "implement. The conformance suite has gained a case this port does not cover "
+ "yet -- extend the adapter rather than skipping the case."
+ )
+
+
+def as_record(feature: Feature) -> dict[str, Any]:
+ """A feature in the suite's spelling, to compare with ``expected``."""
+ return {
+ "key": feature.key,
+ "value": feature.value,
+ "activeAt": feature.active_at,
+ "disabledAt": feature.disabled_at,
+ "tags": list(feature.tags),
+ }
diff --git a/tests/conformance/test_decorator.py b/tests/conformance/test_decorator.py
new file mode 100644
index 0000000..fb95486
--- /dev/null
+++ b/tests/conformance/test_decorator.py
@@ -0,0 +1,206 @@
+"""Runs the shared decorator cases against this port.
+
+These are scenarios rather than input/output pairs -- "the fallback receives
+the same receiver" cannot be written as JSON. Each case names a scenario and an
+outcome in port-neutral terms, and this module maps them onto Python
+decorators. An unknown value fails instead of being skipped.
+"""
+
+import asyncio
+import inspect
+from collections.abc import Iterator
+from typing import Any
+
+import pytest
+
+from yaft import ProviderNotSetError, feature_toggle, get_provider, set_provider
+
+from .cases import Case, load, title, unsupported
+
+CASES = load("decorator")
+KEY = "conformanceToggle"
+ORIGINAL = "original"
+FALLBACK = "fallback"
+
+
+class SwitchableProvider:
+ """A provider whose answer can change between decoration and call."""
+
+ def __init__(self, enabled: bool):
+ self.enabled = enabled
+
+ def is_enabled(self, key: str) -> bool:
+ return self.enabled
+
+
+@pytest.fixture(autouse=True)
+def restore_provider() -> Iterator[None]:
+ saved = get_provider()
+ yield
+ set_provider(saved)
+
+
+def set_up(case: Case) -> SwitchableProvider:
+ """Installs a provider in the state ``toggle`` starts in.
+
+ ``on-then-off`` is on while decorating and off by the time the class or
+ method is used, which is what separates "evaluated once" (R14) from
+ "evaluated per call" (R15).
+ """
+ match case["toggle"]:
+ case "on" | "on-then-off":
+ provider = SwitchableProvider(True)
+ case "off" | "off-then-on":
+ provider = SwitchableProvider(False)
+ case other:
+ unsupported("toggle", other, case)
+ set_provider(provider)
+ return provider
+
+
+def flip_if_two_phase(case: Case, provider: SwitchableProvider) -> None:
+ if case["toggle"] == "on-then-off":
+ provider.enabled = False
+ elif case["toggle"] == "off-then-on":
+ provider.enabled = True
+
+
+def test_loads_the_suite() -> None:
+ assert CASES
+
+
+@pytest.mark.parametrize("case", CASES, ids=title)
+def test_decorator(case: Case) -> None:
+ if case["toggle"] == "no-provider":
+ expect_decoration_error(case)
+ return
+ match case["target"]:
+ case "method":
+ method_case(case)
+ case "async-method":
+ async_method_case(case)
+ case "class":
+ class_case(case)
+ case other:
+ unsupported("target", other, case)
+
+
+def method_case(case: Case) -> None:
+ provider = set_up(case)
+ seen: dict[str, Any] = {"ran": ""}
+
+ def fallback_method(this: object, *args: object) -> str:
+ seen.update(ran=FALLBACK, args=args, receiver=this)
+ return FALLBACK
+
+ match case["fallback"]:
+ case "none":
+ fallback = None
+ case "method":
+ fallback = fallback_method
+ case other:
+ unsupported("fallback", f"{other} on a method target", case)
+
+ class Subject:
+ @feature_toggle(KEY, fallback)
+ def run(self, *args: object) -> str:
+ seen["ran"] = ORIGINAL
+ return ORIGINAL
+
+ flip_if_two_phase(case, provider)
+ instance = Subject()
+ result: object = instance.run("a", 1)
+
+ match case["expected"]:
+ case "original":
+ assert (seen["ran"], result) == (ORIGINAL, ORIGINAL)
+ case "fallback":
+ assert (seen["ran"], result) == (FALLBACK, FALLBACK)
+ case "nothing":
+ assert (seen["ran"], result) == ("", None)
+ case other:
+ unsupported("expected", other, case)
+
+ for assertion in case.get("assertions", []):
+ match assertion:
+ case "same-arguments":
+ assert seen["args"] == ("a", 1)
+ case "same-receiver":
+ assert seen["receiver"] is instance
+ case other:
+ unsupported("assertion", other, case)
+
+
+def async_method_case(case: Case) -> None:
+ provider = set_up(case)
+ if case["fallback"] != "none":
+ unsupported("fallback", f"{case['fallback']} on an async method", case)
+ seen = {"ran": ""}
+
+ class Subject:
+ @feature_toggle(KEY)
+ async def run(self) -> str:
+ seen["ran"] = ORIGINAL
+ return ORIGINAL
+
+ flip_if_two_phase(case, provider)
+ result = Subject().run()
+ # An await at the call site must not break: the result is awaitable even
+ # when the method is off (R18).
+ assert inspect.iscoroutine(result)
+ value = asyncio.run(result)
+
+ match case["expected"]:
+ case "original":
+ assert (seen["ran"], value) == (ORIGINAL, ORIGINAL)
+ case "resolved-nothing":
+ assert (seen["ran"], value) == ("", None)
+ case other:
+ unsupported("expected", other, case)
+
+
+def class_case(case: Case) -> None:
+ provider = set_up(case)
+
+ class FallbackClass:
+ def which(self) -> str:
+ return FALLBACK
+
+ match case["fallback"]:
+ case "none":
+ fallback: type | None = None
+ case "class":
+ fallback = FallbackClass
+ case other:
+ unsupported("fallback", f"{other} on a class target", case)
+
+ @feature_toggle(KEY, fallback)
+ class Subject:
+ def which(self) -> str:
+ return ORIGINAL
+
+ # A class is decided when it is decorated, so this flip must have no
+ # effect -- exactly what the two-phase cases check (R14).
+ flip_if_two_phase(case, provider)
+ which = Subject().which()
+
+ match case["expected"]:
+ case "original":
+ assert which == ORIGINAL
+ case "fallback":
+ assert which == FALLBACK
+ case "empty-shell":
+ # The shell still answers every method of the original, each with
+ # nothing, so calling into a disabled class does not raise.
+ assert which is None
+ case other:
+ unsupported("expected", other, case)
+
+
+def expect_decoration_error(case: Case) -> None:
+ """With no provider, decorating itself must fail, not the first call (R16)."""
+ if case["expected"] != "decoration-error":
+ unsupported("expected", case["expected"], case)
+ set_provider(None)
+ with pytest.raises(ProviderNotSetError, match="FeatureToggleProvider not set"):
+ feature_toggle(KEY)
diff --git a/tests/conformance/test_evaluation.py b/tests/conformance/test_evaluation.py
new file mode 100644
index 0000000..e65c0ed
--- /dev/null
+++ b/tests/conformance/test_evaluation.py
@@ -0,0 +1,28 @@
+"""Runs the shared evaluation cases against this port.
+
+Each case brings its own ``now``, which is why the clock is injectable (R9).
+"""
+
+import pytest
+
+from yaft import evaluate, normalise_feature, parse_timestamp
+
+from .cases import Case, load, title
+
+CASES = load("evaluation")
+
+
+def test_loads_the_suite() -> None:
+ assert CASES
+
+
+@pytest.mark.parametrize("case", CASES, ids=title)
+def test_evaluation(case: Case) -> None:
+ now = parse_timestamp(case["now"])
+ # A case whose own now does not parse would evaluate against nothing and
+ # pass or fail for the wrong reason.
+ assert now is not None, f"now {case['now']!r} does not parse"
+
+ raw = case["features"].get(case["key"])
+ feature = normalise_feature(raw) if raw is not None else None
+ assert evaluate(feature, now) is case["expected"]
diff --git a/tests/conformance/test_mapping.py b/tests/conformance/test_mapping.py
new file mode 100644
index 0000000..46e214c
--- /dev/null
+++ b/tests/conformance/test_mapping.py
@@ -0,0 +1,73 @@
+"""Runs the shared mapping cases against this port.
+
+These pin down how a backend response becomes provider data: which envelopes
+exist, how the two field spellings are reconciled, that a present but empty
+value is kept, and that a refresh replaces the data as a whole or not at all.
+"""
+
+from collections.abc import Mapping
+
+import pytest
+
+from yaft import Feature, LocalBooleanProvider, LocalFeatureProvider, normalise_collection
+
+from .cases import Case, as_record, load, title, unsupported
+
+CASES = load("mapping")
+
+
+def records(data: Mapping[str, Feature]) -> dict[str, object]:
+ return {key: as_record(feature) for key, feature in data.items()}
+
+
+def test_loads_the_suite() -> None:
+ assert CASES
+
+
+@pytest.mark.parametrize("case", CASES, ids=title)
+def test_mapping(case: Case) -> None:
+ match case["shape"]:
+ case "feature" if "held" in case:
+ refresh(case)
+ case "feature":
+ assert records(normalise_collection(case["response"])) == case["expected"]
+ case "boolean":
+ provider = LocalBooleanProvider(case["response"])
+ assert dict(provider.data) == case["expected"]
+ probes = case.get("isEnabled")
+ if not probes:
+ raise AssertionError(f'Case "{case["name"]}" has no isEnabled probes')
+ for key, enabled in probes.items():
+ assert (key, provider.is_enabled(key)) == (key, enabled)
+ case other:
+ unsupported("shape", other, case)
+
+
+def refresh(case: Case) -> None:
+ """A refresh case (R30, R32): ``held`` is loaded, then ``response`` arrives.
+
+ This port has no API provider yet, so the refresh goes through
+ ``LocalFeatureProvider.load``, which the API provider will use as well.
+ ``load`` must raise exactly when the case says the response is rejected
+ (R32), and leave the data the case expects. ``retry`` checks that a
+ rejected refresh does not block the next one; without a collection hash
+ there is nothing to record, so here it only checks the data it loads.
+ """
+ rejected = case["rejected"]
+ if not isinstance(rejected, bool):
+ unsupported("rejected", rejected, case)
+
+ provider = LocalFeatureProvider.from_response({"toggles": list(case["held"].values())})
+ assert records(provider.data) == case["held"]
+
+ if rejected:
+ with pytest.raises(ValueError, match="not a toggle group"):
+ provider.load(case["response"])
+ else:
+ provider.load(case["response"])
+ assert records(provider.data) == case["expected"]
+
+ retry = case.get("retry")
+ if retry is not None:
+ provider.load(retry["response"])
+ assert records(provider.data) == retry["expected"]
diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py
new file mode 100644
index 0000000..d5495bc
--- /dev/null
+++ b/tests/test_evaluate.py
@@ -0,0 +1,68 @@
+"""What the conformance suite cannot see: Python's own traps around time."""
+
+import logging
+from datetime import UTC, datetime, timedelta, timezone
+
+import pytest
+
+from yaft import Feature, evaluate, parse_timestamp, system_clock
+
+ON = Feature(key="f", value="true")
+
+
+def test_parses_to_an_aware_datetime() -> None:
+ assert parse_timestamp("2026-09-18T14:00:00+02:00") == datetime(2026, 9, 18, 12, tzinfo=UTC)
+ assert parse_timestamp("2026-09-18T12:00:00.123456Z") == datetime(
+ 2026, 9, 18, 12, 0, 0, 123_000, tzinfo=UTC
+ )
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ # \d without re.ASCII matches digits of other scripts, and int() reads
+ # them: this would parse as 2026-09-18.
+ "\u0662\u0660\u0662\u0666-\u0660\u0669-\u0661\u0668T12:00:00Z", # Arabic-Indic digits
+ # re.match with $ allows a trailing newline; fullmatch does not.
+ "2026-09-18T12:00:00Z\n",
+ " 2026-09-18T12:00:00Z",
+ ],
+)
+def test_rejects_what_a_loose_pattern_lets_through(value: str) -> None:
+ assert parse_timestamp(value) is None
+
+
+@pytest.mark.parametrize("value", [None, "", 1_789_732_800, ["2026-09-18T12:00:00Z"]])
+def test_ignores_what_is_not_a_timestamp_string(value: object) -> None:
+ assert parse_timestamp(value) is None
+
+
+def test_warns_about_an_ignored_bound(caplog: pytest.LogCaptureFixture) -> None:
+ with caplog.at_level(logging.WARNING, logger="yaft"):
+ assert parse_timestamp("2026-09-18") is None
+ assert "2026-09-18" in caplog.text
+
+
+def test_now_must_be_timezone_aware() -> None:
+ # A naive datetime names no instant; guessing would flip at different
+ # times on different machines.
+ with pytest.raises(ValueError, match="timezone-aware"):
+ evaluate(ON, datetime(2026, 9, 18, 12))
+
+
+def test_now_in_any_timezone_is_the_same_instant() -> None:
+ feature = Feature(key="f", value="true", active_at="2026-09-18T12:00:00Z")
+ berlin = timezone(timedelta(hours=2))
+ assert evaluate(feature, datetime(2026, 9, 18, 14, tzinfo=berlin))
+ assert not evaluate(feature, datetime(2026, 9, 18, 13, 59, 59, 999_999, tzinfo=berlin))
+
+
+def test_now_is_truncated_to_milliseconds() -> None:
+ # Like a millisecond clock: .000999 is still .000, so a bound at .001 has
+ # not been reached. Rounding would make it .001 and switch the feature off.
+ feature = Feature(key="f", value="true", disabled_at="2026-09-18T12:00:00.001Z")
+ assert evaluate(feature, datetime(2026, 9, 18, 12, 0, 0, 999, tzinfo=UTC))
+
+
+def test_system_clock_is_aware() -> None:
+ assert system_clock().utcoffset() == timedelta(0)
diff --git a/tests/test_mapping.py b/tests/test_mapping.py
new file mode 100644
index 0000000..8a73d78
--- /dev/null
+++ b/tests/test_mapping.py
@@ -0,0 +1,41 @@
+"""Mapping details the conformance suite does not pin down."""
+
+from yaft import (
+ Feature,
+ normalise_booleans,
+ normalise_collection,
+ normalise_feature,
+ normalise_group,
+)
+
+
+def test_a_value_that_is_not_a_string_is_not_set() -> None:
+ # A JSON boolean belongs in the boolean shape; in the feature shape it is
+ # not coerced to "true", so the feature is off (R1, R4).
+ assert normalise_feature({"key": "f", "value": True}).value == ""
+ assert normalise_feature({"key": "f", "value": 1}).value == ""
+
+
+def test_an_entry_whose_key_is_not_a_string_is_skipped() -> None:
+ assert normalise_collection({"toggles": [{"key": 7, "value": "true"}]}) == {}
+
+
+def test_tags_that_are_not_strings_are_dropped() -> None:
+ feature = normalise_feature({"key": "f", "value": "true", "tags": ["ok", 42, None, "fine"]})
+ assert feature.tags == ("ok", "fine")
+
+
+def test_a_list_of_toggles_is_not_a_group() -> None:
+ assert normalise_group([{"key": "f", "value": "true"}]) is None
+
+
+def test_single_toggle_is_a_group() -> None:
+ assert normalise_group({"key": "f", "value": "true"}) == {"f": Feature(key="f", value="true")}
+
+
+def test_booleans_keep_only_booleans() -> None:
+ assert normalise_booleans({"on": True, "off": False, "text": "true", "one": 1}) == {
+ "on": True,
+ "off": False,
+ }
+ assert normalise_booleans(None) == {}
diff --git a/tests/test_providers.py b/tests/test_providers.py
new file mode 100644
index 0000000..bc8a846
--- /dev/null
+++ b/tests/test_providers.py
@@ -0,0 +1,103 @@
+"""The local providers."""
+
+import json
+from datetime import UTC, datetime
+from pathlib import Path
+
+import pytest
+
+from yaft import Feature, FeatureProvider, LocalBooleanProvider, LocalFeatureProvider
+
+
+def fixed(instant: str) -> datetime:
+ return datetime.fromisoformat(instant).astimezone(UTC)
+
+
+def write(tmp_path: Path, content: object) -> Path:
+ path = tmp_path / "features.json"
+ path.write_text(json.dumps(content), encoding="utf-8")
+ return path
+
+
+def test_feature_provider_evaluates_against_its_clock() -> None:
+ now = fixed("2026-09-18T12:00:00+00:00")
+ provider = LocalFeatureProvider(
+ {"f": Feature(key="f", value="true", active_at="2026-09-18T13:00:00Z")},
+ clock=lambda: now,
+ )
+ assert not provider.is_enabled("f")
+ now = fixed("2026-09-18T13:00:00+00:00")
+ assert provider.is_enabled("f")
+ assert not provider.is_enabled("missing")
+
+
+def test_reads_the_keyed_file_format_of_yaft_ts(tmp_path: Path) -> None:
+ path = write(
+ tmp_path,
+ {
+ "on": {"key": "on", "value": "true", "activeAt": "", "disabledAt": ""},
+ "off": {"value": "false"},
+ "stray": "not a feature",
+ },
+ )
+ provider = LocalFeatureProvider.from_file(path)
+ assert set(provider.data) == {"on", "off"}
+ assert provider.is_enabled("on")
+ assert not provider.is_enabled("off")
+
+
+def test_reads_a_backend_response_from_a_file(tmp_path: Path) -> None:
+ path = write(tmp_path, {"toggles": [{"Key": "uuid|a", "Value": "true"}]})
+ assert LocalFeatureProvider.from_file(path).is_enabled("uuid|a")
+
+
+def test_a_missing_file_raises(tmp_path: Path) -> None:
+ # Starting with every feature off would hide the misconfiguration.
+ with pytest.raises(FileNotFoundError):
+ LocalFeatureProvider.from_file(tmp_path / "absent.json")
+ with pytest.raises(FileNotFoundError):
+ LocalBooleanProvider.from_file(tmp_path / "absent.json")
+
+
+def test_load_keeps_the_data_when_the_body_is_not_a_group() -> None:
+ provider = LocalFeatureProvider.from_response({"key": "f", "value": "true"})
+ with pytest.raises(ValueError, match="not a toggle group"):
+ provider.load({"error": "Bad Gateway"})
+ assert provider.is_enabled("f")
+
+ provider.load({"toggles": []})
+ assert not provider.is_enabled("f")
+
+
+def test_data_is_read_only() -> None:
+ provider = LocalFeatureProvider({"f": Feature(key="f", value="true")})
+ with pytest.raises(TypeError):
+ provider.data["f"] = Feature(key="f", value="false") # type: ignore[index]
+
+
+def test_constructor_copies_the_data() -> None:
+ data = {"f": Feature(key="f", value="true")}
+ provider = LocalFeatureProvider(data)
+ data.clear()
+ assert provider.is_enabled("f")
+
+
+def test_boolean_provider_drops_non_booleans(tmp_path: Path) -> None:
+ provider = LocalBooleanProvider.from_file(write(tmp_path, {"on": True, "text": "true"}))
+ assert provider.data == {"on": True}
+ assert provider.is_enabled("on")
+ assert not provider.is_enabled("text")
+
+ provider.replace({"on": False, "one": 1})
+ assert provider.data == {"on": False}
+ assert not provider.is_enabled("on")
+
+
+def test_boolean_file_must_be_an_object(tmp_path: Path) -> None:
+ with pytest.raises(ValueError, match="expected a JSON object"):
+ LocalBooleanProvider.from_file(write(tmp_path, [True]))
+
+
+def test_providers_satisfy_the_protocol() -> None:
+ assert isinstance(LocalFeatureProvider(), FeatureProvider)
+ assert isinstance(LocalBooleanProvider(), FeatureProvider)
diff --git a/tests/test_toggle.py b/tests/test_toggle.py
new file mode 100644
index 0000000..3e9010d
--- /dev/null
+++ b/tests/test_toggle.py
@@ -0,0 +1,268 @@
+"""The decorator beyond the shared cases: Python's other kinds of callables."""
+
+import asyncio
+import inspect
+from collections.abc import AsyncIterator, Iterator
+from types import TracebackType
+from typing import Any
+
+import pytest
+
+from yaft import ProviderNotSetError, feature_toggle, get_provider, set_provider
+
+
+class Switch:
+ def __init__(self, enabled: bool = True):
+ self.enabled = enabled
+
+ def is_enabled(self, key: str) -> bool:
+ return self.enabled
+
+
+@pytest.fixture
+def switch() -> Iterator[Switch]:
+ saved = get_provider()
+ provider = Switch()
+ set_provider(provider)
+ yield provider
+ set_provider(saved)
+
+
+async def collect(source: AsyncIterator[Any]) -> list[Any]:
+ return [item async for item in source]
+
+
+def test_keeps_name_and_coroutine_function_marker(switch: Switch) -> None:
+ @feature_toggle("k")
+ async def fetch() -> str:
+ """Fetches."""
+ return "data"
+
+ # Frameworks such as FastAPI decide whether to await by this check.
+ assert inspect.iscoroutinefunction(fetch)
+ assert (fetch.__name__, fetch.__doc__) == ("fetch", "Fetches.")
+
+
+def test_async_method_takes_a_sync_or_async_fallback(switch: Switch) -> None:
+ async def async_fallback() -> str:
+ return "async"
+
+ @feature_toggle("k", fallback=lambda: "sync")
+ async def with_sync() -> str:
+ return "original"
+
+ @feature_toggle("k", fallback=async_fallback)
+ async def with_async() -> str:
+ return "original"
+
+ switch.enabled = False
+ assert asyncio.run(with_sync()) == "sync"
+ assert asyncio.run(with_async()) == "async"
+
+
+def test_generator_off_is_an_empty_iterator(switch: Switch) -> None:
+ @feature_toggle("k")
+ def numbers() -> Iterator[int]:
+ yield from (1, 2)
+
+ assert list(numbers()) == [1, 2]
+ switch.enabled = False
+ # None here would break the caller's for loop, the way None breaks await.
+ assert list(numbers()) == []
+
+
+def test_generator_fallback(switch: Switch) -> None:
+ @feature_toggle("k", fallback=lambda: iter([9]))
+ def numbers() -> Iterator[int]:
+ yield 1
+
+ switch.enabled = False
+ assert list(numbers()) == [9]
+
+
+def test_async_generator(switch: Switch) -> None:
+ async def fallback() -> AsyncIterator[int]:
+ yield 9
+
+ @feature_toggle("k")
+ async def plain() -> AsyncIterator[int]:
+ yield 1
+
+ @feature_toggle("k", fallback=fallback)
+ async def with_fallback() -> AsyncIterator[int]:
+ yield 1
+
+ assert asyncio.run(collect(plain())) == [1]
+ switch.enabled = False
+ assert asyncio.run(collect(plain())) == []
+ assert asyncio.run(collect(with_fallback())) == [9]
+
+
+def test_static_and_class_methods(switch: Switch) -> None:
+ class Subject:
+ name = "subject"
+
+ @feature_toggle("k")
+ @staticmethod
+ def static() -> str:
+ return "static"
+
+ @feature_toggle("k", fallback=lambda cls: f"fallback {cls.name}")
+ @classmethod
+ def klass(cls) -> str:
+ return f"class {cls.name}"
+
+ assert (Subject.static(), Subject.klass()) == ("static", "class subject")
+ switch.enabled = False
+ # Typed as the original's str; off, the static method returns nothing.
+ results: tuple[object, object] = (Subject.static(), Subject.klass())
+ assert results == (None, "fallback subject")
+
+
+def test_fallback_can_be_a_method_of_the_same_class(switch: Switch) -> None:
+ class Pricing:
+ rate = 2
+
+ def old(self, amount: int) -> int:
+ return amount
+
+ @feature_toggle("k", fallback=old)
+ def new(self, amount: int) -> int:
+ return amount * self.rate
+
+ switch.enabled = False
+ assert Pricing().new(5) == 5
+
+
+def test_empty_shell_stands_in_for_the_class(switch: Switch) -> None:
+ class Base:
+ def inherited(self) -> str:
+ return "inherited"
+
+ class Service(Base):
+ limit = 3
+
+ def __init__(self, url: str):
+ self.url = url
+
+ def run(self) -> str:
+ return "run"
+
+ async def fetch(self) -> str:
+ return "fetch"
+
+ def items(self) -> Iterator[int]:
+ yield 1
+
+ @property
+ def status(self) -> str:
+ return "ok"
+
+ @status.setter
+ def status(self, value: str) -> None:
+ pass
+
+ @property
+ def read_only(self) -> str:
+ return "ro"
+
+ @staticmethod
+ def helper() -> str:
+ return "helper"
+
+ def __call__(self) -> str:
+ return "called"
+
+ def __enter__(self) -> "Service":
+ return self
+
+ def __exit__(
+ self,
+ kind: type[BaseException] | None,
+ error: BaseException | None,
+ trace: TracebackType | None,
+ ) -> None:
+ pass
+
+ switch.enabled = False
+ Shell = feature_toggle("k")(Service)
+
+ assert Shell is not Service
+ assert Shell.__name__ == "Service"
+ # Created wherever the original was, with the original's arguments.
+ shell = Shell("https://example.test")
+ assert shell.run() is None
+ assert shell.inherited() is None
+ assert asyncio.run(shell.fetch()) is None
+ assert list(shell.items()) == []
+ assert shell.status is None
+ shell.status = "ignored"
+ with pytest.raises(AttributeError):
+ shell.read_only = "x"
+ assert Shell.helper() is None
+ assert shell() is None
+ with shell as entered:
+ assert entered is None
+ # Only behaviour is stubbed; data attributes are not copied.
+ assert not hasattr(shell, "limit")
+
+
+def test_shell_stubs_keep_their_own_names(switch: Switch) -> None:
+ # functools.wraps mutates the function it is handed; wrapping one shared
+ # function would give every stub the name of the last one.
+ class Service:
+ def first(self) -> None: ...
+
+ def second(self) -> None: ...
+
+ switch.enabled = False
+ shell = feature_toggle("k")(Service)
+ assert (shell.first.__name__, shell.second.__name__) == ("first", "second")
+
+
+def test_class_fallback_must_be_a_class(switch: Switch) -> None:
+ with pytest.raises(TypeError, match="must be a class"):
+
+ @feature_toggle("k", fallback=lambda: None)
+ class Subject:
+ pass
+
+
+def test_function_fallback_must_be_a_function(switch: Switch) -> None:
+ class NotAFunction:
+ pass
+
+ with pytest.raises(TypeError, match="must be a function"):
+
+ @feature_toggle("k", fallback=NotAFunction)
+ def run() -> None: ...
+
+ with pytest.raises(TypeError, match="must be a function"):
+
+ @feature_toggle("k", fallback="old")
+ def run_by_name() -> None: ...
+
+
+def test_rejects_what_it_cannot_decorate(switch: Switch) -> None:
+ with pytest.raises(TypeError, match="cannot decorate"):
+ feature_toggle("k")(42)
+
+
+def test_provider_removed_after_decoration_fails_at_the_call(switch: Switch) -> None:
+ @feature_toggle("k")
+ def run() -> str:
+ return "run"
+
+ set_provider(None)
+ with pytest.raises(ProviderNotSetError):
+ run()
+
+
+def test_the_provider_is_asked_on_every_call(switch: Switch) -> None:
+ @feature_toggle("k")
+ def run() -> str:
+ return "run"
+
+ replacement = Switch(enabled=False)
+ set_provider(replacement)
+ assert run() is None
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..0b34f57
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,381 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+resolution-markers = [
+ "python_full_version >= '3.15'",
+ "python_full_version < '3.15'",
+]
+
+[[package]]
+name = "ast-serialize"
+version = "0.11.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/1e/4f6082cdd6e5a29093513e9a3eabc5ed1c5331a9a84386b2fece80a00a48/ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450", size = 954387, upload-time = "2026-09-13T18:48:55.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/2e/beec3364eef4b01793a676d8cd16e9014c42044a5505000ceae3955e33fa/ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277", size = 897089, upload-time = "2026-09-13T18:47:22.69Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/d7/ef56443df2891c6ba2c4019c2cb3dcaf97c9948da6d963068e04e8dac6ea/ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77", size = 1235218, upload-time = "2026-09-13T18:47:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/42/8d/cff58d17ba1d0272ff0b7ab5d3bdfcf8f47317eb0f47c001d394bffebf95/ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32", size = 1216399, upload-time = "2026-09-13T18:47:26.202Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d2/a1da7675af5f42335c36e4da6d86ef4fd7168cead18de81df0a2d6faeb1a/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4", size = 1282064, upload-time = "2026-09-13T18:47:27.787Z" },
+ { url = "https://files.pythonhosted.org/packages/97/89/5a400a13b2c9c0152ebb5ad45408a3fe5e4e60e325d3ac4e5cf6e915a0cc/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6", size = 1285864, upload-time = "2026-09-13T18:47:29.667Z" },
+ { url = "https://files.pythonhosted.org/packages/02/b8/80a381c70fd49f0316fb0383c4f9e4c13e81b010b64889bd45898ce8f5f4/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded", size = 1554755, upload-time = "2026-09-13T18:47:31.257Z" },
+ { url = "https://files.pythonhosted.org/packages/90/97/dcaa34a32d2db789221c125b3eb10feb5089715fe53d9874d627afc26231/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8", size = 1301807, upload-time = "2026-09-13T18:47:32.714Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9a/84a22420cb312642d7d31547c644d09a3d101418c6d6b9ef2ec30735cf11/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234", size = 1301941, upload-time = "2026-09-13T18:47:34.418Z" },
+ { url = "https://files.pythonhosted.org/packages/20/8a/aa5f3dcf1aed9678c25982f40d366004e3c0cac47bc0c240f6b837dcbb1f/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b", size = 1307910, upload-time = "2026-09-13T18:47:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/78/79/91a5102797fe3dc992171382d8579bcb33cbd1424b864ad3117ac43fb3fe/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e", size = 1356258, upload-time = "2026-09-13T18:47:37.984Z" },
+ { url = "https://files.pythonhosted.org/packages/51/52/54eeef9918e187ced417c4363eecea66975314cd5b9c91759eef7f7b714b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f", size = 1459057, upload-time = "2026-09-13T18:47:39.891Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/cb/fd84b52b15d42f2423319cffd1fb7f1e9df5d5198e69ab0b449c450254cf/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a", size = 1562447, upload-time = "2026-09-13T18:47:41.727Z" },
+ { url = "https://files.pythonhosted.org/packages/be/92/9fb34f2e64b84a63cca92fb86bd0847b995a63b67477f44c20502fb60352/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750", size = 1556423, upload-time = "2026-09-13T18:47:43.357Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0f/c43c44449e7ebc4e83ebd48750088fb06234622faa2d62d2a6dc8970d2a3/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57", size = 1687156, upload-time = "2026-09-13T18:47:44.995Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/a7/9e520f4a79b639da9ee20c1e747c3d739329e902fc55ac38065f25419f56/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439", size = 1481008, upload-time = "2026-09-13T18:47:46.602Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a8/bdd3989f19de09cffcd8179c131f6741a5a8619705fc75b09541ff61530b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c", size = 1501597, upload-time = "2026-09-13T18:47:48.265Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/8a/ca2dce2950875a4ef1d7c298196f803b0adcdb7c15ed0cecc71d84bccd70/ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f", size = 1119503, upload-time = "2026-09-13T18:47:49.896Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/12/3f38e3613d07c46f9f81c5b1352748c6552397cc52825502e2c6ae44c6ea/ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b", size = 1156828, upload-time = "2026-09-13T18:47:51.454Z" },
+ { url = "https://files.pythonhosted.org/packages/22/90/f89a4f67428a261daafdb69a0d0132c27933268702d1ba47e0b61c51aff1/ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a", size = 1128298, upload-time = "2026-09-13T18:47:53.008Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/55/a1962188abf0e62d84d55892bb044347e434711763b9a1d4ad867a70c1be/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85", size = 1235457, upload-time = "2026-09-13T18:47:54.554Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/ad/439c2959150718446af76fbe2f4000f35eba9869ef8564f3d9a3d0b1c370/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32", size = 1215705, upload-time = "2026-09-13T18:47:56.178Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d8/2c6542fc3e7c56a0a25d8d12d034d5a2d2e1900e292567b1c1dca8e83124/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34", size = 1282530, upload-time = "2026-09-13T18:47:57.619Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ad/6f6755cd0842db46c3b10b1e4735f14aad78d71dea4753eb46933101711b/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c", size = 1287792, upload-time = "2026-09-13T18:47:59.227Z" },
+ { url = "https://files.pythonhosted.org/packages/03/40/5da672f5dd23fb7dc0c884c97711e56a3540f2fe3c4355a81f8beb385911/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3", size = 1557971, upload-time = "2026-09-13T18:48:00.96Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c7/2bb25684f697801eb72866fdb94ed5edbff3867ce878b0e542a4a5b9dab9/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6", size = 1303230, upload-time = "2026-09-13T18:48:02.337Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/85/754681846f26e0ff1da729b1ffe3171e93c22f0aa6ec3cea5b14e3703846/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5", size = 1302271, upload-time = "2026-09-13T18:48:03.915Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/dc/f5521d8cb44b69095c3982ae3658a12c403e0efa19e51aeb9c8a79dff60c/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff", size = 1309529, upload-time = "2026-09-13T18:48:05.562Z" },
+ { url = "https://files.pythonhosted.org/packages/73/0d/649182c7fd7c4f782279bed514de2dd67e48a5afecb605a098d64fdc01fd/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c", size = 1356869, upload-time = "2026-09-13T18:48:07.534Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/cc/aff4d84c16afa742d13a75384127c7d24594dc8c304f0558a15924fd51af/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d", size = 1460006, upload-time = "2026-09-13T18:48:09.162Z" },
+ { url = "https://files.pythonhosted.org/packages/94/a7/891cbec2e5e0d7159196159d3ff0646622f3120ff4576c839ac2dd56c719/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70", size = 1562935, upload-time = "2026-09-13T18:48:10.978Z" },
+ { url = "https://files.pythonhosted.org/packages/45/c4/2c8c4498340ea9aff87a9fd408309aa25d56dd51d7bbddfdb46a3c31424a/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437", size = 1557109, upload-time = "2026-09-13T18:48:12.616Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/8b/c5d4e5226fa18885fe17f949aee3ab1aeb8389c384d946ec1b7c9489cc94/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7", size = 1691603, upload-time = "2026-09-13T18:48:14.293Z" },
+ { url = "https://files.pythonhosted.org/packages/73/d6/1d2ca472586f9e3416a289a22f36eeb6dd6f47d77b1a4aba358405babbc7/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e", size = 1483053, upload-time = "2026-09-13T18:48:15.789Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/16/d3703a7c1e3c76b144ac9349a54d3926d0749918a8dc13a66cede208b8ec/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d", size = 1502499, upload-time = "2026-09-13T18:48:17.405Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/e4/d974e55c2e247ef26ed1df01c74940583db9a5b3a8bcaad5732c6e2047fb/ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4", size = 1119739, upload-time = "2026-09-13T18:48:19.005Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/00/d229443488e095054d5e0c0cc20689a2633b899d735849ff1b2c8e4f0cbf/ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f", size = 1158602, upload-time = "2026-09-13T18:48:20.524Z" },
+ { url = "https://files.pythonhosted.org/packages/11/75/389fc1a6cfa0c4b2ce522f47d8401329d8bb11732e516d46465960fef1d9/ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d", size = 1128842, upload-time = "2026-09-13T18:48:22.052Z" },
+ { url = "https://files.pythonhosted.org/packages/45/7d/c4f36898f19c728d091cdfdf960c9488e8d82bbe2e49ba13c05f43907a5d/ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1", size = 897204, upload-time = "2026-09-13T18:48:23.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/54/f67120006fc73a55b6d057d4662d061fbb4eceafce3047c76ca8b382eb11/ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4", size = 1240734, upload-time = "2026-09-13T18:48:25.287Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/7e/8f2ab68bddbe58a66fbbaad87beeae3e7d7edddb17263d1fc423936cf34d/ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4", size = 1228053, upload-time = "2026-09-13T18:48:26.788Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/1b/8a69ab68f4c1603819f0481d756abdd8caf27cec7f1d77caa71007ebe997/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b", size = 1292542, upload-time = "2026-09-13T18:48:28.295Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ce/872f2e00f0467c289e483f0a34543463347243a2d0632748d89fcee5e0dc/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd", size = 1294791, upload-time = "2026-09-13T18:48:29.969Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/82/36277c12af861c64b375c316135d8feffe3f400568463a8d2b2de4c2c4fb/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82", size = 1567583, upload-time = "2026-09-13T18:48:31.85Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d7/ec643df91cea8bcbcb4e8011d6a8b08e5119b84f9554879f3e3c786d29d1/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1", size = 1312878, upload-time = "2026-09-13T18:48:33.495Z" },
+ { url = "https://files.pythonhosted.org/packages/04/6f/4c992cd7841ba589fefb14ddc9aff2f6db7f2a615d4074f9ad04115b5ce0/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc", size = 1312642, upload-time = "2026-09-13T18:48:35.294Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e3/22aaa209c231a83cfea004fd67dee7a7a54da3f169c6c460b14b96887385/ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365", size = 1319776, upload-time = "2026-09-13T18:48:36.866Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/f7/d4685fb54d10108ce44d3bc893ef670854d61645d47ed96d73524db90c23/ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575", size = 1365324, upload-time = "2026-09-13T18:48:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/42/3a/250643ffad02bda520c50a9a5f02a5d43259a06f34ce393c91761d134d7e/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719", size = 1467653, upload-time = "2026-09-13T18:48:40.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/da/af66a646b9b7f8fdec95ce83fc7b1fe538b06864bc79bd554ac4fae2e6ea/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa", size = 1571914, upload-time = "2026-09-13T18:48:41.968Z" },
+ { url = "https://files.pythonhosted.org/packages/34/82/77a9714564b9e8800087a8afec41527c65c39e49282baae2ac847b9c1c6a/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9", size = 1569862, upload-time = "2026-09-13T18:48:43.701Z" },
+ { url = "https://files.pythonhosted.org/packages/65/06/fa77b52f46b9bd6dcd8ff2b880e3781f8c1a316bb1342bc3de92907c6f96/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106", size = 1699020, upload-time = "2026-09-13T18:48:45.261Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/09/239c83153c7e0798e5867d6909cb06f53dccfef02f6999c8e2e21ecb98c3/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07", size = 1492869, upload-time = "2026-09-13T18:48:46.922Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/eb/6108fb9a43fc7ab5529856e38e33c6e3e064fbfe375fdcbb208c7cd5438d/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e", size = 1511621, upload-time = "2026-09-13T18:48:48.667Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/82/60367e58ef346a41ebc90d3f28593c1b8f5c2cb5314c7b2bbd98910ee131/ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99", size = 1125873, upload-time = "2026-09-13T18:48:50.661Z" },
+ { url = "https://files.pythonhosted.org/packages/23/bf/b419c3205ce1143ba7c69baef4f0ba43c14d8712113bf34f9e0d27d609be/ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484", size = 1165434, upload-time = "2026-09-13T18:48:52.169Z" },
+ { url = "https://files.pythonhosted.org/packages/91/a7/c8bbb2173f7a7131b3b2412035b2d814ab5ef2ce9799bd06f07c451640e4/ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27", size = 1136031, upload-time = "2026-09-13T18:48:54.04Z" },
+]
+
+[[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 = "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.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" },
+ { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" },
+ { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" },
+ { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" },
+ { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" },
+ { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" },
+ { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" },
+ { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" },
+ { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" },
+ { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" },
+ { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" },
+ { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" },
+ { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" },
+ { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" },
+ { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" },
+ { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" },
+ { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" },
+ { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" },
+ { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" },
+ { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" },
+ { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" },
+ { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" },
+ { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" },
+ { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" },
+ { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" },
+ { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" },
+ { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" },
+ { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" },
+ { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" },
+ { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" },
+ { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" },
+ { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" },
+ { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" },
+ { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" },
+ { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ast-serialize" },
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/be/c624d4241484f37dc62839e177ab607a9b8b3e96f0866544ca99e8e41d51/mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531", size = 13936739, upload-time = "2026-08-15T03:03:26.475Z" },
+ { url = "https://files.pythonhosted.org/packages/53/84/e3cf72f90dce5960871c82551c8fba6da05fc1018f79be41c047bd126bdd/mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8", size = 14166460, upload-time = "2026-08-15T03:01:50.565Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/ff/6b97d58aa0f79a5ab9b472db1f6d6df1b11a51d74d0c08ab3760d3a613ba/mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8", size = 15100476, upload-time = "2026-08-15T03:03:12.079Z" },
+ { url = "https://files.pythonhosted.org/packages/da/f0/cbb4b7d2ae3ac635f6b4f2d9b04070b8a92edf50da599d3b39e5ed109001/mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01", size = 15347826, upload-time = "2026-08-15T03:03:02.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/10/91dcdc6f8d43fc08e6a06ab1f9732f3abaaf835ac1b2e67b9dff56910855/mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1", size = 11142615, upload-time = "2026-08-15T03:03:36.316Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8a/28d54535bf4b9aa43b2d8918c2ef660378b9f66b23d78dcee052744ae622/mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6", size = 10141145, upload-time = "2026-08-15T03:03:07.406Z" },
+ { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" },
+ { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" },
+ { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" },
+ { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" },
+ { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
+ { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
+ { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
+ { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
+ { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
+ { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
+ { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
+ { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
+ { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
+ { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
+ { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
+ { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
+ { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
+]
+
+[[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 = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
+[[package]]
+name = "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.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.16.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
+ { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
+ { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
+ { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
+ { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
+ { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
+ { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
+ { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
+ { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
+ { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "yaft"
+version = "0.1.0"
+source = { editable = "." }
+
+[package.dev-dependencies]
+dev = [
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "ruff" },
+]