diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..157dfee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 30 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 30 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7617e1f..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: CI - -on: - push: - branches: ["master"] - pull_request: - branches: ["master"] - -jobs: - tox: - runs-on: ubuntu-latest - strategy: - max-parallel: 7 - matrix: - python-version: - - 3.8 - - 3.9 - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - pypy-3.9 - - pypy-3.10 - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Install tox - run: | - python -m pip install --upgrade pip setuptools - pip install --upgrade tox tox-gh-actions - - name: Initialize tox envs - run: | - tox --parallel auto --notest - - name: Test with tox - run: | - tox --parallel 0 - - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..0e796f8 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,40 @@ +name: CodSpeed + +on: + push: + branches: [ master ] + paths-ignore: + - 'docs/**' + - '**/*.md' + pull_request: + branches: [ master ] + paths-ignore: + - 'docs/**' + - '**/*.md' + +permissions: + contents: read + id-token: write # required for OIDC authentication with CodSpeed + +jobs: + benchmarks: + name: Benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + version: "latest" + - name: Install dependencies + run: uv sync --all-extras + - name: Run the benchmarks + uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3 + with: + mode: simulation + run: uv run pytest tests/ --codspeed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c977778 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,114 @@ +name: Release + +on: + push: + tags: + - v* + +permissions: + contents: read + +env: + CIBW_BUILD_VERBOSITY: 1 + +jobs: + compiled-wheels: + name: Compiled wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, windows-latest] + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + version: "latest" + + - name: Set version from tag + shell: bash + run: | + set -euo pipefail + python - <<'PY' + import os, pathlib, re + version = os.environ["GITHUB_REF_NAME"].removeprefix("v") + path = pathlib.Path("h11_mypyc/_version.py") + path.write_text( + re.sub(r"^__version__ = .*$", f'__version__ = "{version}"', path.read_text(), flags=re.M) + ) + print("building h11_mypyc", version) + PY + + - uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + + sdist-and-pure-wheel: + name: sdist and pure-Python wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + version: "latest" + python-version: "3.12" + + - name: Set version from tag + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + sed -i "s/^__version__ = .*/__version__ = \"${version}\"/" h11_mypyc/_version.py + echo "Building h11_mypyc ${version}" + + - name: Build + run: | + set -euxo pipefail + uv build + ls -l dist + # A platform-tagged wheel here would mean H11_MYPYC leaked in and the + # fallback is gone, leaving PyPy with nothing to install. + ls dist/*-py3-none-any.whl + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sdist-and-pure-wheel + path: dist/* + + publish: + name: Publish to PyPI + needs: [compiled-wheels, sdist-and-pure-wheel] + runs-on: ubuntu-latest + environment: + name: release + permissions: + contents: read + id-token: write # trusted publishing to PyPI + steps: + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + version: "latest" + python-version: "3.12" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: dist + merge-multiple: true + + - name: Show what is being published + run: ls -l dist + + - name: Publish + run: uv publish --trusted-publishing always dist/* diff --git a/.github/workflows/release_docs.yml b/.github/workflows/release_docs.yml new file mode 100644 index 0000000..2c78eac --- /dev/null +++ b/.github/workflows/release_docs.yml @@ -0,0 +1,45 @@ +name: Release docs + +on: + push: + branches: ["master"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + python-version: "3.12" + version: "latest" + + - name: Install dependencies + run: uv sync --group docs --frozen + + - name: Build site + run: uv run --no-sync zensical build --strict + + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: site + + - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + id: deployment diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b125594 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,93 @@ +name: Tests + +on: + push: + branches: [ master ] + paths-ignore: + - 'docs/**' + - '**/*.md' + pull_request: + branches: [ master ] + paths-ignore: + - 'docs/**' + - '**/*.md' + workflow_dispatch: # for CodeSpeed + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + version: "latest" + python-version: "3.12" + - name: Install dependencies + run: uv sync --group lint + - name: Run ruff + run: uv run ruff check h11_mypyc tests + - name: Run zizmor + run: uv run zizmor .github + + type-check: + name: Type check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + version: "latest" + python-version: "3.12" + - name: Install dependencies + run: uv sync --group build + - name: Run type checkers + run: uv run mypy h11_mypyc + + python-tests: + name: Python tests + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + cache-suffix: ${{ matrix.python-version }} + version: "latest" + python-version: ${{ matrix.python-version }} + - name: Run tests + run: uv run --group test pytest + + mypyc-tests: + name: Tests against the compiled build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + version: "latest" + python-version: "3.12" + - name: Install dependencies + run: uv sync --group build --group test + - name: Compile with mypyc + run: uv run env H11_MYPYC=1 python setup.py build_ext --inplace + - name: Run tests against the compiled build + run: uv run pytest tests -q diff --git a/.gitignore b/.gitignore index 7ad0c44..f8cb680 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,6 @@ # Project-specific generated files -docs/source/_static/CLIENT.dot -docs/source/_static/CLIENT.svg -docs/source/_static/SERVER.dot -docs/source/_static/SERVER.svg -docs/source/_static/special-states.dot -docs/source/_static/special-states.svg -docs/build/ +/site/ +.zensical/ bench/results/ bench/env/ @@ -63,6 +58,4 @@ coverage.xml # Django stuff: *.log *.pot - -# Sphinx documentation -doc/_build/ +wheelhouse/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..53a51de --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +fail_fast: false +default_language_version: + python: python3.12 +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.2 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.29.0 + hooks: + - id: zizmor + files: ^\.github/ + types: [file] + pass_filenames: false + args: ["--no-progress", ".github/workflows"] + + - repo: builtin + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 38d4fcc..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 - -build: - os: ubuntu-22.04 - apt_packages: - - graphviz - tools: - python: "3.8" - -sphinx: - configuration: docs/source/conf.py - -python: - install: - - method: pip - path: . - - requirements: docs/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index f2f65de..62e2467 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ -include LICENSE.txt README.rst notes.org tiny-client-demo.py h11/py.typed -recursive-include docs * -recursive-include h11/tests * -recursive-include fuzz * -prune docs/build +prune tests +prune docs +prune fuzz +global-exclude __pycache__ *.py[cod] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5588862 --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +.PHONY: lint typecheck test build clean mypyc mypyc-clean test-mypyc bench bench-compare bench-py \ + docs docs-serve + +lint: + @uv run ruff check h11_mypyc tests --fix + +typecheck: + @uv run mypy h11_mypyc + +test: + @uv run pytest tests -q + +build: + @uv build + +clean: mypyc-clean + @rm -rf dist site + +# Compiles in place, next to the .py sources: Python prefers the .so, so +# `import h11_mypyc` picks up the compiled build. `make mypyc-clean` reverts it. +mypyc: mypyc-clean + @uv run env H11_MYPYC=1 python setup.py build_ext --inplace + +mypyc-clean: + @rm -rf build *.egg-info + @rm -f h11_mypyc/*.so *.so + +# The compiled build is where annotations become runtime checks, so it can fail +# where the interpreted one passes. +test-mypyc: mypyc + @uv run pytest tests -q + +# --- docs ---------------------------------------------------------------- +# +# Zensical renders docs/src/*.md into site/. Everything else under docs/ is +# build input that must stay outside docs_dir, because Zensical publishes every +# file it finds there and has no exclude setting -- that is where the Mermaid +# state-machine diagrams in docs/includes/ live. They mirror the transition +# tables in h11_mypyc/_state.py, so edit them alongside it. + +# --clean, not the incremental default: Zensical's cache keys off the page +# sources, so editing a snippet under docs/includes/ does not invalidate the +# pages that pull it in. +docs: + @uv run --group docs zensical build --clean --strict + +# The preview server has the same blind spot: restart it after editing a +# snippet under docs/includes/. +docs-serve: + @uv run --group docs zensical serve + +# --- benchmarks ---------------------------------------------------------- + +# PYTHONPATH=. finds mypyc's shared runtime module, which lands in the repo +# root while a script run puts only its own directory on sys.path. +bench: + @PYTHONPATH=. uv run python -c "import h11_mypyc; \ + print('mode:', 'mypyc' if h11_mypyc._connection.__file__.endswith('.so') else 'pure Python')" + @PYTHONPATH=. uv run python bench/benchmarks/benchmarks.py + +bench-compare: + @$(MAKE) --no-print-directory mypyc-clean + @echo "=== pure Python ===" + @$(MAKE) --no-print-directory bench + @$(MAKE) --no-print-directory mypyc + @echo "=== mypyc ===" + @$(MAKE) --no-print-directory bench + @$(MAKE) --no-print-directory mypyc-clean + +bench-py: + @uv run pytest tests/test_benchmarks.py --codspeed diff --git a/README.rst b/README.md similarity index 50% rename from README.rst rename to README.md index 5f28616..2ff2e42 100644 --- a/README.rst +++ b/README.md @@ -1,80 +1,69 @@ -h11 -=== +# h11-mypyc -.. image:: https://travis-ci.org/python-hyper/h11.svg?branch=master - :target: https://travis-ci.org/python-hyper/h11 - :alt: Automated test status - -.. image:: https://codecov.io/gh/python-hyper/h11/branch/master/graph/badge.svg - :target: https://codecov.io/gh/python-hyper/h11 - :alt: Test coverage - -.. image:: https://readthedocs.org/projects/h11/badge/?version=latest - :target: http://h11.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status +> A fork of [h11](https://github.com/python-hyper/h11) whose annotations have been +> made accurate enough for [mypyc](https://mypyc.readthedocs.io/) to compile it, +> which is worth ~1.9x on the benchmark suite. It installs as `h11_mypyc`, so it +> sits alongside the original rather than replacing it. This is a little HTTP/1.1 library written from scratch in Python, -heavily inspired by `hyper-h2 `_. +heavily inspired by [hyper-h2](https://hyper-h2.readthedocs.io/). -It's a "bring-your-own-I/O" library; h11 contains no IO code -whatsoever. This means you can hook h11 up to your favorite network +It's a "bring-your-own-I/O" library; h11-mypyc contains no IO code +whatsoever. This means you can hook h11-mypyc up to your favorite network API, and that could be anything you want: synchronous, threaded, -asynchronous, or your own implementation of `RFC 6214 -`_ -- h11 won't judge you. -(Compare this to the current state of the art, where every time a `new -network API `_ comes along then someone +asynchronous, or your own implementation of +[RFC 6214](https://tools.ietf.org/html/rfc6214) -- h11-mypyc won't judge you. +(Compare this to the current state of the art, where every time a +[new network API](https://trio.readthedocs.io/) comes along then someone gets to start over reimplementing the entire HTTP protocol from -scratch.) Cory Benfield made an `excellent blog post describing the -benefits of this approach -`_, or if you like video -then here's his `PyCon 2016 talk on the same theme -`_. +scratch.) Cory Benfield made an +[excellent blog post describing the benefits of this approach](https://lukasa.co.uk/2015/10/The_New_Hyper/), +or if you like video then here's his +[PyCon 2016 talk on the same theme](https://www.youtube.com/watch?v=7cC3_jGwl_U). -This also means that h11 is not immediately useful out of the box: +This also means that h11-mypyc is not immediately useful out of the box: it's a toolkit for building programs that speak HTTP, not something -that could directly replace ``requests`` or ``twisted.web`` or -whatever. But h11 makes it much easier to implement something like -``requests`` or ``twisted.web``. +that could directly replace `requests` or `twisted.web` or +whatever. But h11-mypyc makes it much easier to implement something like +`requests` or `twisted.web`. -At a high level, working with h11 goes like this: +At a high level, working with h11-mypyc goes like this: -1) First, create an ``h11.Connection`` object to track the state of a +1. First, create an `h11_mypyc.Connection` object to track the state of a single HTTP/1.1 connection. -2) When you read data off the network, pass it to - ``conn.receive_data(...)``; you'll get back a list of objects +2. When you read data off the network, pass it to + `conn.receive_data(...)`; you'll get back a list of objects representing high-level HTTP "events". -3) When you want to send a high-level HTTP event, create the - corresponding "event" object and pass it to ``conn.send(...)``; +3. When you want to send a high-level HTTP event, create the + corresponding "event" object and pass it to `conn.send(...)`; this will give you back some bytes that you can then push out through the network. For example, a client might instantiate and then send a -``h11.Request`` object, then zero or more ``h11.Data`` objects for the +`h11_mypyc.Request` object, then zero or more `h11_mypyc.Data` objects for the request body (e.g., if this is a POST), and then a -``h11.EndOfMessage`` to indicate the end of the message. Then the -server would then send back a ``h11.Response``, some ``h11.Data``, and -its own ``h11.EndOfMessage``. If either side violates the protocol, -you'll get a ``h11.ProtocolError`` exception. +`h11_mypyc.EndOfMessage` to indicate the end of the message. Then the +server would then send back a `h11_mypyc.Response`, some `h11_mypyc.Data`, and +its own `h11_mypyc.EndOfMessage`. If either side violates the protocol, +you'll get a `h11_mypyc.ProtocolError` exception. -h11 is suitable for implementing both servers and clients, and has a +h11-mypyc is suitable for implementing both servers and clients, and has a pleasantly symmetric API: the events you send as a client are exactly the ones that you receive as a server and vice-versa. -`Here's an example of a tiny HTTP client -`_ +[Here's an example of a tiny HTTP client](https://github.com/danfimov/h11/blob/master/examples/basic-client.py) -It also has `a fine manual `_. +It also has [a fine manual](https://danfimov.github.io/h11/). -FAQ ---- +## FAQ *Whyyyyy?* -I wanted to play with HTTP in `Curio -`__ and `Trio -`__, which at the time didn't have any +I wanted to play with HTTP in +[Curio](https://curio.readthedocs.io/en/latest/tutorial.html) and +[Trio](https://trio.readthedocs.io), which at the time didn't have any HTTP libraries. So I thought, no big deal, Python has, like, a dozen different implementations of HTTP, surely I can find one that's reusable. I didn't find one, but I did find Cory's call-to-arms @@ -92,9 +81,9 @@ to talk to you before making any incompatible changes! *What are the features/limitations?* Roughly speaking, it's trying to be a robust, complete, and non-hacky -implementation of the first "chapter" of the HTTP/1.1 spec: `RFC 7230: -HTTP/1.1 Message Syntax and Routing -`_. That is, it mostly focuses on +implementation of the first "chapter" of the HTTP/1.1 spec: +[RFC 7230: HTTP/1.1 Message Syntax and Routing](https://tools.ietf.org/html/rfc7230). +That is, it mostly focuses on implementing HTTP at the level of taking bytes on and off the wire, and the headers related to that, and tries to be anal about spec conformance. It doesn't know about higher-level concerns like URL @@ -104,29 +93,30 @@ cross-version differences in keep-alive handling, and the "obsolete line folding" rule, so you can focus your energies on the hard / interesting parts for your application, and it tries to support the full specification in the sense that any useful HTTP/1.1 conformant -application should be able to use h11. +application should be able to use h11-mypyc. -It's pure Python, and has no dependencies outside of the standard -library. +It is published both as a mypyc-compiled wheel and as a pure-Python one; pip +picks the compiled build where a wheel exists for your interpreter and falls +back to the pure one everywhere else, PyPy included. It has a test suite with 100.0% coverage for both statements and branches. -Currently it supports Python 3 (testing on 3.8-3.12) and PyPy 3. +Currently it supports Python 3.10+ and PyPy 3. The last Python 2-compatible version was h11 0.11.x. -(Originally it had a Cython wrapper for `http-parser -`_ and a beautiful nested state -machine implemented with ``yield from`` to postprocess the output. But +(Originally it had a Cython wrapper for +[http-parser](https://github.com/nodejs/http-parser) and a beautiful nested state +machine implemented with `yield from` to postprocess the output. But I had to take these out -- the new *parser* needs fewer lines-of-code than the old *parser wrapper*, is written in pure Python, uses no exotic language syntax, and has more features. It's sad, really; that old state machine was really slick. I just need a few sentences here to mourn that.) -I don't know how fast it is. I haven't benchmarked or profiled it yet, -so it's probably got a few pointless hot spots, and I've been trying -to err on the side of simplicity and robustness instead of -micro-optimization. But at the architectural level I tried hard to +It is benchmarked and profiled: `make bench-compare` measures the compiled +build against the interpreted one, and `make bench-py` runs the per-path suite +in `tests/test_benchmarks.py`. Upstream erred on the side of simplicity and +robustness over micro-optimization, and at the architectural level tried hard to avoid fundamentally bad decisions, e.g., I believe that all the parsing algorithms remain linear-time even in the face of pathological input like slowloris, and there are no byte-by-byte loops. (I also @@ -148,12 +138,12 @@ details. *How do I try it?* -.. code-block:: sh - - $ pip install h11 - $ git clone git@github.com:python-hyper/h11 - $ cd h11/examples - $ python basic-client.py +```sh +$ pip install h11-mypyc +$ git clone git@github.com:danfimov/h11 +$ cd h11/examples +$ python basic-client.py +``` and go from there. @@ -163,6 +153,6 @@ MIT *Code of conduct?* -Contributors are requested to follow our `code of conduct -`_ in -all project spaces. +Contributors are requested to follow our +[code of conduct](https://github.com/danfimov/h11/blob/master/CODE_OF_CONDUCT.md) +in all project spaces. diff --git a/bench/asv.conf.json b/bench/asv.conf.json index 0a07c42..0931817 100644 --- a/bench/asv.conf.json +++ b/bench/asv.conf.json @@ -4,10 +4,10 @@ "version": 1, // The name of the project being benchmarked - "project": "h11", + "project": "h11-mypyc", // The project's homepage - "project_url": "https://h11.readthedocs.io/", + "project_url": "https://danfimov.github.io/h11/", // The URL or local path of the source code repository for the // project being benchmarked diff --git a/bench/benchmarks/benchmarks.py b/bench/benchmarks/benchmarks.py index 73d078e..887be9a 100644 --- a/bench/benchmarks/benchmarks.py +++ b/bench/benchmarks/benchmarks.py @@ -1,7 +1,7 @@ # Write the benchmarking functions here. # See "Writing benchmarks" in the asv docs for more information. -import h11 +import h11_mypyc as h11 # Basic ASV benchmark of core functionality diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index afba31b..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,233 +0,0 @@ -# Makefile for Sphinx documentation -# - -# So the build will be able to find the h11 sources -export PYTHONPATH := $(CURDIR)/.. - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) - $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/h11.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/h11.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/h11" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/h11" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." diff --git a/docs/includes/client-states.mmd b/docs/includes/client-states.mmd new file mode 100644 index 0000000..31b99a7 --- /dev/null +++ b/docs/includes/client-states.mmd @@ -0,0 +1,33 @@ +--- +config: + flowchart: + useMaxWidth: false + nodeSpacing: 55 + rankSpacing: 70 +--- +flowchart TD + IDLE(["IDLE
start state"]) + ERROR(["ERROR"]) + CLOSED -->|"ConnectionClosed"| CLOSED + DONE -->|"ConnectionClosed"| CLOSED + DONE -->|"start_next_cycle()"| IDLE + DONE -->|"switch proposal
pending
"| MIGHT_SWITCH_PROTOCOL + DONE -->|"keep-alive
is disabled
"| MUST_CLOSE + DONE -->|"peer in
CLOSED"| MUST_CLOSE + DONE -->|"peer in
ERROR"| MUST_CLOSE + IDLE -->|"ConnectionClosed"| CLOSED + IDLE -->|"peer in
CLOSED"| MUST_CLOSE + IDLE -->|"Request"| SEND_BODY + MIGHT_SWITCH_PROTOCOL -->|"no switch proposal
pending
"| DONE + MIGHT_SWITCH_PROTOCOL -->|"peer in
SWITCHED_PROTOCOL"| SWITCHED_PROTOCOL + MUST_CLOSE -->|"ConnectionClosed"| CLOSED + SEND_BODY -->|"EndOfMessage"| DONE + SEND_BODY -->|"Data"| SEND_BODY + linkStyle 2 stroke:#ab47bc,color:#ab47bc,stroke-width:2px + linkStyle 3 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 4 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 5 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 6 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 8 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 10 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 11 stroke:#43a047,color:#43a047,stroke-width:2px diff --git a/docs/source/_examples/myclient.py b/docs/includes/myclient.py similarity index 87% rename from docs/source/_examples/myclient.py rename to docs/includes/myclient.py index 1baaf64..f22e8bb 100644 --- a/docs/source/_examples/myclient.py +++ b/docs/includes/myclient.py @@ -1,5 +1,8 @@ -import socket, ssl -import h11 +import socket +import ssl + +import h11_mypyc + class MyHttpClient: def __init__(self, host, port): @@ -7,7 +10,7 @@ def __init__(self, host, port): if port == 443: ctx = ssl.create_default_context() self.sock = ctx.wrap_socket(self.sock, server_hostname=host) - self.conn = h11.Connection(our_role=h11.CLIENT) + self.conn = h11_mypyc.Connection(our_role=h11_mypyc.CLIENT) def send(self, *events): for event in events: @@ -26,7 +29,7 @@ def next_event(self, max_bytes_per_recv=200): # return that. Otherwise, read some data, add it to the internal # buffer, and then try again. event = self.conn.next_event() - if event is h11.NEED_DATA: + if event is h11_mypyc.NEED_DATA: self.conn.receive_data(self.sock.recv(max_bytes_per_recv)) continue return event diff --git a/docs/includes/server-states.mmd b/docs/includes/server-states.mmd new file mode 100644 index 0000000..94a92df --- /dev/null +++ b/docs/includes/server-states.mmd @@ -0,0 +1,32 @@ +--- +config: + flowchart: + useMaxWidth: false + nodeSpacing: 55 + rankSpacing: 70 +--- +flowchart TD + IDLE(["IDLE
start state"]) + ERROR(["ERROR"]) + CLOSED -->|"ConnectionClosed"| CLOSED + DONE -->|"ConnectionClosed"| CLOSED + DONE -->|"start_next_cycle()"| IDLE + DONE -->|"keep-alive
is disabled
"| MUST_CLOSE + DONE -->|"peer in
CLOSED"| MUST_CLOSE + DONE -->|"peer in
ERROR"| MUST_CLOSE + IDLE -->|"ConnectionClosed"| CLOSED + IDLE -->|"peer in
CLOSED"| MUST_CLOSE + IDLE -->|"Response"| SEND_BODY + IDLE -->|"client makes Request"| SEND_RESPONSE + MUST_CLOSE -->|"ConnectionClosed"| CLOSED + SEND_BODY -->|"EndOfMessage"| DONE + SEND_BODY -->|"Data"| SEND_BODY + SEND_RESPONSE -->|"Response"| SEND_BODY + SEND_RESPONSE -->|"InformationalResponse"| SEND_RESPONSE + SEND_RESPONSE -->|"101 Switching Protocols"| SWITCHED_PROTOCOL + SEND_RESPONSE -->|"CONNECT accepted"| SWITCHED_PROTOCOL + linkStyle 2 stroke:#ab47bc,color:#ab47bc,stroke-width:2px + linkStyle 3 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 4 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 5 stroke:#43a047,color:#43a047,stroke-width:2px + linkStyle 7 stroke:#43a047,color:#43a047,stroke-width:2px diff --git a/docs/includes/special-states.mmd b/docs/includes/special-states.mmd new file mode 100644 index 0000000..ea3fe29 --- /dev/null +++ b/docs/includes/special-states.mmd @@ -0,0 +1,20 @@ +--- +config: + flowchart: + useMaxWidth: false + nodeSpacing: 55 + rankSpacing: 70 +--- +flowchart TD + kaT(["keep-alive is enabled
initial state"]) + kaF(["keep-alive is disabled"]) + upF(["No potential Upgrade: pending
initial state"]) + upT(["Potential Upgrade: pending"]) + coF(["No potential CONNECT pending
initial state"]) + coT(["Potential CONNECT pending"]) + coF -->|"Request with CONNECT"| coT + coT -->|"Response without 2xx status"| coF + kaF -->|"Request/response with
HTTP/1.0 or Connection: close
"| kaF + kaT -->|"Request/response with
HTTP/1.0 or Connection: close
"| kaF + upF -->|"Request with Upgrade:"| upT + upT -->|"Response"| upF diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 1c6aca5..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -mistune -jsonschema -ipython -sphinx<4 -jinja2<3 -markupsafe<2 diff --git a/docs/source/_static/closelabel.png b/docs/source/_static/closelabel.png deleted file mode 100644 index c339e59..0000000 Binary files a/docs/source/_static/closelabel.png and /dev/null differ diff --git a/docs/source/_static/facebox.css b/docs/source/_static/facebox.css deleted file mode 100644 index 3f33b9f..0000000 --- a/docs/source/_static/facebox.css +++ /dev/null @@ -1,80 +0,0 @@ -#facebox { - position: absolute; - top: 0; - left: 0; - z-index: 100; - text-align: left; -} - - -#facebox .popup{ - position:relative; - border:3px solid rgba(0,0,0,0); - -webkit-border-radius:5px; - -moz-border-radius:5px; - border-radius:5px; - -webkit-box-shadow:0 0 18px rgba(0,0,0,0.4); - -moz-box-shadow:0 0 18px rgba(0,0,0,0.4); - box-shadow:0 0 18px rgba(0,0,0,0.4); -} - -#facebox .content { - display:table; - width: 370px; - padding: 10px; - background: #fff; - -webkit-border-radius:4px; - -moz-border-radius:4px; - border-radius:4px; -} - -#facebox .content > p:first-child{ - margin-top:0; -} -#facebox .content > p:last-child{ - margin-bottom:0; -} - -#facebox .close{ - position:absolute; - top:5px; - right:5px; - padding:2px; - background:#fff; -} -#facebox .close img{ - opacity:0.3; -} -#facebox .close:hover img{ - opacity:1.0; -} - -#facebox .loading { - text-align: center; -} - -#facebox .image { - text-align: center; -} - -#facebox img { - border: 0; - margin: 0; -} - -#facebox_overlay { - position: fixed; - top: 0px; - left: 0px; - height:100%; - width:100%; -} - -.facebox_hide { - z-index:-100; -} - -.facebox_overlayBG { - background-color: #000; - z-index: 99; -} \ No newline at end of file diff --git a/docs/source/_static/facebox.js b/docs/source/_static/facebox.js deleted file mode 100644 index b7568e5..0000000 --- a/docs/source/_static/facebox.js +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Facebox (for jQuery) - * version: 1.2 (05/05/2008) - * @requires jQuery v1.2 or later - * - * Examples at http://famspam.com/facebox/ - * - * Licensed under the MIT: - * http://www.opensource.org/licenses/mit-license.php - * - * Copyright 2007, 2008 Chris Wanstrath [ chris@ozmm.org ] - * - * Usage: - * - * jQuery(document).ready(function() { - * jQuery('a[rel*=facebox]').facebox() - * }) - * - * Terms - * Loads the #terms div in the box - * - * Terms - * Loads the terms.html page in the box - * - * Terms - * Loads the terms.png image in the box - * - * - * You can also use it programmatically: - * - * jQuery.facebox('some html') - * jQuery.facebox('some html', 'my-groovy-style') - * - * The above will open a facebox with "some html" as the content. - * - * jQuery.facebox(function($) { - * $.get('blah.html', function(data) { $.facebox(data) }) - * }) - * - * The above will show a loading screen before the passed function is called, - * allowing for a better ajaxy experience. - * - * The facebox function can also display an ajax page, an image, or the contents of a div: - * - * jQuery.facebox({ ajax: 'remote.html' }) - * jQuery.facebox({ ajax: 'remote.html' }, 'my-groovy-style') - * jQuery.facebox({ image: 'stairs.jpg' }) - * jQuery.facebox({ image: 'stairs.jpg' }, 'my-groovy-style') - * jQuery.facebox({ div: '#box' }) - * jQuery.facebox({ div: '#box' }, 'my-groovy-style') - * - * Want to close the facebox? Trigger the 'close.facebox' document event: - * - * jQuery(document).trigger('close.facebox') - * - * Facebox also has a bunch of other hooks: - * - * loading.facebox - * beforeReveal.facebox - * reveal.facebox (aliased as 'afterReveal.facebox') - * init.facebox - * afterClose.facebox - * - * Simply bind a function to any of these hooks: - * - * $(document).bind('reveal.facebox', function() { ...stuff to do after the facebox and contents are revealed... }) - * - */ -(function($) { - $.facebox = function(data, klass) { - $.facebox.loading() - - if (data.ajax) fillFaceboxFromAjax(data.ajax, klass) - else if (data.image) fillFaceboxFromImage(data.image, klass) - else if (data.div) fillFaceboxFromHref(data.div, klass) - else if ($.isFunction(data)) data.call($) - else $.facebox.reveal(data, klass) - } - - /* - * Public, $.facebox methods - */ - - $.extend($.facebox, { - settings: { - opacity : 0.2, - overlay : true, - /* I don't know why absolute paths don't work. If you try to use facebox - * outside of the examples folder these images won't show up. - */ - loadingImage : '_static/loading.gif', - closeImage : '_static/closelabel.png', - imageTypes : [ 'png', 'jpg', 'jpeg', 'gif' ], - faceboxHtml : '\ - ' - }, - - loading: function() { - init() - if ($('#facebox .loading').length == 1) return true - showOverlay() - - $('#facebox .content').empty() - $('#facebox .body').children().hide().end(). - append('
') - - $('#facebox').css({ - top: getPageScroll()[1] + (getPageHeight() / 10), - left: $(window).width() / 2 - 205 - }).show() - - $(document).bind('keydown.facebox', function(e) { - if (e.keyCode == 27) $.facebox.close() - return true - }) - $(document).trigger('loading.facebox') - }, - - reveal: function(data, klass) { - $(document).trigger('beforeReveal.facebox') - if (klass) $('#facebox .content').addClass(klass) - $('#facebox .content').append(data) - $('#facebox .loading').remove() - $('#facebox .body').children().fadeIn('normal') - $('#facebox').css('left', $(window).width() / 2 - ($('#facebox .popup').width() / 2)) - $(document).trigger('reveal.facebox').trigger('afterReveal.facebox') - }, - - close: function() { - $(document).trigger('close.facebox') - return false - } - }) - - /* - * Public, $.fn methods - */ - - $.fn.facebox = function(settings) { - if ($(this).length == 0) return - - init(settings) - - function clickHandler() { - $.facebox.loading(true) - - // support for rel="facebox.inline_popup" syntax, to add a class - // also supports deprecated "facebox[.inline_popup]" syntax - var klass = this.rel.match(/facebox\[?\.(\w+)\]?/) - if (klass) klass = klass[1] - - fillFaceboxFromHref(this.href, klass) - return false - } - - return this.bind('click.facebox', clickHandler) - } - - /* - * Private methods - */ - - // called one time to setup facebox on this page - function init(settings) { - if ($.facebox.settings.inited) return true - else $.facebox.settings.inited = true - - $(document).trigger('init.facebox') - makeCompatible() - - var imageTypes = $.facebox.settings.imageTypes.join('|') - $.facebox.settings.imageTypesRegexp = new RegExp('\.(' + imageTypes + ')$', 'i') - - if (settings) $.extend($.facebox.settings, settings) - $('body').append($.facebox.settings.faceboxHtml) - - var preload = [ new Image(), new Image() ] - preload[0].src = $.facebox.settings.closeImage - preload[1].src = $.facebox.settings.loadingImage - - $('#facebox').find('.b:first, .bl').each(function() { - preload.push(new Image()) - preload.slice(-1).src = $(this).css('background-image').replace(/url\((.+)\)/, '$1') - }) - - $('#facebox .close').click($.facebox.close) - $('#facebox .close_image').attr('src', $.facebox.settings.closeImage) - } - - // getPageScroll() by quirksmode.com - function getPageScroll() { - var xScroll, yScroll; - if (self.pageYOffset) { - yScroll = self.pageYOffset; - xScroll = self.pageXOffset; - } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict - yScroll = document.documentElement.scrollTop; - xScroll = document.documentElement.scrollLeft; - } else if (document.body) {// all other Explorers - yScroll = document.body.scrollTop; - xScroll = document.body.scrollLeft; - } - return new Array(xScroll,yScroll) - } - - // Adapted from getPageSize() by quirksmode.com - function getPageHeight() { - var windowHeight - if (self.innerHeight) { // all except Explorer - windowHeight = self.innerHeight; - } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode - windowHeight = document.documentElement.clientHeight; - } else if (document.body) { // other Explorers - windowHeight = document.body.clientHeight; - } - return windowHeight - } - - // Backwards compatibility - function makeCompatible() { - var $s = $.facebox.settings - - $s.loadingImage = $s.loading_image || $s.loadingImage - $s.closeImage = $s.close_image || $s.closeImage - $s.imageTypes = $s.image_types || $s.imageTypes - $s.faceboxHtml = $s.facebox_html || $s.faceboxHtml - } - - // Figures out what you want to display and displays it - // formats are: - // div: #id - // image: blah.extension - // ajax: anything else - function fillFaceboxFromHref(href, klass) { - // div - if (href.match(/#/)) { - var url = window.location.href.split('#')[0] - var target = href.replace(url,'') - if (target == '#') return - $.facebox.reveal($(target).html(), klass) - - // image - } else if (href.match($.facebox.settings.imageTypesRegexp)) { - fillFaceboxFromImage(href, klass) - // ajax - } else { - fillFaceboxFromAjax(href, klass) - } - } - - function fillFaceboxFromImage(href, klass) { - var image = new Image() - image.onload = function() { - $.facebox.reveal('
', klass) - } - image.src = href - } - - function fillFaceboxFromAjax(href, klass) { - $.get(href, function(data) { $.facebox.reveal(data, klass) }) - } - - function skipOverlay() { - return $.facebox.settings.overlay == false || $.facebox.settings.opacity === null - } - - function showOverlay() { - if (skipOverlay()) return - - if ($('#facebox_overlay').length == 0) - $("body").append('
') - - $('#facebox_overlay').hide().addClass("facebox_overlayBG") - .css('opacity', $.facebox.settings.opacity) - .click(function() { $(document).trigger('close.facebox') }) - .fadeIn(200) - return false - } - - function hideOverlay() { - if (skipOverlay()) return - - $('#facebox_overlay').fadeOut(200, function(){ - $("#facebox_overlay").removeClass("facebox_overlayBG") - $("#facebox_overlay").addClass("facebox_hide") - $("#facebox_overlay").remove() - }) - - return false - } - - /* - * Bindings - */ - - $(document).bind('close.facebox', function() { - $(document).unbind('keydown.facebox') - $('#facebox').fadeOut(function() { - $('#facebox .content').removeClass().addClass('content') - $('#facebox .loading').remove() - $(document).trigger('afterClose.facebox') - }) - hideOverlay() - }) - -})(jQuery); diff --git a/docs/source/_static/loading.gif b/docs/source/_static/loading.gif deleted file mode 100755 index f864d5f..0000000 Binary files a/docs/source/_static/loading.gif and /dev/null differ diff --git a/docs/source/_static/show-code.js b/docs/source/_static/show-code.js deleted file mode 100644 index 6eb5beb..0000000 --- a/docs/source/_static/show-code.js +++ /dev/null @@ -1,75 +0,0 @@ -// Stolen from statsmodels and fixed up -// Here's what statsmodels' LICENSE.txt says: -// -// Copyright (C) 2006, Jonathan E. Taylor -// All rights reserved. -// -// Copyright (c) 2006-2008 Scipy Developers. -// All rights reserved. -// -// Copyright (c) 2009-2012 Statsmodels Developers. -// All rights reserved. -// -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// a. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// b. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// c. Neither the name of Statsmodels nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL STATSMODELS OR CONTRIBUTORS BE LIABLE FOR -// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -// DAMAGE. - - -function htmlescape(text){ - return (text.replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'")) -} - -function scrapeText(codebox){ - /// Returns input lines cleaned of prompt1 and prompt2 - var lines = codebox.split('\n'); - var newlines = new Array(); - $.each(lines, function() { - if (this.match(/^In \[\d+]: /)){ - newlines.push(this.replace(/^(\s)*In \[\d+]: /,"")); - } - else if (this.match(/^(\s)*\.+:/)){ - newlines.push(this.replace(/^(\s)*\.+: /,"")); - } - - } - ); - return newlines.join('\\n'); -} - -$(document).ready( - function() { - // grab all code boxes - var ipythoncode = $(".highlight-ipython"); - $.each(ipythoncode, function() { - var code = scrapeText($(this).text()); - // give them a facebox pop-up with plain text code - $(this).append('View Code'); - $(this,"textarea").select(); - }); -}); diff --git a/docs/source/api.rst b/docs/source/api.rst deleted file mode 100644 index 4b6798e..0000000 --- a/docs/source/api.rst +++ /dev/null @@ -1,1098 +0,0 @@ -.. _API-documentation: - -API documentation -================= - -.. module:: h11 - -.. contents:: - -h11 has a fairly small public API, with all public symbols available -directly at the top level: - -.. ipython:: - - In [2]: import h11 - - @verbatim - In [3]: h11. - h11.CLIENT h11.MUST_CLOSE - h11.CLOSED h11.NEED_DATA - h11.Connection h11.PAUSED - h11.ConnectionClosed h11.PRODUCT_ID - h11.Data h11.ProtocolError - h11.DONE h11.RemoteProtocolError - h11.EndOfMessage h11.Request - h11.ERROR h11.Response - h11.IDLE h11.SEND_BODY - h11.InformationalResponse h11.SEND_RESPONSE - h11.LocalProtocolError h11.SERVER - h11.MIGHT_SWITCH_PROTOCOL h11.SWITCHED_PROTOCOL - -These symbols fall into three main categories: event classes, special -constants used to track different connection states, and the -:class:`Connection` class itself. We'll describe them in that order. - -.. _events: - -Events ------- - -*Events* are the core of h11: the whole point of h11 is to let you -think about HTTP transactions as being a series of events sent back -and forth between a client and a server, instead of thinking in terms -of bytes. - -All events behave in essentially similar ways. Let's take -:class:`Request` as an example. Like all events, this is a "final" -class -- you cannot subclass it. And like all events, it has several -fields. For :class:`Request`, there are four of them: -:attr:`~Request.method`, :attr:`~Request.target`, -:attr:`~Request.headers`, and -:attr:`~Request.http_version`. :attr:`~Request.http_version` -defaults to ``b"1.1"``; the rest have no default, so to create a -:class:`Request` you have to specify their values: - -.. ipython:: python - - req = h11.Request(method="GET", - target="/", - headers=[("Host", "example.com")]) - -Event constructors accept only keyword arguments, not positional arguments. - -Events have a useful repr: - -.. ipython:: python - - req - -And their fields are available as regular attributes: - -.. ipython:: python - - req.method - req.target - req.headers - req.http_version - -Notice that these attributes have been normalized to byte-strings. In -general, events normalize and validate their fields when they're -constructed. Some of these normalizations and checks are specific to a -particular event -- for example, :class:`Request` enforces RFC 7230's -requirement that HTTP/1.1 requests must always contain a ``"Host"`` -header: - -.. ipython:: python - - # HTTP/1.0 requests don't require a Host: header - h11.Request(method="GET", target="/", headers=[], http_version="1.0") - -.. ipython:: python - :okexcept: - - # But HTTP/1.1 requests do - h11.Request(method="GET", target="/", headers=[]) - -This helps protect you from accidentally violating the protocol, and -also helps protect you from remote peers who attempt to violate the -protocol. - -A few of these normalization rules are standard across multiple -events, so we document them here: - -.. _headers-format: - -:attr:`headers`: In h11, headers are represented internally as a list -of (*name*, *value*) pairs, where *name* and *value* are both -byte-strings, *name* is always lowercase, and *name* and *value* are -both guaranteed not to have any leading or trailing whitespace. When -constructing an event, we accept any iterable of pairs like this, and -will automatically convert native strings containing ascii or -:term:`bytes-like object`\s to byte-strings and convert names to -lowercase: - -.. ipython:: python - - original_headers = [("HOST", bytearray(b"Example.Com"))] - req = h11.Request(method="GET", target="/", headers=original_headers) - original_headers - req.headers - -If any names are detected with leading or trailing whitespace, then -this is an error ("in the past, differences in the handling of such -whitespace have led to security vulnerabilities" -- `RFC 7230 -`_). We also check -for certain other protocol violations, e.g. it's always illegal to -have a newline inside a header value, and ``Content-Length: hello`` is -an error because `Content-Length` should always be an integer. We may -add additional checks in the future. - -While we make sure to expose header names as lowercased bytes, we also -preserve the original header casing that is used. Compliant HTTP -agents should always treat headers in a case insensitive manner, but -this may not always be the case. When sending bytes over the wire we -send headers preserving whatever original header casing was used. - -It is possible to access the headers in their raw original casing, -which may be useful for some user output or debugging purposes. - -.. ipython:: python - - original_headers = [("Host", "example.com")] - req = h11.Request(method="GET", target="/", headers=original_headers) - req.headers.raw_items() - -.. _http_version-format: - -It's not just headers we normalize to being byte-strings: the same -type-conversion logic is also applied to the :attr:`Request.method` -and :attr:`Request.target` field, and -- for consistency -- all -:attr:`http_version` fields. In particular, we always represent HTTP -version numbers as byte-strings like ``b"1.1"``. :term:`Bytes-like -object`\s and native strings will be automatically converted to byte -strings. Note that the HTTP standard `specifically guarantees -`_ that all HTTP -version numbers will consist of exactly two digits separated by a dot, -so comparisons like ``req.http_version < b"1.1"`` are safe and valid. - -When manually constructing an event, you generally shouldn't specify -:attr:`http_version`, because it defaults to ``b"1.1"``, and if you -attempt to override this to some other value then -:meth:`Connection.send` will reject your event -- h11 only speaks -HTTP/1.1. But it does understand other versions of HTTP, so you might -receive events with other ``http_version`` values from remote peers. - -Here's the complete set of events supported by h11: - -.. autoclass:: Request - -.. autoclass:: InformationalResponse - -.. autoclass:: Response - -.. autoclass:: Data - -.. autoclass:: EndOfMessage - -.. autoclass:: ConnectionClosed - - -.. _state-machine: - -The state machine ------------------ - -Now that you know what the different events are, the next question is: -what can you do with them? - -A basic HTTP request/response cycle looks like this: - -* The client sends: - - * one :class:`Request` event with request metadata and headers, - * zero or more :class:`Data` events with the request body (if any), - * and an :class:`EndOfMessage` event. - -* And then the server replies with: - - * zero or more :class:`InformationalResponse` events, - * one :class:`Response` event, - * zero or more :class:`Data` events with the response body (if any), - * and a :class:`EndOfMessage` event. - -And once that's finished, both sides either close the connection, or -they go back to the top and re-use it for another request/response -cycle. - -To coordinate this interaction, the h11 :class:`Connection` object -maintains several state machines: one that tracks what the client is -doing, one that tracks what the server is doing, and a few more tiny -ones to track whether :ref:`keep-alive ` is -enabled and whether the client has proposed to :ref:`switch protocols -`. h11 always keeps track of all of these state -machines, regardless of whether it's currently playing the client or -server role. - -The state machines look like this (click on each to expand): - -.. ipython:: python - :suppress: - - import sys - import subprocess - subprocess.check_call([sys.executable, - sys._h11_hack_docs_source_path - + "/make-state-diagrams.py"]) - -.. |client-image| image:: _static/CLIENT.svg - :target: _static/CLIENT.svg - :width: 100% - :align: top - -.. |server-image| image:: _static/SERVER.svg - :target: _static/SERVER.svg - :width: 100% - :align: top - -.. |special-image| image:: _static/special-states.svg - :target: _static/special-states.svg - :width: 100% - -+----------------+----------------+ -| |client-image| | |server-image| | -+----------------+----------------+ -| |special-image| | -+---------------------------------+ - -If you squint at the first two diagrams, you can see the client's IDLE --> SEND_BODY -> DONE path and the server's IDLE -> SEND_RESPONSE -> -SEND_BODY -> DONE path, which encode the basic sequence of events we -described above. But there's a fair amount of other stuff going on -here as well. - -The first thing you should notice is the different colors. These -correspond to the different ways that our state machines can change -state. - -* Dark blue arcs are *event-triggered transitions*: if we're in state - A, and this event happens, when we switch to state B. For the client - machine, these transitions always happen when the client *sends* an - event. For the server machine, most of them involve the server - sending an event, except that the server also goes from IDLE -> - SEND_RESPONSE when the client sends a :class:`Request`. - -* Green arcs are *state-triggered transitions*: these are somewhat - unusual, and are used to couple together the different state - machines -- if, at any moment, one machine is in state A and another - machine is in state B, then the first machine immediately - transitions to state C. For example, if the CLIENT machine is in - state DONE, and the SERVER machine is in the CLOSED state, then the - CLIENT machine transitions to MUST_CLOSE. And the same thing happens - if the CLIENT machine is in the state DONE and the keep-alive - machine is in the state disabled. - -* There are also two purple arcs labeled - :meth:`~Connection.start_next_cycle`: these correspond to an explicit - method call documented below. - -Here's why we have all the stuff in those diagrams above, beyond -what's needed to handle the basic request/response cycle: - -* Server sending a :class:`Response` directly from :data:`IDLE`: This - is used for error responses, when the client's request never arrived - (e.g. 408 Request Timed Out) or was unparseable gibberish (400 Bad - Request) and thus didn't register with our state machine as a real - :class:`Request`. - -* The transitions involving :data:`MUST_CLOSE` and :data:`CLOSE`: - keep-alive and shutdown handling; see - :ref:`keepalive-and-pipelining` and :ref:`closing`. - -* The transitions involving :data:`MIGHT_SWITCH_PROTOCOL` and - :data:`SWITCHED_PROTOCOL`: See :ref:`switching-protocols`. - -* That weird :data:`ERROR` state hanging out all lonely on the bottom: - to avoid cluttering the diagram, we don't draw any arcs coming into - this node, but that doesn't mean it can't be entered. In fact, it - can be entered from any state: if any exception occurs while trying - to send/receive data, then the corresponding machine will transition - directly to this state. Once there, though, it can never leave -- - that part of the diagram is accurate. See :ref:`error-handling`. - -And finally, note that in these diagrams, all the labels that are in -*italics* are informal English descriptions of things that happen in -the code, while the labels in upright text correspond to actual -objects in the public API. You've already seen the event objects like -:class:`Request` and :class:`Response`; there are also a set of opaque -sentinel values that you can use to track and query the client and -server's states. - - -Special constants ------------------ - -h11 exposes some special constants corresponding to the different -states in the client and server state machines described above. The -complete list is: - -.. data:: IDLE - SEND_RESPONSE - SEND_BODY - DONE - MUST_CLOSE - CLOSED - MIGHT_SWITCH_PROTOCOL - SWITCHED_PROTOCOL - ERROR - -For example, we can see that initially the client and server start in -state :data:`IDLE` / :data:`IDLE`: - -.. ipython:: python - - conn = h11.Connection(our_role=h11.CLIENT) - conn.states - -And then if the client sends a :class:`Request`, then the client -switches to state :data:`SEND_BODY`, while the server switches to -state :data:`SEND_RESPONSE`: - -.. ipython:: python - - conn.send(h11.Request(method="GET", target="/", headers=[("Host", "example.com")])); - conn.states - -And we can test these values directly using constants like :data:`SEND_BODY`: - -.. ipython:: python - - conn.states[h11.CLIENT] is h11.SEND_BODY - -This shows how the :class:`Connection` type tracks these state -machines and lets you query their current state. - -The above also showed the special constants that can be used to -indicate the two different roles that a peer can play in an HTTP -connection: - -.. data:: CLIENT - SERVER - -And finally, there are also two special constants that can be returned -from :meth:`Connection.next_event`: - -.. data:: NEED_DATA - PAUSED - -All of these behave the same, and their behavior is modeled after -:data:`None`: they're opaque singletons, their :meth:`__repr__` is -their name, and you compare them with ``is``. - -.. _sentinel-type-trickiness: - -Finally, h11's constants have a quirky feature that can sometimes be -useful: they are instances of themselves. - -.. ipython:: python - - type(h11.NEED_DATA) is h11.NEED_DATA - type(h11.PAUSED) is h11.PAUSED - -The main application of this is that when handling the return value -from :meth:`Connection.next_event`, which is sometimes an instance of -an event class and sometimes :data:`NEED_DATA` or :data:`PAUSED`, you -can always call ``type(event)`` to get something useful to dispatch -one, using e.g. a handler table, :func:`functools.singledispatch`, or -calling ``getattr(some_object, "handle_" + -type(event).__name__)``. Not that this kind of dispatch-based strategy -is always the best approach -- but the option is there if you want it. - - -The Connection object ---------------------- - -.. autoclass:: Connection - - .. automethod:: receive_data - .. automethod:: next_event - .. automethod:: send - .. automethod:: send_with_data_passthrough - .. automethod:: send_failed - - .. automethod:: start_next_cycle - - .. attribute:: our_role - - :data:`CLIENT` if this is a client; :data:`SERVER` if this is a server. - - .. attribute:: their_role - - :data:`SERVER` if this is a client; :data:`CLIENT` if this is a server. - - .. autoattribute:: states - .. autoattribute:: our_state - .. autoattribute:: their_state - - .. attribute:: their_http_version - - The version of HTTP that our peer claims to support. ``None`` if - we haven't yet received a request/response. - - This is preserved by :meth:`start_next_cycle`, so it can be - handy for a client making multiple requests on the same - connection: normally you don't know what version of HTTP the - server supports until after you do a request and get a response - -- so on an initial request you might have to assume the - worst. But on later requests on the same connection, the - information will be available here. - - .. attribute:: client_is_waiting_for_100_continue - - True if the client sent a request with the ``Expect: - 100-continue`` header, and is still waiting for a response - (i.e., the server has not sent a 100 Continue or any other kind - of response, and the client has not gone ahead and started - sending the body anyway). - - See `RFC 7231 section 5.1.1 - `_ for details. - - .. attribute:: they_are_waiting_for_100_continue - - True if :attr:`their_role` is :data:`CLIENT` and - :attr:`client_is_waiting_for_100_continue`. - - .. autoattribute:: trailing_data - - -.. _error-handling: - -Error handling --------------- - -Given the vagaries of networks and the folks on the other side of -them, it's extremely important to be prepared for errors. - -Most errors in h11 are signaled by raising one of -:exc:`ProtocolError`'s two concrete base classes, -:exc:`LocalProtocolError` and :exc:`RemoteProtocolError`: - -.. autoexception:: ProtocolError -.. autoexception:: LocalProtocolError -.. autoexception:: RemoteProtocolError - -There are four cases where these exceptions might be raised: - -* When trying to instantiate an event object - (:exc:`LocalProtocolError`): This indicates that something about - your event is invalid. Your event wasn't constructed, but there are - no other consequences -- feel free to try again. - -* When calling :meth:`Connection.start_next_cycle` - (:exc:`LocalProtocolError`): This indicates that the connection is - not ready to be re-used, because one or both of the peers are not in - the :data:`DONE` state. The :class:`Connection` object remains - usable, and you can try again later. - -* When calling :meth:`Connection.next_event` - (:exc:`RemoteProtocolError`): This indicates that the remote peer - has violated our protocol assumptions. This is unrecoverable -- we - don't know what they're doing and we cannot safely - proceed. :attr:`Connection.their_state` immediately becomes - :data:`ERROR`, and all further calls to - :meth:`~Connection.next_event` will also raise - :exc:`RemoteProtocolError`. :meth:`Connection.send` still works as - normal, so if you're implementing a server and this happens then you - have an opportunity to send back a 400 Bad Request response. But - aside from that, your only real option is to close your socket and - make a new connection. - -* When calling :meth:`Connection.send` or - :meth:`Connection.send_with_data_passthrough` - (:exc:`LocalProtocolError`): This indicates that *you* violated our - protocol assumptions. This is also unrecoverable -- h11 doesn't know - what you're doing, its internal state may be inconsistent, and we - cannot safely proceed. :attr:`Connection.our_state` immediately - becomes :data:`ERROR`, and all further calls to - :meth:`~Connection.send` will also raise - :exc:`LocalProtocolError`. The only thing you can reasonably due at - this point is to close your socket and make a new connection. - -So that's how h11 tells you about errors that it detects. In some -cases, it's also useful to be able to tell h11 about an error that you -detected. In particular, the :class:`Connection` object assumes that -after you call :meth:`Connection.send`, you actually send that data to -the remote peer. But sometimes, for one reason or another, this -doesn't actually happen. - -Here's a concrete example. Suppose you're using h11 to implement an -HTTP client that keeps a pool of connections so it can re-use them -when possible (see :ref:`keepalive-and-pipelining`). You take a -connection from the pool, and start to do a large upload... but then -for some reason this gets cancelled (maybe you have a GUI and a user -clicked "cancel"). This can cause h11's model of this connection to -diverge from reality: for example, h11 might think that you -successfully sent the full request, because you passed an -:class:`EndOfMessage` object to :meth:`Connection.send`, but in fact -you didn't, because you never sent the resulting bytes. And then – -here's the really tricky part! – if you're not careful, you might -think that it's OK to put this connection back into the connection -pool and re-use it, because h11 is telling you that a full -request/response cycle was completed. But this is wrong; in fact you -have to close this connection and open a new one. - -The solution is simple: call :meth:`Connection.send_failed`, and now -h11 knows that your send failed. In this case, -:attr:`Connection.our_state` immediately becomes :data:`ERROR`, just -like if you had tried to do something that violated the protocol. - - -.. _framing: - -Message body framing: ``Content-Length`` and all that ------------------------------------------------------ - -There are two different headers that HTTP/1.1 uses to indicate a -framing mechanism for request/response bodies: ``Content-Length`` and -``Transfer-Encoding``. Our general philosophy is that the way you tell -h11 what configuration you want to use is by setting the appropriate -headers in your request / response, and then h11 will both pass those -headers on to the peer and encode the body appropriately. - -Currently, the only supported ``Transfer-Encoding`` is ``chunked``. - -On requests, this means: - -* No ``Content-Length`` or ``Transfer-Encoding``: no body, equivalent - to ``Content-Length: 0``. - -* ``Content-Length: ...``: You're going to send exactly the specified - number of bytes. h11 will keep track and signal an error if your - :class:`EndOfMessage` doesn't happen at the right place. - -* ``Transfer-Encoding: chunked``: You're going to send a variable / - not yet known number of bytes. - - Note 1: only HTTP/1.1 servers are required to support - ``Transfer-Encoding: chunked``, and as a client you have to decide - whether to send this header before you get to see what protocol - version the server is using. - - Note 2: even though HTTP/1.1 servers are required to support - ``Transfer-Encoding: chunked``, this doesn't necessarily mean that - they actually do -- e.g., applications using Python's standard WSGI - API cannot accept chunked requests. - - Nonetheless, this is the only way to send request where you don't - know the size of the body ahead of time, so if that's the situation - you find yourself in then you might as well try it and hope. - -On responses, things are a bit more subtle. There are effectively two -cases: - -* ``Content-Length: ...``: You're going to send exactly the specified - number of bytes. h11 will keep track and signal an error if your - :class:`EndOfMessage` doesn't happen at the right place. - -* ``Transfer-Encoding: chunked``, *or*, neither framing header is - provided: These two cases are handled differently at the wire level, - but as far as the application is concerned they provide (almost) - exactly the same semantics: in either case, you'll send a variable / - not yet known number of bytes. The difference between them is that - ``Transfer-Encoding: chunked`` works better (compatible with - keep-alive, allows trailing headers, clearly distinguishes between - successful completion and network errors), but requires an HTTP/1.1 - client; for HTTP/1.0 clients the only option is the no-headers - approach where you have to close the socket to indicate completion. - - Since this is (almost) entirely a wire-level-encoding concern, h11 - abstracts it: when sending a response you can set either - ``Transfer-Encoding: chunked`` or leave off both framing headers, - and h11 will treat both cases identically: it will automatically - pick the best option given the client's advertised HTTP protocol - level. - - You need to watch out for this if you're using trailing headers - (i.e., a non-empty ``headers`` attribute on :class:`EndOfMessage`), - since trailing headers are only legal if we actually ended up using - ``Transfer-Encoding: chunked``. Trying to send a non-empty set of - trailing headers to a HTTP/1.0 client will raise a - :exc:`LocalProtocolError`. If this use case is important to you, check - :attr:`Connection.their_http_version` to confirm that the client - speaks HTTP/1.1 before you attempt to send any trailing headers. - - -.. _keepalive-and-pipelining: - -Re-using a connection: keep-alive and pipelining ------------------------------------------------- - -HTTP/1.1 allows a connection to be re-used for multiple -request/response cycles (also known as "keep-alive"). This can make -things faster by letting us skip the costly connection setup, but it -does create some complexities: we have to keep track of whether a -connection is reusable, and when there are multiple requests and -responses flowing through the same connection we need to be careful -not to get confused about which request goes with which response. - -h11 considers a connection to be reusable if, and only if, both -sides (a) speak HTTP/1.1 (HTTP/1.0 did have some complex and fragile -support for keep-alive bolted on, but h11 currently doesn't support -that -- possibly this will be added in the future), and (b) neither -side has explicitly disabled keep-alive by sending a ``Connection: -close`` header. - -If you plan to make only a single request or response and then close -the connection, you should manually set the ``Connection: close`` -header in your request/response. h11 will notice and update its state -appropriately. - -There are also some situations where you are required to send a -``Connection: close`` header, e.g. if you are a server talking to a -client that doesn't support keep-alive. You don't need to worry about -these cases -- h11 will automatically add this header when -necessary. Just worry about setting it when it's actually something -that you're actively choosing. - -If you want to re-use a connection, you have to wait until both the -request and the response have been completed, bringing both the client -and server to the :data:`DONE` state. Once this has happened, you can -explicitly call :meth:`Connection.start_next_cycle` to reset both -sides back to the :data:`IDLE` state. This makes sure that the client -and server remain synched up. - -If keep-alive is disabled for whatever reason -- someone set -``Connection: close``, lack of protocol support, one of the sides just -unilaterally closed the connection -- then the state machines will -skip past the :data:`DONE` state directly to the :data:`MUST_CLOSE` or -:data:`CLOSED` states. In this case, trying to call -:meth:`~Connection.start_next_cycle` will raise an error, and the only -thing you can legally do is to close this connection and make a new -one. - -HTTP/1.1 also allows for a more aggressive form of connection re-use, -in which a client sends multiple requests in quick succession, and -then waits for the responses to stream back in order -("pipelining"). This is generally considered to have been a bad idea, -because it makes things like error recovery very complicated. - -As a client, h11 does not support pipelining. This is enforced by the -structure of the state machine: after sending one :class:`Request`, -you can't send another until after calling -:meth:`~Connection.start_next_cycle`, and you can't call -:meth:`~Connection.start_next_cycle` until the server has entered the -:data:`DONE` state, which requires reading the server's full -response. - -As a server, h11 provides the minimal support for pipelining required -to comply with the HTTP/1.1 standard: if the client sends multiple -pipelined requests, then we handle the first request until we reach the -:data:`DONE` state, and then :meth:`~Connection.next_event` will -pause and refuse to parse any more events until the response is -completed and :meth:`~Connection.start_next_cycle` is called. See the -next section for more details. - - -.. _flow-control: - -Flow control ------------- - -Presumably you know when you want to send things, and the -:meth:`~Connection.send` interface is very simple: it just immediately -returns all the data you need to send for the given event, so you can -apply whatever send buffer strategy you want. But reading from the -remote peer is a bit trickier: you don't want to read data from the -remote peer if it can't be processed (i.e., you want to apply -backpressure and avoid building arbitrarily large in-memory buffers), -and you definitely don't want to block waiting on data from the remote -peer at the same time that it's blocked waiting for you, because that -will cause a deadlock. - -One complication here is that if you're implementing a server, you -have to be prepared to handle :class:`Request`\s that have an -``Expect: 100-continue`` header. You can `read the spec -`_ for the full -details, but basically what this header means is that after sending -the :class:`Request`, the client plans to pause and wait until they -see some response from the server before they send that request's -:class:`Data`. The server's response would normally be an -:class:`InformationalResponse` with status ``100 Continue``, but it -could be anything really (e.g. a full :class:`Response` with a 4xx -status code). The crucial thing as a server, though, is that you -should never block trying to read a request body if the client is -blocked waiting for you to tell them to send the request body. - -Fortunately, h11 makes this easy, because it tracks whether the client -is in the waiting-for-100-continue state, and exposes this as -:attr:`Connection.they_are_waiting_for_100_continue`. So you don't -have to pay attention to the ``Expect`` header yourself; you just have -to make sure that before you block waiting to read a request body, you -execute some code like: - -.. code-block:: python - - if conn.they_are_waiting_for_100_continue: - do_send(conn, h11.InformationalResponse(100, headers=[...])) - do_read(...) - -In fact, if you're lazy (and what programmer isn't?) then you can just -do this check before all reads -- it's mandatory before blocking to -read a request body, but it's safe at any time. - -And the other thing you want to pay attention to is the special values -that :meth:`~Connection.next_event` might return: :data:`NEED_DATA` -and :data:`PAUSED`. - -:data:`NEED_DATA` is what it sounds like: it means that -:meth:`~Connection.next_event` is guaranteed not to return any more -real events until you've called :meth:`~Connection.receive_data` at -least once. - -:data:`PAUSED` is a little more subtle: it means that -:meth:`~Connection.next_event` is guaranteed not to return any more -real events until something else has happened to clear up the paused -state. There are three cases where this can happen: - -1) We received a full request/response from the remote peer, and then - we received some more data after that. (The main situation where - this might happen is a server responding to a pipelining client.) - The :data:`PAUSED` state will go away after you call - :meth:`~Connection.start_next_cycle`. - -2) A successful ``CONNECT`` or ``Upgrade:`` request has caused the - connection to switch to some other protocol (see - :ref:`switching-protocols`). This :data:`PAUSED` state is - permanent; you should abandon this :class:`Connection` and go do - whatever it is you're going to do with your new protocol. - -3) We're a server, and the client we're talking to proposed to switch - protocols (see :ref:`switching-protocols`), and now is waiting to - find out whether their request was successful or not. Once we - either accept or deny their request then this will turn into one of - the above two states, so you probably don't need to worry about - handling it specially. - -Putting all this together -- - -If your I/O is organized around a "pull" strategy, where your code -requests events as its ready to handle them (e.g. classic synchronous -code, or asyncio's ``await loop.sock_recv(...)``, or `Trio's streams -`__), -then you'll probably want logic that looks something like: - -.. code-block:: python - - # Replace do_sendall and do_recv with your I/O code - def get_next_event(): - while True: - event = conn.next_event() - if event is h11.NEED_DATA: - if conn.they_are_waiting_for_100_continue: - do_sendall(conn, h11.InformationalResponse(100, ...)) - conn.receive_data(do_recv()) - continue - return event - -And then your code that calls this will need to make sure to call it -only at appropriate times (e.g., not immediately after receiving -:class:`EndOfMessage` or :data:`PAUSED`). - -If your I/O is organized around a "push" strategy, where the network -drives processing (e.g. you're using `Twisted -`_, or implementing an -:class:`asyncio.Protocol`), then you'll want to internally apply -back-pressure whenever you see :data:`PAUSED`, remove back-pressure -when you call :meth:`~Connection.start_next_cycle`, and otherwise just -deliver events as they arrive. Something like: - -.. code-block:: python - - class HTTPProtocol(asyncio.Protocol): - # Save the transport for later -- needed to access the - # backpressure API. - def connection_made(self, transport): - self._transport = transport - - # Internal helper function -- deliver all pending events - def _deliver_events(self): - while True: - event = self.conn.next_event() - if event is h11.NEED_DATA: - break - elif event is h11.PAUSED: - # Apply back-pressure - self._transport.pause_reading() - break - else: - self.event_received(event) - - # Called by "someone" whenever new data appears on our socket - def data_received(self, data): - self.conn.receive_data(data) - self._deliver_events() - - # Called by "someone" whenever the peer closes their socket - def eof_received(self): - self.conn.receive_data(b"") - self._deliver_events() - # asyncio will close our socket unless we return True here. - return True - - # Called by your code when its ready to start a new - # request/response cycle - def start_next_cycle(self): - self.conn.start_next_cycle() - # New events might have been buffered internally, and only - # become deliverable after calling start_next_cycle - self._deliver_events() - # Remove back-pressure - self._transport.resume_reading() - - # Fill in your code here - def event_received(self, event): - ... - -And your code that uses this will have to remember to check for -:attr:`~Connection.they_are_waiting_for_100_continue` at the -appropriate time. - - -.. _closing: - -Closing connections -------------------- - -h11 represents a connection shutdown with the special event type -:class:`ConnectionClosed`. You can send this event, in which case -:meth:`~Connection.send` will simply update the state machine and -then return ``None``. You can receive this event, if you call -``conn.receive_data(b"")``. (The actual receipt might be delayed if -the connection is :ref:`paused `.) It's safe and legal -to call ``conn.receive_data(b"")`` multiple times, and once you've -done this once, then all future calls to -:meth:`~Connection.receive_data` will also return -``ConnectionClosed()``: - -.. ipython:: python - - conn = h11.Connection(our_role=h11.CLIENT) - conn.receive_data(b"") - conn.receive_data(b"") - conn.receive_data(None) - -(Or if you try to actually pass new data in after calling -``conn.receive_data(b"")``, that will raise an exception.) - -h11 is careful about interpreting connection closure in a *half-duplex -fashion*. TCP sockets pretend to be a two-way connection, but really -they're two one-way connections. In particular, it's possible for one -party to shut down their sending connection -- which causes the other -side to be notified that the connection has closed via the usual -``socket.recv(...) -> b""`` mechanism -- while still being able to -read from their receiving connection. (On Unix, this is generally -accomplished via the ``shutdown(2)`` system call.) So, for example, a -client could send a request, and then close their socket for writing -to indicate that they won't be sending any more requests, and then -read the response. It's this kind of closure that is indicated by -h11's :class:`ConnectionClosed`: it means that this party will not be -sending any more data -- nothing more, nothing less. You can see this -reflected in the :ref:`state machine `, in which one -party transitioning to :data:`CLOSED` doesn't immediately halt the -connection, but merely prevents it from continuing for another -request/response cycle. - -The state machine also indicates that :class:`ConnectionClosed` events -can only happen in certain states. This isn't true, of course -- any -party can close their connection at any time, and h11 can't stop -them. But what h11 can do is distinguish between clean and unclean -closes. For example, if both sides complete a request/response cycle -and then close the connection, that's a clean closure and everyone -will transition to the :data:`CLOSED` state in an orderly fashion. On -the other hand, if one party suddenly closes the connection while -they're in the middle of sending a chunked response body, or when they -promised a ``Content-Length:`` of 1000 bytes but have only sent 500, -then h11 knows that this is a violation of the HTTP protocol, and will -raise a :exc:`ProtocolError`. Basically h11 treats an unexpected -close the same way it would treat unexpected, uninterpretable data -arriving -- it lets you know that something has gone wrong. - -As a client, the proper way to perform a single request and then close -the connection is: - -1) Send a :class:`Request` with ``Connection: close`` - -2) Send the rest of the request body - -3) Read the server's :class:`Response` and body - -4) ``conn.our_state is h11.MUST_CLOSE`` will now be true. Call - ``conn.send(ConnectionClosed())`` and then close the socket. Or - really you could just close the socket -- the thing calling - ``send`` will do is raise an error if you're not in - :data:`MUST_CLOSE` as expected. So it's between you and your - conscience and your code reviewers. - -(Technically it would also be legal to shutdown your socket for -writing as step 2.5, but this doesn't serve any purpose and some -buggy servers might get annoyed, so it's not recommended.) - -As a server, the proper way to perform a response is: - -1) Send your :class:`Response` and body - -2) Check if ``conn.our_state is h11.MUST_CLOSE``. This might happen - for a variety of reasons; for example, if the response had unknown - length and the client speaks only HTTP/1.0, then the client will - not consider the connection complete until we issue a close. - -You should be particularly careful to take into consideration the -following note fromx `RFC 7230 section 6.6 -`_: - - If a server performs an immediate close of a TCP connection, there is - a significant risk that the client will not be able to read the last - HTTP response. If the server receives additional data from the - client on a fully closed connection, such as another request that was - sent by the client before receiving the server's response, the - server's TCP stack will send a reset packet to the client; - unfortunately, the reset packet might erase the client's - unacknowledged input buffers before they can be read and interpreted - by the client's HTTP parser. - - To avoid the TCP reset problem, servers typically close a connection - in stages. First, the server performs a half-close by closing only - the write side of the read/write connection. The server then - continues to read from the connection until it receives a - corresponding close by the client, or until the server is reasonably - certain that its own TCP stack has received the client's - acknowledgement of the packet(s) containing the server's last - response. Finally, the server fully closes the connection. - - -.. _switching-protocols: - -Switching protocols -------------------- - -h11 supports two kinds of "protocol switches": requests with method -``CONNECT``, and the newer ``Upgrade:`` header, most commonly used for -negotiating WebSocket connections. Both follow the same pattern: the -client proposes that they switch from regular HTTP to some other kind -of interaction, and then the server either rejects the suggestion -- -in which case we return to regular HTTP rules -- or else accepts -it. (For ``CONNECT``, acceptance means a response with 2xx status -code; for ``Upgrade:``, acceptance means an -:class:`InformationalResponse` with status ``101 Switching -Protocols``) If the proposal is accepted, then both sides switch to -doing something else with their socket, and h11's job is done. - -As a developer using h11, it's your responsibility to send and -interpret the actual ``CONNECT`` or ``Upgrade:`` request and response, -and to figure out what to do after the handover; it's h11's job to -understand what's going on, and help you make the handover -smoothly. - -Specifically, what h11 does is :ref:`pause ` parsing -incoming data at the boundary between the two protocols, and then you -can retrieve any unprocessed data from the -:attr:`Connection.trailing_data` attribute. - - -.. _sendfile: - -Support for ``sendfile()`` --------------------------- - -Many networking APIs provide some efficient way to send particular -data, e.g. asking the operating system to stream files directly off of -the disk and into a socket without passing through userspace. - -It's possible to use these APIs together with h11. The basic strategy -is: - -* Create some placeholder object representing the special data, that - your networking code knows how to "send" by invoking whatever the - appropriate underlying APIs are. - -* Make sure your placeholder object implements a ``__len__`` method - returning its size in bytes. - -* Call ``conn.send_with_data_passthrough(Data(data=))`` - -* This returns a list whose contents are a mixture of (a) bytes-like - objects, and (b) your placeholder object. You should send them to - the network in order. - -Here's a sketch of what this might look like: - -.. code-block:: python - - class FilePlaceholder: - def __init__(self, file, offset, count): - self.file = file - self.offset = offset - self.count = count - - def __len__(self): - return self.count - - def send_data(sock, data): - if isinstance(data, FilePlaceholder): - # socket.sendfile added in Python 3.5 - sock.sendfile(data.file, data.offset, data.count) - else: - # data is a bytes-like object to be sent directly - sock.sendall(data) - - placeholder = FilePlaceholder(open("...", "rb"), 0, 200) - for data in conn.send_with_data_passthrough(Data(data=placeholder)): - send_data(sock, data) - -This works with all the different framing modes (``Content-Length``, -``Transfer-Encoding: chunked``, etc.) -- h11 will add any necessary -framing data, update its internal state, and away you go. - - -Identifying h11 in requests and responses ------------------------------------------ - -According to RFC 7231, client requests are supposed to include a -``User-Agent:`` header identifying what software they're using, and -servers are supposed to respond with a ``Server:`` header doing the -same. h11 doesn't construct these headers for you, but to make it -easier for you to construct this header, it provides: - -.. data:: PRODUCT_ID - - A string suitable for identifying the current version of h11 in a - ``User-Agent:`` or ``Server:`` header. - - The version of h11 that was used to build these docs identified - itself as: - - .. ipython:: python - - h11.PRODUCT_ID - - -.. _chunk-delimiters-are-bad: - -Chunked Transfer Encoding Delimiters ------------------------------------- - -.. versionadded:: 0.7.0 - -HTTP/1.1 allows for the use of Chunked Transfer Encoding to frame request and -response bodies. This form of transfer encoding allows the implementation to -provide its body data in the form of length-prefixed "chunks" of data. - -RFC 7230 is extremely clear that the breaking points between chunks of data are -non-semantic: that is, users should not rely on them or assign any meaning to -them. This is particularly important given that RFC 7230 also allows -intermediaries such as proxies and caches to change the chunk boundaries as -they see fit, or even to remove the chunked transfer encoding entirely. - -However, for some applications it is valuable or essential to see the chunk -boundaries because the peer implementation has assigned meaning to them. While -this is against the specification, if you do really need access to this -information h11 makes it available to you in the form of the -:data:`Data.chunk_start` and :data:`Data.chunk_end` properties of the -:class:`Data` event. - -:data:`Data.chunk_start` is set to ``True`` for the first :class:`Data` event -for a given chunk of data. :data:`Data.chunk_end` is set to ``True`` for the -last :class:`Data` event that is emitted for a given chunk of data. h11 -guarantees that it will always emit at least one :class:`Data` event for each -chunk of data received from the remote peer, but due to its internal buffering -logic it may return more than one. It is possible for a single :class:`Data` -event to have both :data:`Data.chunk_start` and :data:`Data.chunk_end` set to -``True``, in which case it will be the only :class:`Data` event for that chunk -of data. - -Again, it is *strongly encouraged* that you avoid relying on this information -if at all possible. This functionality should be considered an escape hatch for -when there is no alternative but to rely on the information, rather than a -general source of data that is worth relying on. diff --git a/docs/source/basic-usage.rst b/docs/source/basic-usage.rst deleted file mode 100644 index 7bc7c3b..0000000 --- a/docs/source/basic-usage.rst +++ /dev/null @@ -1,362 +0,0 @@ -Getting started: Writing your own HTTP/1.1 client -================================================= - -.. currentmodule:: h11 - -h11 can be used to implement both HTTP/1.1 clients and servers. To -give a flavor for how the API works, we'll demonstrate a small -client. - - -HTTP basics ------------ - -An HTTP interaction always starts with a client sending a *request*, -optionally some *data* (e.g., a POST body); and then the server -responds with a *response* and optionally some *data* (e.g. the -requested document). Requests and responses have some data associated -with them: for requests, this is a method (e.g. ``GET``), a target -(e.g. ``/index.html``), and a collection of headers -(e.g. ``User-agent: demo-clent``). For responses, it's a status code -(e.g. 404 Not Found) and a collection of headers. - -Of course, as far as the network is concerned, there's no such thing -as "requests" and "responses" -- there's just bytes being sent from -one computer to another. Let's see what this looks like, by fetching -https://httpbin.org/xml: - -.. ipython:: python - - import ssl, socket - - ctx = ssl.create_default_context() - sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), - server_hostname="httpbin.org") - - # Send request - sock.sendall(b"GET /xml HTTP/1.1\r\nhost: httpbin.org\r\n\r\n") - # Read response - response_data = sock.recv(1024) - # Let's see what we got! - print(response_data) - -.. warning:: - - If you try to reproduce these examples interactively, then you'll - have the most luck if you paste them in all at once. Remember we're - talking to a remote server here – if you type them in one at a - time, and you're too slow, then the server might give up on waiting - for you and close the connection. One way to recognize that this - has happened is if ``response_data`` comes back as an empty string, - or later on when we're working with h11 this might cause errors - that mention ``ConnectionClosed``. - -So that's, uh, very convenient and readable. It's a little more -understandable if we print the bytes as text: - -.. ipython:: python - - print(response_data.decode("ascii")) - -Here we can see the status code at the top (200, which is the code for -"OK"), followed by the headers, followed by the data (a silly little -XML document). But we can already see that working with bytes by hand -like this is really cumbersome. What we need to do is to move up to a -higher level of abstraction. - -This is what h11 does. Instead of talking in bytes, it lets you talk -in high-level HTTP "events". To see what this means, let's repeat the -above exercise, but using h11. We start by making a TLS connection -like before, but now we'll also import :mod:`h11`, and create a -:class:`h11.Connection` object: - -.. ipython:: python - - import ssl, socket - import h11 - - ctx = ssl.create_default_context() - sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), - server_hostname="httpbin.org") - - conn = h11.Connection(our_role=h11.CLIENT) - -Next, to send an event to the server, there are three steps we have to -take. First, we create an object representing the event we want to -send -- in this case, a :class:`h11.Request`: - -.. ipython:: python - - request = h11.Request(method="GET", - target="/xml", - headers=[("Host", "httpbin.org")]) - -Next, we pass this to our connection's :meth:`~Connection.send` -method, which gives us back the bytes corresponding to this message: - -.. ipython:: python - - bytes_to_send = conn.send(request) - -And then we send these bytes across the network: - -.. ipython:: python - - sock.sendall(bytes_to_send) - -There's nothing magical here -- these are the same bytes that we sent -up above: - -.. ipython:: python - - bytes_to_send - -Why doesn't h11 go ahead and send the bytes for you? Because it's -designed to be usable no matter what socket API you're using -- -doesn't matter if it's synchronous like this, asynchronous, -callback-based, whatever; if you can read and write bytes from the -network, then you can use h11. - -In this case, we're not quite done yet -- we have to send another -event to tell the other side that we're finished, which we do by -sending an :class:`EndOfMessage` event: - -.. ipython:: python - - end_of_message_bytes_to_send = conn.send(h11.EndOfMessage()) - sock.sendall(end_of_message_bytes_to_send) - -Of course, it turns out that in this case, the HTTP/1.1 specification -tells us that any request that doesn't contain either a -``Content-Length`` or ``Transfer-Encoding`` header automatically has a -0 length body, and h11 knows that, and h11 knows that the server knows -that, so it actually encoded the :class:`EndOfMessage` event as the -empty string: - -.. ipython:: python - - end_of_message_bytes_to_send - -But there are other cases where it might not, depending on what -headers are set, what message is being responded to, the HTTP version -of the remote peer, etc. etc. So for consistency, h11 requires that -you *always* finish your messages by sending an explicit -:class:`EndOfMessage` event; then it keeps track of the details of -what that actually means in any given situation, so that you don't -have to. - -Finally, we have to read the server's reply. By now you can probably -guess how this is done, at least in the general outline: we read some -bytes from the network, then we hand them to the connection (using -:meth:`Connection.receive_data`) and it converts them into events -(using :meth:`Connection.next_event`). - -.. ipython:: python - - bytes_received = sock.recv(1024) - conn.receive_data(bytes_received) - conn.next_event() - conn.next_event() - conn.next_event() - -(Remember, if you're following along and get an error here mentioning -``ConnectionClosed``, then try again, but going through the steps -faster!) - -Here the server sent us three events: a :class:`Response` object, -which is similar to the :class:`Request` object that we created -earlier and has the response's status code (200 OK) and headers; a -:class:`Data` object containing the response data; and another -:class:`EndOfMessage` object. This similarity between what we send and -what we receive isn't accidental: if we were using h11 to write an HTTP -server, then these are the objects we would have created and passed to -:meth:`~Connection.send` -- h11 in client and server mode has an API -that's almost exactly symmetric. - -One thing we have to deal with, though, is that an entire response -doesn't always arrive in a single call to :meth:`socket.recv` -- -sometimes the network will decide to trickle it in at its own pace, in -multiple pieces. Let's try that again: - -.. ipython:: python - - import ssl, socket - import h11 - - ctx = ssl.create_default_context() - sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), - server_hostname="httpbin.org") - - conn = h11.Connection(our_role=h11.CLIENT) - request = h11.Request(method="GET", - target="/xml", - headers=[("Host", "httpbin.org")]) - sock.sendall(conn.send(request)) - -and this time, we'll read in chunks of 200 bytes, to see how h11 -handles it: - -.. ipython:: python - - bytes_received = sock.recv(200) - conn.receive_data(bytes_received) - conn.next_event() - -:data:`NEED_DATA` is a special value that indicates that we, well, -need more data. h11 has buffered the first chunk of data; let's read -some more: - -.. ipython:: python - - bytes_received = sock.recv(200) - conn.receive_data(bytes_received) - conn.next_event() - -Now it's managed to read a complete :class:`Request`. - - -A basic client object ---------------------- - -Now let's use what we've learned to wrap up our socket and -:class:`Connection` into a single object with some convenience -methods: - -.. literalinclude:: _examples/myclient.py - -.. ipython:: python - :suppress: - - import sys - with open(sys._h11_hack_docs_source_path + "/_examples/myclient.py") as f: - exec(f.read()) - -And then we can send requests: - -.. ipython:: python - - client = MyHttpClient("httpbin.org", 443) - - client.send(h11.Request(method="GET", target="/xml", - headers=[("Host", "httpbin.org")])) - client.send(h11.EndOfMessage()) - -And read back the events: - -.. ipython:: python - - client.next_event() - client.next_event() - -Note here that we received a :class:`Data` event that only has *part* -of the response body -- this is another consequence of our reading in -small chunks. h11 tries to buffer as little as it can, so it streams -out data as it arrives, which might mean that a message body might be -split up into multiple :class:`Data` events. (Of course, if you're the -one sending data, you can do the same thing: instead of buffering all -your data in one giant :class:`Data` event, you can send multiple -:class:`Data` events yourself to stream the data out incrementally; -just make sure that you set the appropriate ``Content-Length`` / -``Transfer-Encoding`` headers.) If we keep reading, we'll see more -:class:`Data` events, and then eventually the :class:`EndOfMessage`: - -.. ipython:: python - - client.next_event() - client.next_event() - client.next_event() - -Now we can see why :class:`EndOfMessage` is so important -- otherwise, -we can't tell when we've received the end of the data. And since -that's the end of this response, the server won't send us anything -more until we make another request -- if we try, then the socket read -will just hang forever, unless we set a timeout or interrupt it: - -.. ipython:: python - :okexcept: - - client.sock.settimeout(2) - client.next_event() - - -Keep-alive ----------- - -For some servers, we'd have to stop here, because they require a new -connection for every request/response. But, this server is smarter -than that -- it supports `keep-alive -`_, so we -can re-use this connection to send another request. There's a few ways -we can tell. First, if it didn't, then it would have closed the -connection already, and we would have gotten a -:class:`ConnectionClosed` event on our last call to -:meth:`~Connection.next_event`. We can also tell by checking h11's -internal idea of what state the two sides of the conversation are in: - -.. ipython:: python - - client.conn.our_state, client.conn.their_state - -If the server didn't support keep-alive, then these would be -:data:`MUST_CLOSE` and either :data:`MUST_CLOSE` or :data:`CLOSED`, -respectively (depending on whether we'd seen the socket actually close -yet). :data:`DONE` / :data:`DONE`, on the other hand, means that this -request/response cycle has totally finished, but the connection itself -is still viable, and we can start over and send a new request on this -same connection. - -To do this, we tell h11 to get ready (this is needed as a safety -measure to make sure different requests/responses on the same -connection don't get accidentally mixed up): - -.. ipython:: python - - client.conn.start_next_cycle() - -This resets both sides back to their initial :data:`IDLE` state, -allowing us to send another :class:`Request`: - -.. ipython:: python - - client.conn.our_state, client.conn.their_state - - client.send(h11.Request(method="GET", target="/get", - headers=[("Host", "httpbin.org")])) - client.send(h11.EndOfMessage()) - client.next_event() - - -What's next? ------------- - -Here's some ideas of things you might try: - -* Adapt the above examples to make a POST request. (Don't forget to - set the ``Content-Length`` header -- but don't worry, if you do - forget, then h11 will give you an error when you try to send data): - - .. code-block:: python - - client.send(h11.Request(method="POST", target="/post", - headers=[("Host", "httpbin.org"), - ("Content-Length", "10")])) - client.send(h11.Data(data=b"1234567890")) - client.send(h11.EndOfMessage()) - -* Experiment with what happens if you try to violate the HTTP protocol - by sending a :class:`Response` as a client, or sending two - :class:`Request`\s in a row. - -* Write your own basic ``http_get`` function that takes a URL, parses - out the host/port/path, then connects to the server, does a ``GET`` - request, and then collects up all the resulting :class:`Data` - objects, concatenates their payloads, and returns it. - -* Adapt the above code to use your favorite non-blocking API - -* Use h11 to write a simple HTTP server. (If you get stuck, `here's an - example - `_.) - -And of course, you'll want to read the :ref:`API-documentation` for -all the details. diff --git a/docs/source/changes.rst b/docs/source/changes.rst deleted file mode 100644 index 10857d5..0000000 --- a/docs/source/changes.rst +++ /dev/null @@ -1,309 +0,0 @@ -History of changes -================== - -.. currentmodule:: h11 - -.. towncrier release notes start - -H11 0.16.0 (2025-04-23) ------------------------ - -Security fix -~~~~~~~~~~~~ - -Reject certain malformed `Transfer-Encoding: chunked` bodies that were previously accepted. These could have enabled request-smuggling attacks when an h11-based HTTP server was placed behind a load balancer with a matching bug in its `chunked` handling. - -Advisory with more details: https://github.com/python-hyper/h11/security/advisories/GHSA-vqfr-h8mv-ghfj - -Reported by: Jeppe Bonde Weikop - -H11 0.15.0 (2025-04-23) ------------------------ - -Bugfixes -~~~~~~~~ - -- Reject Content-Lengths >= 1 zettabyte (1 billion terabytes) early, `without attempting to parse the integer `__ (`#181 `__) - - -Miscellaneous internal changes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Remove the `tests` folder from wheel files. This reduces the zipped file size by 20KB (about 30%). (`#158 `__) - - -H11 0.14.0 (2022-09-25) ------------------------ - -Features -~~~~~~~~ - -- Allow additional trailing whitespace in chunk headers for additional - compatibility with existing servers. (`#133 - `__) -- Improve the type hints for Sentinel types, which should make it - easier to type hint h11 usage. (`#151 - `__ & `#144 - `__)) - -Deprecations and Removals -~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Python 3.6 support is removed. h11 now requires Python>=3.7 - including PyPy 3. Users running `pip install h11` on Python 2 will - automatically get the last Python 2-compatible version. (`#138 - `__) - - -v0.13.0 (2022-01-19) --------------------- - -Features -~~~~~~~~ - -- Clarify that the Headers class is a Sequence and inherit from the - collections Sequence abstract base class to also indicate this (and - gain the mixin methods). See also #104. (`#112 - `__) -- Switch event classes to dataclasses for easier typing and slightly - improved performance. (`#124 - `__) -- Shorten traceback of protocol errors for easier readability (`#132 - `__). -- Add typing including a PEP 561 marker for usage by type checkers - (`#135 `__). -- Expand the allowed status codes to [0, 999] from [0, 600] (`#134 - https://github.com/python-hyper/h11/issues/134`__). - -Backwards **in**\compatible changes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Ensure request method is a valid token (`#141 - https://github.com/python-hyper/h11/pull/141>`__). - - -v0.12.0 (2021-01-01) --------------------- - -Features -~~~~~~~~ - -- Added support for servers with broken line endings. - - After this change h11 accepts both ``\r\n`` and ``\n`` as a headers - delimiter. (`#7 `__) -- Add early detection of invalid http data when request line starts - with binary (`#122 - `__) - - -Deprecations and Removals -~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Python 2.7 and PyPy 2 support is removed. h11 now requires - Python>=3.6 including PyPy 3. Users running `pip install h11` on - Python 2 will automatically get the last Python 2-compatible - version. (`#114 `__) - - -v0.11.0 (2020-10-05) --------------------- - -New features: - -* h11 now stores and makes available the raw header name as - received. In addition h11 will write out header names with the same - casing as passed to it. This allows compatibility with systems that - expect titlecased header names. See `#31 - `__. -* Multiple content length headers are now merged into a single header - if all the values are equal, if any are unequal a LocalProtocol - error is raised (as before). See `#92 - `__. - -Backwards **in**\compatible changes: - -* Headers added by h11, rather than passed to it, now have titlecased - names. Whilst this should help compatibility it replaces the - previous lowercased header names. - -v0.10.0 (2020-08-14) --------------------- - -Other changes: - -* Drop support for Python 3.4. -* Support Python 3.8. -* Make error messages returned by match failures less ambiguous (`#98 - `__). - -v0.9.0 (2019-05-15) -------------------- - -Bug fixes: - -* Allow a broader range of characters in header values. This violates - the RFC, but is apparently required for compatibility with - real-world code, like Google Analytics cookies (`#57 - `__, `#58 - `__). -* Validate incoming and outgoing request paths for invalid - characters. This prevents a variety of potential security issues - that have affected other HTTP clients. (`#69 - `__). -* Force status codes to be integers, thereby allowing stdlib - HTTPStatus IntEnums to be used when constructing responses (`#72 - `__). - -Other changes: - -* Make all sentinel values inspectable by IDEs, and split - ``SEND_BODY_DONE`` into ``SEND_BODY``, and ``DONE`` (`#75 - `__). -* Drop support for Python 3.3. -* LocalProtocolError raised in start_next_cycle now shows states for - more informative errors (`#80 - `__). - -v0.8.1 (2018-04-14) -------------------- - -Bug fixes: - -* Always return headers as ``bytes`` objects (`#60 - `__) - -Other changes: - -* Added proper license notices to the Javascript used in our - documentation (`#61 - `__) - - -v0.8.0 (2018-03-20) -------------------- - -Backwards **in**\compatible changes: - -* h11 now performs stricter validation on outgoing header names and - header values: illegal characters are now rejected (example: you - can't put a newline into an HTTP header), and header values with - leading/trailing whitespace are also rejected (previously h11 would - silently discard the whitespace). All these checks were already - performed on incoming headers; this just extends that to outgoing - headers. - -New features: - -* New method :meth:`Connection.send_failed`, to notify a - :class:`Connection` object when data returned from - :meth:`Connection.send` was *not* sent. - -Bug fixes: - -* Make sure that when computing the framing headers for HEAD - responses, we produce the same results as we would for the - corresponding GET. - -* Error out if a request has multiple Host: headers. - -* Send the Host: header first, as recommended by RFC 7230. - -* The Expect: header `is case-insensitive - `__, so use - case-insensitive matching when looking for 100-continue. - -Other changes: - -* Better error messages in several cases. - -* Provide correct ``error_status_hint`` in exception raised when - encountering an invalid ``Transfer-Encoding`` header. - -* For better compatibility with broken servers, h11 now tolerates - responses where the reason phrase is missing (not just empty). - -* Various optimizations and documentation improvements. - - -v0.7.0 (2016-11-25) -------------------- - -New features (backwards compatible): - -* Made it so that sentinels are :ref:`instances of themselves - `, to enable certain dispatch tricks on - the return value of :func:`Connection.next_event` (see `issue #8 - `__ for discussion). - -* Added :data:`Data.chunk_start` and :data:`Data.chunk_end` properties - to the :class:`Data` event. These provide the user information - about where chunk delimiters are in the data stream from the remote - peer when chunked transfer encoding is in use. You :ref:`probably - shouldn't use these `, but sometimes - there's no alternative (see `issue #19 - `__ for discussion). - -* Expose :data:`Response.reason` attribute, making it possible to read - or set the textual "reason phrase" on responses (`issue #13 - `__). - -Bug fixes: - -* Fix the error message given when a call to an event constructor is - missing a required keyword argument (`issue #14 - `__). - -* Fixed encoding of empty :class:`Data` events (``Data(data=b"")``) - when using chunked encoding (`issue #21 - `__). - -v0.6.0 (2016-10-24) -------------------- - -This is the first release since we started using h11 to write -non-trivial server code, and this experience triggered a number of -substantial API changes. - -Backwards **in**\compatible changes: - -* Split the old :meth:`receive_data` into the new - :meth:`~Connection.receive_data` and - :meth:`~Connection.next_event`, and replaced the old :class:`Paused` - pseudo-event with the new :data:`NEED_DATA` and :data:`PAUSED` - sentinels. - -* Simplified the API by replacing the old :meth:`Connection.state_of`, - :attr:`Connection.client_state`, :attr:`Connection.server_state` with - the new :attr:`Connection.states`. - -* Renamed the old :meth:`prepare_to_reuse` to the new - :meth:`~Connection.start_next_cycle`. - -* Removed the ``Paused`` pseudo-event. - -Backwards compatible changes: - -* State machine: added a :data:`DONE` -> :data:`MUST_CLOSE` transition - triggered by our peer being in the :data:`ERROR` state. - -* Split :exc:`ProtocolError` into :exc:`LocalProtocolError` and - :exc:`RemoteProtocolError` (see :ref:`error-handling`). Use case: HTTP - servers want to be able to distinguish between an error that - originates locally (which produce a 500 status code) versus errors - caused by remote misbehavior (which produce a 4xx status code). - -* Changed the :data:`PRODUCT_ID` from ``h11/`` to - ``python-h11/``. (This is similar to what requests uses, - and much more searchable than plain h11.) - -Other changes: - -* Added a minimal benchmark suite, and used it to make a few small - optimizations (maybe ~20% speedup?). - - -v0.5.0 (2016-05-14) -------------------- - -* Initial release. diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index b3627f5..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,325 +0,0 @@ -#!/usr/bin/env python3 -# -# h11 documentation build configuration file, created by -# sphinx-quickstart on Tue May 3 00:20:14 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -################################################################ -# hack hack -# -# The live ipython examples want to know where the docs source/ directory is, -# so that they can find files that live there. -# -# There's no guarantee that our CWD == the source directory, but conf.py -# *does* know what directory it lives in, so it can stash that in a public -# place where the later code can find it. -# -# (In particular, the sphinx Makefile runs sphinx-build from a different -# directory -- but RTD runs sphinx-build directly from inside the source/ -# directory, so there's no single value of this that works for both.) -# -import os.path -sys._h11_hack_docs_source_path = os.path.dirname(__file__) -################################################################ - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'IPython.sphinxext.ipython_directive', - 'IPython.sphinxext.ipython_console_highlighting', -] - -# Undocumented trick: if we def setup here in conf.py, it gets called just -# like an extension's setup function. -def setup(app): - app.add_javascript("show-code.js") - app.add_javascript("facebox.js") - app.add_stylesheet("facebox.css") - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'h11' -copyright = '2016, Nathaniel J. Smith' -author = 'Nathaniel J. Smith' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -import h11 -version = h11.__version__ -# The full version, including alpha/beta/rc tags. -release = h11.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -#html_title = 'h11 v0.0.1' - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'h11doc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'h11.tex', 'h11 Documentation', - 'Nathaniel J. Smith', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'h11', 'h11 Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'h11', 'h11 Documentation', - author, 'h11', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python': ('https://docs.python.org/3.5', None), -} diff --git a/docs/source/examples.rst b/docs/source/examples.rst deleted file mode 100644 index c3d6f9d..0000000 --- a/docs/source/examples.rst +++ /dev/null @@ -1,22 +0,0 @@ -Examples -======== - -.. - If we add any more examples then we should probably split this out - into separate pages for each example - -You can also find these in the `examples/ directory of a source -checkout `_. - -Minimal client, using synchronous I/O -------------------------------------- - -.. literalinclude:: ../../examples/basic-client.py - :language: python - - -Fairly complete server with error handling, using Trio for async I/O --------------------------------------------------------------------- - -.. literalinclude:: ../../examples/trio-server.py - :language: python diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index ee02847..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,75 +0,0 @@ -h11: A pure-Python HTTP/1.1 protocol library -============================================ - -h11 is an HTTP/1.1 protocol library written in Python, heavily inspired -by `hyper-h2 `_. - -h11's goal is to be a simple, robust, complete, and non-hacky -implementation of the first "chapter" of the HTTP/1.1 spec: `RFC 7230: -HTTP/1.1 Message Syntax and Routing -`_. That is, it mostly focuses on -implementing HTTP at the level of taking bytes on and off the wire, -and the headers related to that, and tries to be picky about spec -conformance when possible. It doesn't know about higher-level concerns -like URL routing, conditional GETs, cross-origin cookie policies, or -content negotiation. But it does know how to take care of framing, -cross-version differences in keep-alive handling, and the "obsolete -line folding" rule, and to use bounded time and space to process even -pathological / malicious input, so that you can focus your energies on -the hard / interesting parts for your application. And it tries to -support the full specification in the sense that any useful HTTP/1.1 -conformant application should be able to use h11. - -This is a "bring-your-own-I/O" protocol library; like h2, it contains -no I/O code whatsoever. This means you can hook h11 up to your -favorite network API, and that could be anything you want: -synchronous, threaded, asynchronous, or your own implementation of -`RFC 6214 `_ -- h11 won't judge -you. This is h11's main feature compared to the current state of the -art, where every HTTP library is tightly bound to a particular network -framework, and every time a `new network API -`_ comes along then someone has to -start over reimplementing the entire HTTP stack from scratch. We -highly recommend `Cory Benfield's excellent blog post about the -advantages of this approach -`_. - -This also means that h11 is not immediately useful out of the box: -it's a toolkit for building programs that speak HTTP, not something -that could directly replace ``requests`` or ``twisted.web`` or -whatever. But h11 makes it much easier to implement something like -``requests`` or ``twisted.web``. - - -Vital statistics ----------------- - -* Requirements: Python 3.8+ (PyPy works great) - - The last Python 2-compatible version was h11 0.11.x. - -* Install: ``pip install h11`` - -* Sources and bug tracker: https://github.com/python-hyper/h11 - -* Docs: https://h11.readthedocs.io - -* License: MIT - -* Code of conduct: Contributors are requested to follow our `code of - conduct - `_ in - all project spaces. - - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - basic-usage.rst - api.rst - examples.rst - supported-http.rst - changes.rst diff --git a/docs/source/make-state-diagrams.py b/docs/source/make-state-diagrams.py deleted file mode 100644 index 617efa5..0000000 --- a/docs/source/make-state-diagrams.py +++ /dev/null @@ -1,189 +0,0 @@ -#!python - -import sys -sys.path.append("../..") - -import os.path -import subprocess - -from h11._events import * -from h11._state import * -from h11._state import ( - _SWITCH_UPGRADE, _SWITCH_CONNECT, - EVENT_TRIGGERED_TRANSITIONS, STATE_TRIGGERED_TRANSITIONS, -) - -_EVENT_COLOR = "#002092" -_STATE_COLOR = "#017517" -_SPECIAL_COLOR = "#7600a1" - -HEADER = """ -digraph { - graph [fontname = "Lato" bgcolor="transparent"] - node [fontname = "Lato"] - edge [fontname = "Lato"] -""" - -def finish(machine_name): - return (""" - labelloc="t" - labeljust="l" - label=<h11 state machine: {}> -}} -""".format(machine_name)) - -class Edges: - def __init__(self): - self.edges = [] - - def e(self, source, target, label, color, italicize=False, weight=1): - if italicize: - quoted_label = f"<{label}>" - else: - quoted_label = f'<{label}>' - self.edges.append( - f'{source} -> {target} [\n' - f' label={quoted_label},\n' - f' color="{color}", fontcolor="{color}",\n' - f' weight={weight},\n' - f']\n' - ) - - def write(self, f): - self.edges.sort() - f.write("".join(self.edges)) - -def make_dot_special_state(out_path): - with open(out_path, "w") as f: - f.write(HEADER) - f.write(""" - kaT [label=<keep-alive is enabled
initial state
>] - kaF [label=<keep-alive is disabled>] - - upF [label=<No potential Upgrade: pending
initial state
>] - upT [label=<Potential Upgrade: pending>] - - coF [label=<No potential CONNECT pending
initial state
>] - coT [label=<Potential CONNECT pending>] -""") - edges = Edges() - for s in ["kaT", "kaF"]: - edges.e(s, "kaF", - "Request/response with
HTTP/1.0 or Connection: close", - color=_EVENT_COLOR, - italicize=True) - - edges.e("upF", "upT", - "Request with Upgrade:", - color=_EVENT_COLOR, italicize=True) - edges.e("upT", "upF", - "Response", - color=_EVENT_COLOR, italicize=True) - - edges.e("coF", "coT", - "Request with CONNECT", - color=_EVENT_COLOR, italicize=True) - edges.e("coT", "coF", - "Response without 2xx status", - color=_EVENT_COLOR, italicize=True) - - edges.write(f) - - f.write(finish("special states")) - -def make_dot(role, out_path): - with open(out_path, "w") as f: - f.write(HEADER) - f.write(""" - IDLE [label=start state>] - // move ERROR down to the bottom - {rank=same CLOSED ERROR} -""") - - # Dot output is sensitive to the order in which the nodes and edges - # are listed. We generate them in python's randomized dict iteration - # order. So to normalize order, we accumulate and then sort. - # Fortunately, this order happens to be one that produces a nice - # layout... with other orders I've seen really terrible layouts, and - # had to do things like move the server's IDLE->MUST_CLOSE to the top - # of the file to fix them. - edges = Edges() - - CORE_EVENTS = {Request, InformationalResponse, - Response, Data, EndOfMessage} - - for (source_state, t) in EVENT_TRIGGERED_TRANSITIONS[role].items(): - for (event_type, target_state) in t.items(): - weight = 1 - color = _EVENT_COLOR - italicize = False - if (event_type in CORE_EVENTS - and source_state is not target_state): - weight = 10 - # exception - if (event_type is Response and source_state is IDLE): - weight = 1 - if isinstance(event_type, tuple): - # The weird special cases - #color = _SPECIAL_COLOR - if event_type == (Request, CLIENT): - name = "client makes Request" - weight = 10 - elif event_type[1] is _SWITCH_UPGRADE: - name = "101 Switching Protocols" - weight = 1 - elif event_type[1] is _SWITCH_CONNECT: - name = "CONNECT accepted" - weight = 1 - else: - assert False - else: - name = event_type.__name__ - edges.e(source_state, target_state, name, color, - weight=weight, italicize=italicize) - - for state_pair, updates in STATE_TRIGGERED_TRANSITIONS.items(): - if role not in updates: - continue - if role is CLIENT: - (our_state, their_state) = state_pair - else: - (their_state, our_state) = state_pair - edges.e(our_state, updates[role], - f"peer in
{their_state}", - color=_STATE_COLOR) - - if role is CLIENT: - edges.e(DONE, MIGHT_SWITCH_PROTOCOL, - "Potential Upgrade:
or CONNECT pending", - _STATE_COLOR, - italicize=True) - edges.e(MIGHT_SWITCH_PROTOCOL, DONE, - "No potential Upgrade:
or CONNECT pending", - _STATE_COLOR, - italicize=True) - - edges.e(DONE, MUST_CLOSE, "keep-alive
is disabled", _STATE_COLOR, - italicize=True) - edges.e(DONE, IDLE, "start_next_cycle()", _SPECIAL_COLOR) - - edges.write(f) - - # For some reason labelfontsize doesn't seem to do anything, but this - # works - f.write(finish(role)) - -my_dir = os.path.dirname(__file__) -out_dir = os.path.join(my_dir, "_static") -if not os.path.exists(out_dir): - os.path.mkdir(out_dir) -for role in (CLIENT, SERVER): - dot_path = os.path.join(out_dir, str(role) + ".dot") - svg_path = dot_path[:-3] + "svg" - make_dot(role, dot_path) - subprocess.check_call(["dot", "-Tsvg", dot_path, "-o", svg_path]) - -dot_path = os.path.join(out_dir, "special-states.dot") -svg_path = dot_path[:-3] + "svg" -make_dot_special_state(dot_path) -subprocess.check_call(["dot", "-Tsvg", dot_path, "-o", svg_path]) diff --git a/docs/source/supported-http.rst b/docs/source/supported-http.rst deleted file mode 100644 index 5fc33b8..0000000 --- a/docs/source/supported-http.rst +++ /dev/null @@ -1,76 +0,0 @@ -Details of our HTTP support for HTTP nerds -========================================== - -.. currentmodule:: h11 - -h11 only speaks HTTP/1.1. It can talk to HTTP/1.0 clients and servers, -but it itself only does HTTP/1.1. - -We fully support HTTP/1.1 keep-alive. - -We have a little bit of support for HTTP/1.1 pipelining -- basically -the minimum that's required by the standard. In server mode we can -handle pipelined requests in a serial manner, responding completely to -each request before reading the next (and our API is designed to make -it easy for servers to keep this straight). Client mode doesn't -support pipelining at all. As far as I can tell, this matches the -state of the art in all the major HTTP implementations: the consensus -seems to be that HTTP/1.1 pipelining was a nice try but unworkable in -practice, and if you really need pipelining to work then instead of -trying to fix HTTP/1.1 you should switch to HTTP/2.0. - -The HTTP/1.0 ``Connection: keep-alive`` pseudo-standard is currently -not supported. (Note that this only affects h11 as a server, because -h11 as a client always speaks HTTP/1.1.) Supporting this would be -possible, but it's fragile and finicky and I'm suspicious that if we -leave it out then no-one will notice or care. HTTP/1.1 is now almost -old enough to vote in United States elections. I get that people -sometimes write HTTP/1.0 clients because they don't want to deal with -annoying stuff like chunked encoding, and I completely sympathize with -that, but I'm guessing that you're not going to find too many people -these days who care desperately about keep-alive *and at the same -time* are too lazy to implement Transfer-Encoding: chunked. Still, -this would be my bet as to the missing feature that people are most -likely to eventually complain about... - -Of the headers defined in RFC 7230, the ones h11 knows and has some -special-case logic to care about are: ``Connection:``, -``Transfer-Encoding:``, ``Content-Length:``, ``Host:``, ``Upgrade:``, -and ``Expect:`` (which is really from `RFC 7231 -`_ but -whatever). The other headers in RFC 7230 are ``TE:``, ``Trailer:``, -and ``Via:``; h11 also supports these in the sense that it ignores -them and that's really all it should be doing. - -Transfer-Encoding support: we only know ``chunked``, not ``gzip`` or -``deflate``. We're in good company in this: node.js at least doesn't -handle anything besides ``chunked`` either. So I'm not too worried -about this being a problem in practice. But I'm not majorly opposed to -adding support for more features here either. - -A quirk in our :class:`Response` encoding: we don't bother including -ascii status messages -- instead of ``200 OK`` we just say -``200``. This is totally legal and no program should care, and it lets -us skip carrying around a pointless table of status message strings, -but I suppose it might be worth fixing at some point. - -When parsing chunked encoding, we parse but discard "chunk -extensions". This is an extremely obscure feature that allows -arbitrary metadata to be interleaved into a chunked transfer -stream. This metadata has no standard uses, and proxies are allowed to -strip it out. I don't think anyone will notice this lack, but it could -be added if someone really wants it; I just ran out of energy for -implementing weirdo features no-one uses. - -Currently we *do* implement support for "obsolete line folding" when -reading HTTP headers. This is an optional part of the spec -- -conforming HTTP/1.1 implementations MUST NOT send continuation lines, -and conforming HTTP/1.1 servers MAY send 400 Bad Request responses -back at clients who do send them (`ref -`_). I'm tempted to -remove this support, since it adds some complicated and ugly code -right at the center of the request/response parsing loop, and I'm not -sure whether anyone actually needs it. Unfortunately a few major -implementations that I spot-checked (node.js, go) do still seem to -support reading such headers (but not generating them), so it might or -might not be obsolete in practice -- it's hard to know. diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..e3e0de6 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,1009 @@ +# API documentation + +h11-mypyc has a fairly small public API, with all public symbols available directly at +the top level: + +```pycon +>>> import h11_mypyc +>>> h11_mypyc. +h11_mypyc.CLIENT h11_mypyc.MUST_CLOSE +h11_mypyc.CLOSED h11_mypyc.NEED_DATA +h11_mypyc.Connection h11_mypyc.PAUSED +h11_mypyc.ConnectionClosed h11_mypyc.PRODUCT_ID +h11_mypyc.DONE h11_mypyc.ProtocolError +h11_mypyc.Data h11_mypyc.RemoteProtocolError +h11_mypyc.ERROR h11_mypyc.Request +h11_mypyc.EndOfMessage h11_mypyc.Response +h11_mypyc.Event h11_mypyc.SEND_BODY +h11_mypyc.IDLE h11_mypyc.SEND_RESPONSE +h11_mypyc.InformationalResponse h11_mypyc.SERVER +h11_mypyc.LocalProtocolError h11_mypyc.SWITCHED_PROTOCOL +h11_mypyc.MIGHT_SWITCH_PROTOCOL +``` + +These symbols fall into three main categories: event classes, special constants +used to track different connection states, and the +[`Connection`][h11_mypyc.Connection] class itself. We'll describe them in that order. + +## Events { #events } + +*Events* are the core of h11-mypyc: the whole point of h11-mypyc is to let you think about +HTTP transactions as being a series of events sent back and forth between a +client and a server, instead of thinking in terms of bytes. + +All events behave in essentially similar ways. Let's take +[`Request`][h11_mypyc.Request] as an example. Like all events, this is a "final" class +-- you cannot subclass it. And like all events, it has several fields. For +[`Request`][h11_mypyc.Request], there are four of them: `method`, `target`, `headers`, +and `http_version`. `http_version` defaults to `b"1.1"`; the rest have no +default, so to create a [`Request`][h11_mypyc.Request] you have to specify their +values: + +```pycon +>>> req = h11_mypyc.Request(method="GET", +... target="/", +... headers=[("Host", "example.com")]) +``` + +Event constructors accept only keyword arguments, not positional arguments. + +Events have a useful repr: + +```pycon +>>> req +Request(method=b'GET', headers=, target=b'/', http_version=b'1.1') +``` + +And their fields are available as regular attributes: + +```pycon +>>> req.method +b'GET' +>>> req.target +b'/' +>>> req.headers + +>>> req.http_version +b'1.1' +``` + +Notice that these attributes have been normalized to byte-strings. In general, +events normalize and validate their fields when they're constructed. Some of +these normalizations and checks are specific to a particular event -- for +example, [`Request`][h11_mypyc.Request] enforces RFC 7230's requirement that HTTP/1.1 +requests must always contain a `"Host"` header: + +```pycon +>>> # HTTP/1.0 requests don't require a Host: header +... h11_mypyc.Request(method="GET", target="/", headers=[], http_version="1.0") +Request(method=b'GET', headers=, target=b'/', http_version=b'1.0') +``` + +```pycon +>>> # But HTTP/1.1 requests do +... h11_mypyc.Request(method="GET", target="/", headers=[]) +Traceback (most recent call last): + ... +h11_mypyc._util.LocalProtocolError: Missing mandatory Host: header +``` + +This helps protect you from accidentally violating the protocol, and also helps +protect you from remote peers who attempt to violate the protocol. + +A few of these normalization rules are standard across multiple events, so we +document them here: + +### Header normalization rules { #headers-format } + +In h11-mypyc, headers are represented internally as a list of (*name*, *value*) pairs, +where *name* and *value* are both byte-strings, *name* is always lowercase, and +*name* and *value* are both guaranteed not to have any leading or trailing +whitespace. When constructing an event, we accept any iterable of pairs like +this, and will automatically convert native strings containing ascii or +[bytes-like objects](https://docs.python.org/3/glossary.html#term-bytes-like-object) +to byte-strings and convert names to lowercase: + +```pycon +>>> original_headers = [("HOST", bytearray(b"Example.Com"))] +>>> req = h11_mypyc.Request(method="GET", target="/", headers=original_headers) +>>> original_headers +[('HOST', bytearray(b'Example.Com'))] +>>> req.headers + +``` + +If any names are detected with leading or trailing whitespace, then this is an +error ("in the past, differences in the handling of such whitespace have led to +security vulnerabilities" -- +[RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2.4)). We also check for +certain other protocol violations, e.g. it's always illegal to have a newline +inside a header value, and `Content-Length: hello` is an error because +`Content-Length` should always be an integer. We may add additional checks in the +future. + +While we make sure to expose header names as lowercased bytes, we also preserve +the original header casing that is used. Compliant HTTP agents should always +treat headers in a case insensitive manner, but this may not always be the case. +When sending bytes over the wire we send headers preserving whatever original +header casing was used. + +It is possible to access the headers in their raw original casing, which may be +useful for some user output or debugging purposes. + +```pycon +>>> original_headers = [("Host", "example.com")] +>>> req = h11_mypyc.Request(method="GET", target="/", headers=original_headers) +>>> req.headers.raw_items() +[(b'Host', b'example.com')] +``` + +### HTTP version normalization rules { #http-version-format } + +It's not just headers we normalize to being byte-strings: the same +type-conversion logic is also applied to the `Request.method` and +`Request.target` field, and -- for consistency -- all `http_version` fields. In +particular, we always represent HTTP version numbers as byte-strings like +`b"1.1"`. +[Bytes-like objects](https://docs.python.org/3/glossary.html#term-bytes-like-object) +and native strings will be automatically converted to byte strings. Note that the +HTTP standard +[specifically guarantees](https://tools.ietf.org/html/rfc7230#section-2.6) that +all HTTP version numbers will consist of exactly two digits separated by a dot, +so comparisons like `req.http_version < b"1.1"` are safe and valid. + +When manually constructing an event, you generally shouldn't specify +`http_version`, because it defaults to `b"1.1"`, and if you attempt to override +this to some other value then [`Connection.send()`][h11_mypyc.Connection.send] will +reject your event -- h11-mypyc only speaks HTTP/1.1. But it does understand other +versions of HTTP, so you might receive events with other `http_version` values +from remote peers. + +### Event reference + +Here's the complete set of events supported by h11-mypyc: + +::: h11_mypyc.Request + +::: h11_mypyc.InformationalResponse + +::: h11_mypyc.Response + +::: h11_mypyc.Data + +::: h11_mypyc.EndOfMessage + +::: h11_mypyc.ConnectionClosed + +## The state machine { #state-machine } + +Now that you know what the different events are, the next question is: what can +you do with them? + +A basic HTTP request/response cycle looks like this: + +- The client sends: + + - one [`Request`][h11_mypyc.Request] event with request metadata and headers, + - zero or more [`Data`][h11_mypyc.Data] events with the request body (if any), + - and an [`EndOfMessage`][h11_mypyc.EndOfMessage] event. + +- And then the server replies with: + + - zero or more [`InformationalResponse`][h11_mypyc.InformationalResponse] events, + - one [`Response`][h11_mypyc.Response] event, + - zero or more [`Data`][h11_mypyc.Data] events with the response body (if any), + - and an [`EndOfMessage`][h11_mypyc.EndOfMessage] event. + +And once that's finished, both sides either close the connection, or they go back +to the top and re-use it for another request/response cycle. + +To coordinate this interaction, the h11-mypyc [`Connection`][h11_mypyc.Connection] object +maintains several state machines: one that tracks what the client is doing, one +that tracks what the server is doing, and a few more tiny ones to track whether +[keep-alive](#keepalive-and-pipelining) is enabled and whether the client has +proposed to [switch protocols](#switching-protocols). h11-mypyc always keeps track of +all of these state machines, regardless of whether it's currently playing the +client or server role. + +The state machines look like this: + +=== "Client" + + ```mermaid + --8<-- "client-states.mmd" + ``` + +=== "Server" + + ```mermaid + --8<-- "server-states.mmd" + ``` + +=== "Special states" + + ```mermaid + --8<-- "special-states.mmd" + ``` + +If you squint at the first two diagrams, you can see the client's IDLE -> +SEND_BODY -> DONE path and the server's IDLE -> SEND_RESPONSE -> SEND_BODY -> +DONE path, which encode the basic sequence of events we described above. But +there's a fair amount of other stuff going on here as well. + +The first thing you should notice is the different colors. These correspond to +the different ways that our state machines can change state. + +- Plain arcs are *event-triggered transitions*: if we're in state A, and this + event happens, then we switch to state B. For the client machine, these + transitions always happen when the client *sends* an event. For the server + machine, most of them involve the server sending an event, except that the + server also goes from IDLE -> SEND_RESPONSE when the client sends a + [`Request`][h11_mypyc.Request]. + +- Green arcs are *state-triggered transitions*: these are somewhat unusual, and + are used to couple together the different state machines -- if, at any moment, + one machine is in state A and another machine is in state B, then the first + machine immediately transitions to state C. For example, if the CLIENT machine + is in state DONE, and the SERVER machine is in the CLOSED state, then the + CLIENT machine transitions to MUST_CLOSE. And the same thing happens if the + CLIENT machine is in the state DONE and the keep-alive machine is in the state + disabled. + +- There are also two purple arcs labeled + [`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle]: these correspond to an + explicit method call documented below. + +Here's why we have all the stuff in those diagrams above, beyond what's needed to +handle the basic request/response cycle: + +- Server sending a [`Response`][h11_mypyc.Response] directly from `IDLE`: This is used + for error responses, when the client's request never arrived (e.g. 408 Request + Timed Out) or was unparseable gibberish (400 Bad Request) and thus didn't + register with our state machine as a real [`Request`][h11_mypyc.Request]. + +- The transitions involving `MUST_CLOSE` and `CLOSED`: keep-alive and shutdown + handling; see [Re-using a connection](#keepalive-and-pipelining) and + [Closing connections](#closing). + +- The transitions involving `MIGHT_SWITCH_PROTOCOL` and `SWITCHED_PROTOCOL`: see + [Switching protocols](#switching-protocols). + +- That weird `ERROR` state hanging out all lonely on the bottom: to avoid + cluttering the diagram, we don't draw any arcs coming into this node, but that + doesn't mean it can't be entered. In fact, it can be entered from any state: if + any exception occurs while trying to send/receive data, then the corresponding + machine will transition directly to this state. Once there, though, it can + never leave -- that part of the diagram is accurate. See + [Error handling](#error-handling). + +And finally, note that in these diagrams, all the labels that are in *italics* +are informal English descriptions of things that happen in the code, while the +labels in upright text correspond to actual objects in the public API. You've +already seen the event objects like [`Request`][h11_mypyc.Request] and +[`Response`][h11_mypyc.Response]; there are also a set of opaque sentinel values that +you can use to track and query the client and server's states. + +## Special constants + +h11-mypyc exposes some special constants corresponding to the different states in the +client and server state machines described above. The complete list is: + +`IDLE`, `SEND_RESPONSE`, `SEND_BODY`, `DONE`, `MUST_CLOSE`, `CLOSED`, +`MIGHT_SWITCH_PROTOCOL`, `SWITCHED_PROTOCOL`, `ERROR` + +For example, we can see that initially the client and server start in state +`IDLE` / `IDLE`: + +```pycon +>>> conn = h11_mypyc.Connection(our_role=h11_mypyc.CLIENT) +>>> conn.states +{: , : } +``` + +And then if the client sends a [`Request`][h11_mypyc.Request], then the client switches +to state `SEND_BODY`, while the server switches to state `SEND_RESPONSE`: + +```pycon +>>> conn.send(h11_mypyc.Request(method="GET", target="/", headers=[("Host", "example.com")])) +b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' +>>> conn.states +{: , : } +``` + +And we can test these values directly using constants like `SEND_BODY`: + +```pycon +>>> conn.states[h11_mypyc.CLIENT] is h11_mypyc.SEND_BODY +True +``` + +This shows how the [`Connection`][h11_mypyc.Connection] type tracks these state +machines and lets you query their current state. + +### Roles { #roles } + +The above also showed the special constants that can be used to indicate the two +different roles that a peer can play in an HTTP connection: `CLIENT` and +`SERVER`. + +And finally, there are also two special constants that can be returned from +[`Connection.next_event()`][h11_mypyc.Connection.next_event]: `NEED_DATA` and `PAUSED`. + +### How the sentinels work { #sentinel-types } + +All of h11-mypyc's constants are *classes*, not instances -- they are never +instantiated, and you compare them with `is`, the way you would compare against +[`None`][]: + +```pycon +>>> conn.states[h11_mypyc.CLIENT] is h11_mypyc.SEND_BODY +True +``` + +Because they are classes, `type(some_sentinel)` is just [`type`][], so you cannot +use `type()` to dispatch on the return value of +[`Connection.next_event()`][h11_mypyc.Connection.next_event] the way you can for event +objects. What you can do instead is dispatch on the value itself -- for example, +with a handler table keyed on the sentinel, or by checking for the sentinels +explicitly before falling back to `type(event)` for the real events: + +```python +event = conn.next_event() +if event is h11_mypyc.NEED_DATA: + ... +elif event is h11_mypyc.PAUSED: + ... +else: + handler = getattr(some_object, "handle_" + type(event).__name__) + handler(event) +``` + +Not that this kind of dispatch-based strategy is always the best approach -- but +the option is there if you want it. + +!!! note + + In h11 releases 0.7.0 through 0.16.0, the sentinels were instances of + themselves (`type(h11_mypyc.NEED_DATA) is h11_mypyc.NEED_DATA`), which made the plain + `type(event)` dispatch above work uniformly. That relied on a custom + metaclass, which mypyc cannot compile, so it was dropped. `is` comparisons + against the sentinels are unaffected. + +## The Connection object + +::: h11_mypyc.Connection + options: + members: + - receive_data + - next_event + - send + - send_with_data_passthrough + - send_failed + - start_next_cycle + - states + - our_state + - their_state + - trailing_data + +In addition to the members documented above, a +[`Connection`][h11_mypyc.Connection] has the following attributes: + +`our_role` +: `CLIENT` if this is a client; `SERVER` if this is a server. + +`their_role` +: `SERVER` if this is a client; `CLIENT` if this is a server. + +`their_http_version` +: The version of HTTP that our peer claims to support. `None` if we haven't yet + received a request/response. + + This is preserved by + [`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle], so it can be handy + for a client making multiple requests on the same connection: normally you + don't know what version of HTTP the server supports until after you do a + request and get a response -- so on an initial request you might have to + assume the worst. But on later requests on the same connection, the + information will be available here. + +`client_is_waiting_for_100_continue` +: True if the client sent a request with the `Expect: 100-continue` header, and + is still waiting for a response (i.e., the server has not sent a 100 Continue + or any other kind of response, and the client has not gone ahead and started + sending the body anyway). + + See + [RFC 7231 section 5.1.1](https://tools.ietf.org/html/rfc7231#section-5.1.1) + for details. + +`they_are_waiting_for_100_continue` +: True if `their_role` is `CLIENT` and `client_is_waiting_for_100_continue`. + +## Error handling { #error-handling } + +Given the vagaries of networks and the folks on the other side of them, it's +extremely important to be prepared for errors. + +Most errors in h11-mypyc are signaled by raising one of +[`ProtocolError`][h11_mypyc.ProtocolError]'s two concrete subclasses, +[`LocalProtocolError`][h11_mypyc.LocalProtocolError] and +[`RemoteProtocolError`][h11_mypyc.RemoteProtocolError]: + +::: h11_mypyc.ProtocolError + +::: h11_mypyc.LocalProtocolError + +::: h11_mypyc.RemoteProtocolError + +There are four cases where these exceptions might be raised: + +- When trying to instantiate an event object + ([`LocalProtocolError`][h11_mypyc.LocalProtocolError]): This indicates that something + about your event is invalid. Your event wasn't constructed, but there are no + other consequences -- feel free to try again. + +- When calling + [`Connection.start_next_cycle()`][h11_mypyc.Connection.start_next_cycle] + ([`LocalProtocolError`][h11_mypyc.LocalProtocolError]): This indicates that the + connection is not ready to be re-used, because one or both of the peers are not + in the `DONE` state. The [`Connection`][h11_mypyc.Connection] object remains usable, + and you can try again later. + +- When calling [`Connection.next_event()`][h11_mypyc.Connection.next_event] + ([`RemoteProtocolError`][h11_mypyc.RemoteProtocolError]): This indicates that the + remote peer has violated our protocol assumptions. This is unrecoverable -- we + don't know what they're doing and we cannot safely proceed. + [`Connection.their_state`][h11_mypyc.Connection.their_state] immediately becomes + `ERROR`, and all further calls to + [`next_event()`][h11_mypyc.Connection.next_event] will also raise + [`RemoteProtocolError`][h11_mypyc.RemoteProtocolError]. + [`Connection.send()`][h11_mypyc.Connection.send] still works as normal, so if you're + implementing a server and this happens then you have an opportunity to send + back a 400 Bad Request response. But aside from that, your only real option is + to close your socket and make a new connection. + +- When calling [`Connection.send()`][h11_mypyc.Connection.send] or + [`Connection.send_with_data_passthrough()`][h11_mypyc.Connection.send_with_data_passthrough] + ([`LocalProtocolError`][h11_mypyc.LocalProtocolError]): This indicates that *you* + violated our protocol assumptions. This is also unrecoverable -- h11-mypyc doesn't + know what you're doing, its internal state may be inconsistent, and we cannot + safely proceed. [`Connection.our_state`][h11_mypyc.Connection.our_state] immediately + becomes `ERROR`, and all further calls to + [`send()`][h11_mypyc.Connection.send] will also raise + [`LocalProtocolError`][h11_mypyc.LocalProtocolError]. The only thing you can + reasonably do at this point is to close your socket and make a new connection. + +So that's how h11-mypyc tells you about errors that it detects. In some cases, it's +also useful to be able to tell h11-mypyc about an error that you detected. In +particular, the [`Connection`][h11_mypyc.Connection] object assumes that after you call +[`Connection.send()`][h11_mypyc.Connection.send], you actually send that data to the +remote peer. But sometimes, for one reason or another, this doesn't actually +happen. + +Here's a concrete example. Suppose you're using h11-mypyc to implement an HTTP client +that keeps a pool of connections so it can re-use them when possible (see +[Re-using a connection](#keepalive-and-pipelining)). You take a connection from +the pool, and start to do a large upload... but then for some reason this gets +cancelled (maybe you have a GUI and a user clicked "cancel"). This can cause +h11-mypyc's model of this connection to diverge from reality: for example, h11-mypyc might +think that you successfully sent the full request, because you passed an +[`EndOfMessage`][h11_mypyc.EndOfMessage] object to +[`Connection.send()`][h11_mypyc.Connection.send], but in fact you didn't, because you +never sent the resulting bytes. And then -- here's the really tricky part! -- if +you're not careful, you might think that it's OK to put this connection back into +the connection pool and re-use it, because h11-mypyc is telling you that a full +request/response cycle was completed. But this is wrong; in fact you have to +close this connection and open a new one. + +The solution is simple: call +[`Connection.send_failed()`][h11_mypyc.Connection.send_failed], and now h11-mypyc knows that +your send failed. In this case, +[`Connection.our_state`][h11_mypyc.Connection.our_state] immediately becomes `ERROR`, +just like if you had tried to do something that violated the protocol. + +## Message body framing: `Content-Length` and all that { #framing } + +There are two different headers that HTTP/1.1 uses to indicate a framing +mechanism for request/response bodies: `Content-Length` and `Transfer-Encoding`. +Our general philosophy is that the way you tell h11-mypyc what configuration you want +to use is by setting the appropriate headers in your request / response, and then +h11-mypyc will both pass those headers on to the peer and encode the body +appropriately. + +Currently, the only supported `Transfer-Encoding` is `chunked`. + +On requests, this means: + +- No `Content-Length` or `Transfer-Encoding`: no body, equivalent to + `Content-Length: 0`. + +- `Content-Length: ...`: You're going to send exactly the specified number of + bytes. h11-mypyc will keep track and signal an error if your + [`EndOfMessage`][h11_mypyc.EndOfMessage] doesn't happen at the right place. + +- `Transfer-Encoding: chunked`: You're going to send a variable / not yet known + number of bytes. + + Note 1: only HTTP/1.1 servers are required to support + `Transfer-Encoding: chunked`, and as a client you have to decide whether to + send this header before you get to see what protocol version the server is + using. + + Note 2: even though HTTP/1.1 servers are required to support + `Transfer-Encoding: chunked`, this doesn't necessarily mean that they + actually do -- e.g., applications using Python's standard WSGI API cannot + accept chunked requests. + + Nonetheless, this is the only way to send a request where you don't know the + size of the body ahead of time, so if that's the situation you find yourself + in then you might as well try it and hope. + +On responses, things are a bit more subtle. There are effectively two cases: + +- `Content-Length: ...`: You're going to send exactly the specified number of + bytes. h11-mypyc will keep track and signal an error if your + [`EndOfMessage`][h11_mypyc.EndOfMessage] doesn't happen at the right place. + +- `Transfer-Encoding: chunked`, *or*, neither framing header is provided: These + two cases are handled differently at the wire level, but as far as the + application is concerned they provide (almost) exactly the same semantics: in + either case, you'll send a variable / not yet known number of bytes. The + difference between them is that `Transfer-Encoding: chunked` works better + (compatible with keep-alive, allows trailing headers, clearly distinguishes + between successful completion and network errors), but requires an HTTP/1.1 + client; for HTTP/1.0 clients the only option is the no-headers approach where + you have to close the socket to indicate completion. + + Since this is (almost) entirely a wire-level-encoding concern, h11-mypyc abstracts + it: when sending a response you can set either `Transfer-Encoding: chunked` + or leave off both framing headers, and h11-mypyc will treat both cases identically: + it will automatically pick the best option given the client's advertised HTTP + protocol level. + + You need to watch out for this if you're using trailing headers (i.e., a + non-empty `headers` attribute on [`EndOfMessage`][h11_mypyc.EndOfMessage]), since + trailing headers are only legal if we actually ended up using + `Transfer-Encoding: chunked`. Trying to send a non-empty set of trailing + headers to a HTTP/1.0 client will raise a + [`LocalProtocolError`][h11_mypyc.LocalProtocolError]. If this use case is important + to you, check `Connection.their_http_version` to confirm that the client + speaks HTTP/1.1 before you attempt to send any trailing headers. + +## Re-using a connection: keep-alive and pipelining { #keepalive-and-pipelining } + +HTTP/1.1 allows a connection to be re-used for multiple request/response cycles +(also known as "keep-alive"). This can make things faster by letting us skip the +costly connection setup, but it does create some complexities: we have to keep +track of whether a connection is reusable, and when there are multiple requests +and responses flowing through the same connection we need to be careful not to +get confused about which request goes with which response. + +h11-mypyc considers a connection to be reusable if, and only if, both sides (a) speak +HTTP/1.1 (HTTP/1.0 did have some complex and fragile support for keep-alive +bolted on, but h11-mypyc currently doesn't support that -- possibly this will be added +in the future), and (b) neither side has explicitly disabled keep-alive by +sending a `Connection: close` header. + +If you plan to make only a single request or response and then close the +connection, you should manually set the `Connection: close` header in your +request/response. h11-mypyc will notice and update its state appropriately. + +There are also some situations where you are required to send a +`Connection: close` header, e.g. if you are a server talking to a client that +doesn't support keep-alive. You don't need to worry about these cases -- h11-mypyc will +automatically add this header when necessary. Just worry about setting it when +it's actually something that you're actively choosing. + +If you want to re-use a connection, you have to wait until both the request and +the response have been completed, bringing both the client and server to the +`DONE` state. Once this has happened, you can explicitly call +[`Connection.start_next_cycle()`][h11_mypyc.Connection.start_next_cycle] to reset both +sides back to the `IDLE` state. This makes sure that the client and server remain +synched up. + +If keep-alive is disabled for whatever reason -- someone set `Connection: close`, +lack of protocol support, one of the sides just unilaterally closed the +connection -- then the state machines will skip past the `DONE` state directly to +the `MUST_CLOSE` or `CLOSED` states. In this case, trying to call +[`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle] will raise an error, and +the only thing you can legally do is to close this connection and make a new one. + +HTTP/1.1 also allows for a more aggressive form of connection re-use, in which a +client sends multiple requests in quick succession, and then waits for the +responses to stream back in order ("pipelining"). This is generally considered to +have been a bad idea, because it makes things like error recovery very +complicated. + +As a client, h11-mypyc does not support pipelining. This is enforced by the structure +of the state machine: after sending one [`Request`][h11_mypyc.Request], you can't send +another until after calling +[`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle], and you can't call +[`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle] until the server has +entered the `DONE` state, which requires reading the server's full response. + +As a server, h11-mypyc provides the minimal support for pipelining required to comply +with the HTTP/1.1 standard: if the client sends multiple pipelined requests, then +we handle the first request until we reach the `DONE` state, and then +[`next_event()`][h11_mypyc.Connection.next_event] will pause and refuse to parse any +more events until the response is completed and +[`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle] is called. See the next +section for more details. + +## Flow control { #flow-control } + +Presumably you know when you want to send things, and the +[`send()`][h11_mypyc.Connection.send] interface is very simple: it just immediately +returns all the data you need to send for the given event, so you can apply +whatever send buffer strategy you want. But reading from the remote peer is a bit +trickier: you don't want to read data from the remote peer if it can't be +processed (i.e., you want to apply backpressure and avoid building arbitrarily +large in-memory buffers), and you definitely don't want to block waiting on data +from the remote peer at the same time that it's blocked waiting for you, because +that will cause a deadlock. + +One complication here is that if you're implementing a server, you have to be +prepared to handle [`Request`][h11_mypyc.Request]s that have an `Expect: 100-continue` +header. You can +[read the spec](https://tools.ietf.org/html/rfc7231#section-5.1.1) for the full +details, but basically what this header means is that after sending the +[`Request`][h11_mypyc.Request], the client plans to pause and wait until they see some +response from the server before they send that request's [`Data`][h11_mypyc.Data]. The +server's response would normally be an +[`InformationalResponse`][h11_mypyc.InformationalResponse] with status `100 Continue`, +but it could be anything really (e.g. a full [`Response`][h11_mypyc.Response] with a +4xx status code). The crucial thing as a server, though, is that you should never +block trying to read a request body if the client is blocked waiting for you to +tell them to send the request body. + +Fortunately, h11-mypyc makes this easy, because it tracks whether the client is in the +waiting-for-100-continue state, and exposes this as +`Connection.they_are_waiting_for_100_continue`. So you don't have to pay +attention to the `Expect` header yourself; you just have to make sure that before +you block waiting to read a request body, you execute some code like: + +```python +if conn.they_are_waiting_for_100_continue: + do_send(conn, h11_mypyc.InformationalResponse(100, headers=[...])) +do_read(...) +``` + +In fact, if you're lazy (and what programmer isn't?) then you can just do this +check before all reads -- it's mandatory before blocking to read a request body, +but it's safe at any time. + +And the other thing you want to pay attention to is the special values that +[`next_event()`][h11_mypyc.Connection.next_event] might return: `NEED_DATA` and +`PAUSED`. + +`NEED_DATA` is what it sounds like: it means that +[`next_event()`][h11_mypyc.Connection.next_event] is guaranteed not to return any more +real events until you've called +[`receive_data()`][h11_mypyc.Connection.receive_data] at least once. + +`PAUSED` is a little more subtle: it means that +[`next_event()`][h11_mypyc.Connection.next_event] is guaranteed not to return any more +real events until something else has happened to clear up the paused state. There +are three cases where this can happen: + +1. We received a full request/response from the remote peer, and then we received + some more data after that. (The main situation where this might happen is a + server responding to a pipelining client.) The `PAUSED` state will go away + after you call + [`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle]. + +2. A successful `CONNECT` or `Upgrade:` request has caused the connection to + switch to some other protocol (see + [Switching protocols](#switching-protocols)). This `PAUSED` state is + permanent; you should abandon this [`Connection`][h11_mypyc.Connection] and go do + whatever it is you're going to do with your new protocol. + +3. We're a server, and the client we're talking to proposed to switch protocols + (see [Switching protocols](#switching-protocols)), and now is waiting to find + out whether their request was successful or not. Once we either accept or deny + their request then this will turn into one of the above two states, so you + probably don't need to worry about handling it specially. + +Putting all this together -- + +If your I/O is organized around a "pull" strategy, where your code requests +events as it's ready to handle them (e.g. classic synchronous code, or asyncio's +`await loop.sock_recv(...)`, or +[Trio's streams](https://trio.readthedocs.io/en/latest/reference-io.html#the-abstract-stream-api)), +then you'll probably want logic that looks something like: + +```python +# Replace do_sendall and do_recv with your I/O code +def get_next_event(): + while True: + event = conn.next_event() + if event is h11_mypyc.NEED_DATA: + if conn.they_are_waiting_for_100_continue: + do_sendall(conn, h11_mypyc.InformationalResponse(100, ...)) + conn.receive_data(do_recv()) + continue + return event +``` + +And then your code that calls this will need to make sure to call it only at +appropriate times (e.g., not immediately after receiving +[`EndOfMessage`][h11_mypyc.EndOfMessage] or `PAUSED`). + +If your I/O is organized around a "push" strategy, where the network drives +processing (e.g. you're using [Twisted](https://twistedmatrix.com/), or +implementing an [`asyncio.Protocol`][]), then you'll want to internally apply +back-pressure whenever you see `PAUSED`, remove back-pressure when you call +[`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle], and otherwise just +deliver events as they arrive. Something like: + +```python +class HTTPProtocol(asyncio.Protocol): + # Save the transport for later -- needed to access the + # backpressure API. + def connection_made(self, transport): + self._transport = transport + + # Internal helper function -- deliver all pending events + def _deliver_events(self): + while True: + event = self.conn.next_event() + if event is h11_mypyc.NEED_DATA: + break + elif event is h11_mypyc.PAUSED: + # Apply back-pressure + self._transport.pause_reading() + break + else: + self.event_received(event) + + # Called by "someone" whenever new data appears on our socket + def data_received(self, data): + self.conn.receive_data(data) + self._deliver_events() + + # Called by "someone" whenever the peer closes their socket + def eof_received(self): + self.conn.receive_data(b"") + self._deliver_events() + # asyncio will close our socket unless we return True here. + return True + + # Called by your code when it's ready to start a new + # request/response cycle + def start_next_cycle(self): + self.conn.start_next_cycle() + # New events might have been buffered internally, and only + # become deliverable after calling start_next_cycle + self._deliver_events() + # Remove back-pressure + self._transport.resume_reading() + + # Fill in your code here + def event_received(self, event): + ... +``` + +And your code that uses this will have to remember to check for +`they_are_waiting_for_100_continue` at the appropriate time. + +## Closing connections { #closing } + +h11-mypyc represents a connection shutdown with the special event type +[`ConnectionClosed`][h11_mypyc.ConnectionClosed]. You can send this event, in which +case [`send()`][h11_mypyc.Connection.send] will simply update the state machine and +then return `None`. You can receive this event, if you call +`conn.receive_data(b"")`. (The actual receipt might be delayed if the connection +is [paused](#flow-control).) It's safe and legal to call +`conn.receive_data(b"")` multiple times, and once you've done this once, then all +future calls to [`receive_data()`][h11_mypyc.Connection.receive_data] will also return +`ConnectionClosed()`: + +```pycon +>>> conn = h11_mypyc.Connection(our_role=h11_mypyc.CLIENT) +>>> conn.receive_data(b"") +>>> conn.receive_data(b"") +>>> conn.receive_data(None) +``` + +(Or if you try to actually pass new data in after calling +`conn.receive_data(b"")`, that will raise an exception.) + +h11-mypyc is careful about interpreting connection closure in a *half-duplex fashion*. +TCP sockets pretend to be a two-way connection, but really they're two one-way +connections. In particular, it's possible for one party to shut down their +sending connection -- which causes the other side to be notified that the +connection has closed via the usual `socket.recv(...) -> b""` mechanism -- while +still being able to read from their receiving connection. (On Unix, this is +generally accomplished via the `shutdown(2)` system call.) So, for example, a +client could send a request, and then close their socket for writing to indicate +that they won't be sending any more requests, and then read the response. It's +this kind of closure that is indicated by h11-mypyc's +[`ConnectionClosed`][h11_mypyc.ConnectionClosed]: it means that this party will not be +sending any more data -- nothing more, nothing less. You can see this reflected +in the [state machine](#state-machine), in which one party transitioning to +`CLOSED` doesn't immediately halt the connection, but merely prevents it from +continuing for another request/response cycle. + +The state machine also indicates that +[`ConnectionClosed`][h11_mypyc.ConnectionClosed] events can only happen in certain +states. This isn't true, of course -- any party can close their connection at any +time, and h11-mypyc can't stop them. But what h11-mypyc can do is distinguish between clean +and unclean closes. For example, if both sides complete a request/response cycle +and then close the connection, that's a clean closure and everyone will transition +to the `CLOSED` state in an orderly fashion. On the other hand, if one party +suddenly closes the connection while they're in the middle of sending a chunked +response body, or when they promised a `Content-Length:` of 1000 bytes but have +only sent 500, then h11-mypyc knows that this is a violation of the HTTP protocol, and +will raise a [`ProtocolError`][h11_mypyc.ProtocolError]. Basically h11-mypyc treats an +unexpected close the same way it would treat unexpected, uninterpretable data +arriving -- it lets you know that something has gone wrong. + +As a client, the proper way to perform a single request and then close the +connection is: + +1. Send a [`Request`][h11_mypyc.Request] with `Connection: close` + +2. Send the rest of the request body + +3. Read the server's [`Response`][h11_mypyc.Response] and body + +4. `conn.our_state is h11_mypyc.MUST_CLOSE` will now be true. Call + `conn.send(ConnectionClosed())` and then close the socket. Or really you could + just close the socket -- the thing calling `send` will do is raise an error if + you're not in `MUST_CLOSE` as expected. So it's between you and your conscience + and your code reviewers. + +(Technically it would also be legal to shutdown your socket for writing as step +2.5, but this doesn't serve any purpose and some buggy servers might get annoyed, +so it's not recommended.) + +As a server, the proper way to perform a response is: + +1. Send your [`Response`][h11_mypyc.Response] and body + +2. Check if `conn.our_state is h11_mypyc.MUST_CLOSE`. This might happen for a variety + of reasons; for example, if the response had unknown length and the client + speaks only HTTP/1.0, then the client will not consider the connection + complete until we issue a close. + +You should be particularly careful to take into consideration the following note +from [RFC 7230 section 6.6](https://tools.ietf.org/html/rfc7230#section-6.6): + +> If a server performs an immediate close of a TCP connection, there is a +> significant risk that the client will not be able to read the last HTTP +> response. If the server receives additional data from the client on a fully +> closed connection, such as another request that was sent by the client before +> receiving the server's response, the server's TCP stack will send a reset packet +> to the client; unfortunately, the reset packet might erase the client's +> unacknowledged input buffers before they can be read and interpreted by the +> client's HTTP parser. +> +> To avoid the TCP reset problem, servers typically close a connection in stages. +> First, the server performs a half-close by closing only the write side of the +> read/write connection. The server then continues to read from the connection +> until it receives a corresponding close by the client, or until the server is +> reasonably certain that its own TCP stack has received the client's +> acknowledgement of the packet(s) containing the server's last response. Finally, +> the server fully closes the connection. + +## Switching protocols { #switching-protocols } + +h11-mypyc supports two kinds of "protocol switches": requests with method `CONNECT`, +and the newer `Upgrade:` header, most commonly used for negotiating WebSocket +connections. Both follow the same pattern: the client proposes that they switch +from regular HTTP to some other kind of interaction, and then the server either +rejects the suggestion -- in which case we return to regular HTTP rules -- or +else accepts it. (For `CONNECT`, acceptance means a response with 2xx status +code; for `Upgrade:`, acceptance means an +[`InformationalResponse`][h11_mypyc.InformationalResponse] with status +`101 Switching Protocols`.) If the proposal is accepted, then both sides switch +to doing something else with their socket, and h11-mypyc's job is done. + +As a developer using h11-mypyc, it's your responsibility to send and interpret the +actual `CONNECT` or `Upgrade:` request and response, and to figure out what to do +after the handover; it's h11-mypyc's job to understand what's going on, and help you +make the handover smoothly. + +Specifically, what h11-mypyc does is [pause](#flow-control) parsing incoming data at +the boundary between the two protocols, and then you can retrieve any unprocessed +data from the [`Connection.trailing_data`][h11_mypyc.Connection.trailing_data] +attribute. + +## Support for `sendfile()` { #sendfile } + +Many networking APIs provide some efficient way to send particular data, e.g. +asking the operating system to stream files directly off of the disk and into a +socket without passing through userspace. + +It's possible to use these APIs together with h11-mypyc. The basic strategy is: + +- Create some placeholder object representing the special data, that your + networking code knows how to "send" by invoking whatever the appropriate + underlying APIs are. + +- Make sure your placeholder object implements a `__len__` method returning its + size in bytes. + +- Call `conn.send_with_data_passthrough(Data(data=))` + +- This returns a list whose contents are a mixture of (a) bytes-like objects, and + (b) your placeholder object. You should send them to the network in order. + +Here's a sketch of what this might look like: + +```python +class FilePlaceholder: + def __init__(self, file, offset, count): + self.file = file + self.offset = offset + self.count = count + + def __len__(self): + return self.count + +def send_data(sock, data): + if isinstance(data, FilePlaceholder): + # socket.sendfile added in Python 3.5 + sock.sendfile(data.file, data.offset, data.count) + else: + # data is a bytes-like object to be sent directly + sock.sendall(data) + +placeholder = FilePlaceholder(open("...", "rb"), 0, 200) +for data in conn.send_with_data_passthrough(Data(data=placeholder)): + send_data(sock, data) +``` + +This works with all the different framing modes (`Content-Length`, +`Transfer-Encoding: chunked`, etc.) -- h11-mypyc will add any necessary framing data, +update its internal state, and away you go. + +## Identifying h11-mypyc in requests and responses + +According to RFC 7231, client requests are supposed to include a `User-Agent:` +header identifying what software they're using, and servers are supposed to +respond with a `Server:` header doing the same. h11-mypyc doesn't construct these +headers for you, but to make it easier for you to construct this header, it +provides `h11_mypyc.PRODUCT_ID`: a string suitable for identifying the current version +of h11-mypyc in a `User-Agent:` or `Server:` header. + +The version of h11-mypyc that was used to build these docs identified itself as: + +```pycon +>>> h11_mypyc.PRODUCT_ID +'python-h11-mypyc/0.17.0' +``` + +## Chunked Transfer Encoding Delimiters { #chunk-delimiters-are-bad } + +!!! info "Added in version 0.7.0" + +HTTP/1.1 allows for the use of Chunked Transfer Encoding to frame request and +response bodies. This form of transfer encoding allows the implementation to +provide its body data in the form of length-prefixed "chunks" of data. + +RFC 7230 is extremely clear that the breaking points between chunks of data are +non-semantic: that is, users should not rely on them or assign any meaning to +them. This is particularly important given that RFC 7230 also allows +intermediaries such as proxies and caches to change the chunk boundaries as they +see fit, or even to remove the chunked transfer encoding entirely. + +However, for some applications it is valuable or essential to see the chunk +boundaries because the peer implementation has assigned meaning to them. While +this is against the specification, if you do really need access to this +information h11-mypyc makes it available to you in the form of the `Data.chunk_start` +and `Data.chunk_end` properties of the [`Data`][h11_mypyc.Data] event. + +`Data.chunk_start` is set to `True` for the first [`Data`][h11_mypyc.Data] event for a +given chunk of data. `Data.chunk_end` is set to `True` for the last +[`Data`][h11_mypyc.Data] event that is emitted for a given chunk of data. h11-mypyc +guarantees that it will always emit at least one [`Data`][h11_mypyc.Data] event for +each chunk of data received from the remote peer, but due to its internal +buffering logic it may return more than one. It is possible for a single +[`Data`][h11_mypyc.Data] event to have both `Data.chunk_start` and `Data.chunk_end` set +to `True`, in which case it will be the only [`Data`][h11_mypyc.Data] event for that +chunk of data. + +Again, it is *strongly encouraged* that you avoid relying on this information if +at all possible. This functionality should be considered an escape hatch for when +there is no alternative but to rely on the information, rather than a general +source of data that is worth relying on. diff --git a/docs/src/basic-usage.md b/docs/src/basic-usage.md new file mode 100644 index 0000000..5a81b27 --- /dev/null +++ b/docs/src/basic-usage.md @@ -0,0 +1,372 @@ +# Getting started: writing your own HTTP/1.1 client + +h11-mypyc can be used to implement both HTTP/1.1 clients and servers. To give a flavor +for how the API works, we'll demonstrate a small client. + +## HTTP basics + +An HTTP interaction always starts with a client sending a *request*, optionally +some *data* (e.g., a POST body); and then the server responds with a *response* +and optionally some *data* (e.g. the requested document). Requests and responses +have some data associated with them: for requests, this is a method (e.g. `GET`), +a target (e.g. `/index.html`), and a collection of headers (e.g. +`User-agent: demo-client`). For responses, it's a status code (e.g. 404 Not +Found) and a collection of headers. + +Of course, as far as the network is concerned, there's no such thing as +"requests" and "responses" -- there's just bytes being sent from one computer to +another. Let's see what this looks like, by fetching +: + +```pycon +>>> import ssl, socket +>>> ctx = ssl.create_default_context() +... sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), +... server_hostname="httpbin.org") +>>> # Send request +... sock.sendall(b"GET /xml HTTP/1.1\r\nhost: httpbin.org\r\n\r\n") +>>> # Read response +... response_data = sock.recv(1024) +>>> # Let's see what we got! +... print(response_data) +b'HTTP/1.1 200 OK\r\nDate: Tue, 25 Aug 2026 08:39:31 GMT\r\nContent-Type: application/xml\r\nContent-Length: 522\r\nConnection: keep-alive\r\nServer: gunicorn/19.9.0\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Credentials: true\r\n\r\n\n\n\n\n\n\n \n \n Wake up to WonderWidgets!\n \n\n \n \n Overview\n Why WonderWidgets are great\n \n Who buys WonderWidgets\n \n\n' +``` + +!!! warning + + If you try to reproduce these examples interactively, then you'll have the + most luck if you paste them in all at once. Remember we're talking to a + remote server here -- if you type them in one at a time, and you're too slow, + then the server might give up on waiting for you and close the connection. + One way to recognize that this has happened is if `response_data` comes back + as an empty string, or later on when we're working with h11-mypyc this might cause + errors that mention `ConnectionClosed`. + +So that's, uh, very convenient and readable. It's a little more understandable if +we print the bytes as text: + +```pycon +>>> print(response_data.decode("ascii")) +HTTP/1.1 200 OK +Date: Tue, 25 Aug 2026 08:39:31 GMT +Content-Type: application/xml +Content-Length: 522 +Connection: keep-alive +Server: gunicorn/19.9.0 +Access-Control-Allow-Origin: * +Access-Control-Allow-Credentials: true + + + + + + + + + + Wake up to WonderWidgets! + + + + + Overview + Why WonderWidgets are great + + Who buys WonderWidgets + + + +``` + +Here we can see the status code at the top (200, which is the code for "OK"), +followed by the headers, followed by the data (a silly little XML document). But +we can already see that working with bytes by hand like this is really +cumbersome. What we need to do is to move up to a higher level of abstraction. + +This is what h11-mypyc does. Instead of talking in bytes, it lets you talk in +high-level HTTP "events". To see what this means, let's repeat the above +exercise, but using h11-mypyc. We start by making a TLS connection like before, but now +we'll also import `h11_mypyc`, and create a [`h11_mypyc.Connection`][h11_mypyc.Connection] object: + +```pycon +>>> import ssl, socket +... import h11_mypyc +>>> ctx = ssl.create_default_context() +... sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), +... server_hostname="httpbin.org") +>>> conn = h11_mypyc.Connection(our_role=h11_mypyc.CLIENT) +``` + +Next, to send an event to the server, there are three steps we have to take. +First, we create an object representing the event we want to send -- in this +case, a [`h11_mypyc.Request`][h11_mypyc.Request]: + +```pycon +>>> request = h11_mypyc.Request(method="GET", +... target="/xml", +... headers=[("Host", "httpbin.org")]) +``` + +Next, we pass this to our connection's [`send()`][h11_mypyc.Connection.send] method, +which gives us back the bytes corresponding to this message: + +```pycon +>>> bytes_to_send = conn.send(request) +``` + +And then we send these bytes across the network: + +```pycon +>>> sock.sendall(bytes_to_send) +``` + +There's nothing magical here -- these are the same bytes that we sent up above: + +```pycon +>>> bytes_to_send +b'GET /xml HTTP/1.1\r\nHost: httpbin.org\r\n\r\n' +``` + +Why doesn't h11-mypyc go ahead and send the bytes for you? Because it's designed to be +usable no matter what socket API you're using -- doesn't matter if it's +synchronous like this, asynchronous, callback-based, whatever; if you can read +and write bytes from the network, then you can use h11-mypyc. + +In this case, we're not quite done yet -- we have to send another event to tell +the other side that we're finished, which we do by sending an +[`EndOfMessage`][h11_mypyc.EndOfMessage] event: + +```pycon +>>> end_of_message_bytes_to_send = conn.send(h11_mypyc.EndOfMessage()) +... sock.sendall(end_of_message_bytes_to_send) +``` + +Of course, it turns out that in this case, the HTTP/1.1 specification tells us +that any request that doesn't contain either a `Content-Length` or +`Transfer-Encoding` header automatically has a 0 length body, and h11-mypyc knows that, +and h11-mypyc knows that the server knows that, so it actually encoded the +[`EndOfMessage`][h11_mypyc.EndOfMessage] event as the empty string: + +```pycon +>>> end_of_message_bytes_to_send +b'' +``` + +But there are other cases where it might not, depending on what headers are set, +what message is being responded to, the HTTP version of the remote peer, etc. +etc. So for consistency, h11-mypyc requires that you *always* finish your messages by +sending an explicit [`EndOfMessage`][h11_mypyc.EndOfMessage] event; then it keeps track +of the details of what that actually means in any given situation, so that you +don't have to. + +Finally, we have to read the server's reply. By now you can probably guess how +this is done, at least in the general outline: we read some bytes from the +network, then we hand them to the connection (using +[`Connection.receive_data()`][h11_mypyc.Connection.receive_data]) and it converts them +into events (using [`Connection.next_event()`][h11_mypyc.Connection.next_event]). + +```pycon +>>> bytes_received = sock.recv(1024) +... conn.receive_data(bytes_received) +>>> conn.next_event() +Response(headers=, http_version=b'1.1', reason=b'OK', status_code=200) +>>> conn.next_event() +Data(data=bytearray(b'\n\n\n\n\n\n \n \n Wake up to WonderWidgets!\n \n\n \n \n Overview\n Why WonderWidgets are great\n \n Who buys WonderWidgets\n \n\n'), chunk_start=False, chunk_end=False) +>>> conn.next_event() +EndOfMessage(headers=) +``` + +(Remember, if you're following along and get an error here mentioning +`ConnectionClosed`, then try again, but going through the steps faster!) + +Here the server sent us three events: a [`Response`][h11_mypyc.Response] object, which +is similar to the [`Request`][h11_mypyc.Request] object that we created earlier and has +the response's status code (200 OK) and headers; a [`Data`][h11_mypyc.Data] object +containing the response data; and another [`EndOfMessage`][h11_mypyc.EndOfMessage] +object. This similarity between what we send and what we receive isn't +accidental: if we were using h11-mypyc to write an HTTP server, then these are the +objects we would have created and passed to +[`send()`][h11_mypyc.Connection.send] -- h11-mypyc in client and server mode has an API +that's almost exactly symmetric. + +One thing we have to deal with, though, is that an entire response doesn't always +arrive in a single call to [`socket.recv()`][socket.socket.recv] -- sometimes the +network will decide to trickle it in at its own pace, in multiple pieces. Let's +try that again: + +```pycon +>>> import ssl, socket +... import h11_mypyc +>>> ctx = ssl.create_default_context() +... sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), +... server_hostname="httpbin.org") +>>> conn = h11_mypyc.Connection(our_role=h11_mypyc.CLIENT) +... request = h11_mypyc.Request(method="GET", +... target="/xml", +... headers=[("Host", "httpbin.org")]) +... sock.sendall(conn.send(request)) +``` + +and this time, we'll read in chunks of 200 bytes, to see how h11-mypyc handles it: + +```pycon +>>> bytes_received = sock.recv(200) +... conn.receive_data(bytes_received) +>>> conn.next_event() + +``` + +`NEED_DATA` is a special value that indicates that we, well, need more data. h11-mypyc +has buffered the first chunk of data; let's read some more: + +```pycon +>>> bytes_received = sock.recv(200) +... conn.receive_data(bytes_received) +>>> conn.next_event() +Response(headers=, http_version=b'1.1', reason=b'OK', status_code=200) +``` + +Now it's managed to read a complete [`Response`][h11_mypyc.Response]. + +## A basic client object + +Now let's use what we've learned to wrap up our socket and +[`Connection`][h11_mypyc.Connection] into a single object with some convenience +methods: + +```python title="myclient.py" +--8<-- "myclient.py" +``` + +And then we can send requests: + +```pycon +>>> client = MyHttpClient("httpbin.org", 443) +>>> client.send(h11_mypyc.Request(method="GET", target="/xml", +... headers=[("Host", "httpbin.org")])) +... client.send(h11_mypyc.EndOfMessage()) +``` + +And read back the events: + +```pycon +>>> client.next_event() +Response(headers=, http_version=b'1.1', reason=b'OK', status_code=200) +>>> client.next_event() +Data(data=bytearray(b'\n\n\n\n\n\n \n \n Wake up to WonderWidgets!\n \n\n \n \n Overview\n Why WonderWid'), chunk_start=False, chunk_end=False) +>>> client.next_event() +Data(data=bytearray(b'gets are great\n \n Who buys WonderWidgets\n \n\n'), chunk_start=False, chunk_end=False) +>>> client.next_event() +EndOfMessage(headers=) +``` + +Now we can see why [`EndOfMessage`][h11_mypyc.EndOfMessage] is so important -- +otherwise, we can't tell when we've received the end of the data. And since +that's the end of this response, the server won't send us anything more until we +make another request -- if we try, then the socket read will just hang forever, +unless we set a timeout or interrupt it: + +```pycon +>>> client.sock.settimeout(2) +... client.next_event() +Traceback (most recent call last): + ... +TimeoutError: The read operation timed out +``` + +## Keep-alive + +For some servers, we'd have to stop here, because they require a new connection +for every request/response. But, this server is smarter than that -- it supports +[keep-alive](https://en.wikipedia.org/wiki/HTTP_persistent_connection), so we can +re-use this connection to send another request. There's a few ways we can tell. +First, if it didn't, then it would have closed the connection already, and we +would have gotten a [`ConnectionClosed`][h11_mypyc.ConnectionClosed] event on our last +call to [`next_event()`][h11_mypyc.Connection.next_event]. We can also tell by checking +h11-mypyc's internal idea of what state the two sides of the conversation are in: + +```pycon +>>> client.conn.our_state, client.conn.their_state +(, ) +``` + +If the server didn't support keep-alive, then these would be `MUST_CLOSE` and +either `MUST_CLOSE` or `CLOSED`, respectively (depending on whether we'd seen the +socket actually close yet). `DONE` / `DONE`, on the other hand, means that this +request/response cycle has totally finished, but the connection itself is still +viable, and we can start over and send a new request on this same connection. + +To do this, we tell h11-mypyc to get ready (this is needed as a safety measure to make +sure different requests/responses on the same connection don't get accidentally +mixed up): + +```pycon +>>> client.conn.start_next_cycle() +``` + +This resets both sides back to their initial `IDLE` state, allowing us to send +another [`Request`][h11_mypyc.Request]: + +```pycon +>>> client.conn.our_state, client.conn.their_state +(, ) +>>> client.send(h11_mypyc.Request(method="GET", target="/get", +... headers=[("Host", "httpbin.org")])) +... client.send(h11_mypyc.EndOfMessage()) +>>> client.next_event() +Response(headers=, http_version=b'1.1', reason=b'OK', status_code=200) +``` + +## What's next? + +Here's some ideas of things you might try: + +- Adapt the above examples to make a POST request. (Don't forget to set the + `Content-Length` header -- but don't worry, if you do forget, then h11-mypyc will + give you an error when you try to send data): + + ```python + client.send(h11_mypyc.Request(method="POST", target="/post", + headers=[("Host", "httpbin.org"), + ("Content-Length", "10")])) + client.send(h11_mypyc.Data(data=b"1234567890")) + client.send(h11_mypyc.EndOfMessage()) + ``` + +- Experiment with what happens if you try to violate the HTTP protocol by sending + a [`Response`][h11_mypyc.Response] as a client, or sending two + [`Request`][h11_mypyc.Request]s in a row. + +- Write your own basic `http_get` function that takes a URL, parses out the + host/port/path, then connects to the server, does a `GET` request, and then + collects up all the resulting [`Data`][h11_mypyc.Data] objects, concatenates their + payloads, and returns it. + +- Adapt the above code to use your favorite non-blocking API. + +- Use h11-mypyc to write a simple HTTP server. (If you get stuck, + [here's an example](https://github.com/danfimov/h11/blob/master/examples/trio-server.py).) + +And of course, you'll want to read the [API documentation](api.md) for all the +details. diff --git a/docs/src/changes.md b/docs/src/changes.md new file mode 100644 index 0000000..b493ce8 --- /dev/null +++ b/docs/src/changes.md @@ -0,0 +1,303 @@ +# History of changes + +## H11 MyPyc 0.17.0 (2026-08-25) + +First release of the fork. Everything below is relative to upstream h11 0.16.0; +the protocol behaviour is unchanged, and the whole upstream test suite passes against both the interpreted and the compiled build. + +### Compiled builds + +- The package is compiled with [mypyc](https://mypyc.readthedocs.io/). On the + benchmark suite this is worth about **1.9x** (27,700 → 52,900 requests/sec on the reference machine). +- Both a compiled wheel and a pure-Python `py3-none-any` wheel are published. + Installers pick the compiled build when a wheel matches the interpreter and + fall back to the pure one otherwise, so PyPy and platforms without a wheel + keep working. No configuration is needed on the consuming side. +- Building from an sdist produces the pure-Python build unless `H11_MYPYC=1` is set, so a missing compiler never breaks an install. + +### Backwards-incompatible changes + +- **The import name is `h11_mypyc`, not `h11`.** The distribution deliberately + does not ship an `h11/` directory: two distributions writing to the same + directory overwrite each other silently, and uninstalling either one takes + the other's files with it. Under the new name the fork and upstream h11 can be installed side by side. +- `PRODUCT_ID` is now `python-h11-mypyc/` rather than + `python-h11/`, so the fork does not report itself as upstream in `User-Agent:` and `Server:` headers. +- The sentinels (`NEED_DATA`, `PAUSED`, `IDLE`, ...) are no longer instances of + themselves: `type(h11_mypyc.NEED_DATA) is h11_mypyc.NEED_DATA` is now false. + That property came from a custom metaclass, which mypyc cannot compile. + `is` comparisons, dict keys and `Type[Sentinel]` annotations are unaffected. +- `Headers` no longer inherits from `collections.abc.Sequence`; it is registered + as a virtual subclass instead, so `isinstance(headers, Sequence)` still holds. + A compiled class inheriting an ABC shares the base's `isinstance` cache, which + silently corrupts `isinstance` in both directions across the process. +- `Event` no longer inherits from `abc.ABC`, and the event classes use + `@dataclass(slots=True)` rather than a hand-written `__slots__`. The events are + still frozen, still without `__dict__`, and still work with + `dataclasses.fields()` and `dataclasses.replace()`. +- Several annotations were widened to match what the code has always accepted at + runtime, because mypyc turns annotations into runtime checks: + `Data.data` and `Response.status_code` are now `Any` (to keep the documented + sendfile pass-through and the `LocalProtocolError` for a non-integer status + code), and the event constructors accept any bytes-like object. +- `validate()` no longer returns the match groups; use `match_or_raise()` or + `validate_and_group()` for those. +- Requires Python 3.10 or newer. + +### Performance + +Most of the work below helps the pure-Python build too, which is about 17% faster than upstream on the same benchmark: + +- Header names and values are checked with byte-class tests instead of regexes. + The equivalence to the ABNF patterns is pinned by a differential test. +- `_obsolete_line_fold()` no longer runs a regex on every header line; obs-fold continuations are detected from the first byte. +- Header parsing no longer builds a dict per header line just to read two fields out of it. +- Sentinel and event classes are `@final`, which lets mypyc call their methods directly rather than through a vtable. + +### Miscellaneous internal changes + +- Packaging metadata moved from `setup.py` to `pyproject.toml`; `setup.py` remains only to declare the mypyc extension modules. +- Added a benchmark suite (`tests/test_benchmarks.py`, run with `make bench-py`) covering header parsing, the state machine and whole request/response cycles. + +## H11 0.16.0 (2025-04-23) + +### Security fix + +Reject certain malformed `Transfer-Encoding: chunked` bodies that were previously accepted. These could have enabled request-smuggling attacks when an h11-based HTTP server was placed behind a load balancer with a matching bug in its `chunked` handling. + +Advisory with more details: https://github.com/python-hyper/h11/security/advisories/GHSA-vqfr-h8mv-ghfj + +Reported by: Jeppe Bonde Weikop + +## H11 0.15.0 (2025-04-23) + +### Bugfixes + +- Reject Content-Lengths >= 1 zettabyte (1 billion terabytes) early, [without attempting to parse the integer](https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation) ([#181](https://github.com/python-hyper/h11/issues/181)) + +### Miscellaneous internal changes + +- Remove the `tests` folder from wheel files. This reduces the zipped file size by 20KB (about 30%). ([#158](https://github.com/python-hyper/h11/issues/158)) + +## H11 0.14.0 (2022-09-25) + +### Features + +- Allow additional trailing whitespace in chunk headers for additional + compatibility with existing servers. ([#133](https://github.com/python-hyper/h11/issues/133)) +- Improve the type hints for Sentinel types, which should make it + easier to type hint h11 usage. ([#151](https://github.com/python-hyper/h11/pull/151) & [#144](https://github.com/python-hyper/h11/pull/144))) + +### Deprecations and Removals + +- Python 3.6 support is removed. h11 now requires Python>=3.7 + including PyPy 3. Users running `pip install h11` on Python 2 will + automatically get the last Python 2-compatible version. ([#138](https://github.com/python-hyper/h11/issues/138)) + +## v0.13.0 (2022-01-19) + +### Features + +- Clarify that the Headers class is a Sequence and inherit from the + collections Sequence abstract base class to also indicate this (and + gain the mixin methods). See also #104. ([#112](https://github.com/python-hyper/h11/issues/112)) +- Switch event classes to dataclasses for easier typing and slightly + improved performance. ([#124](https://github.com/python-hyper/h11/issues/124)) +- Shorten traceback of protocol errors for easier readability ([#132](https://github.com/python-hyper/h11/pull/132)). +- Add typing including a PEP 561 marker for usage by type checkers + ([#135](https://github.com/python-hyper/h11/pull/135)). +- Expand the allowed status codes to [0, 999] from [0, 600] ([#134](https://github.com/python-hyper/h11/issues/134)). + +### Backwards **in**compatible changes + +- Ensure request method is a valid token ([#141](https://github.com/python-hyper/h11/pull/141)). + +## v0.12.0 (2021-01-01) + +### Features + +- Added support for servers with broken line endings. + + After this change h11 accepts both `\r\n` and `\n` as a headers + delimiter. ([#7](https://github.com/python-hyper/h11/issues/7)) +- Add early detection of invalid http data when request line starts + with binary ([#122](https://github.com/python-hyper/h11/issues/122)) + +### Deprecations and Removals + +- Python 2.7 and PyPy 2 support is removed. h11 now requires + Python>=3.6 including PyPy 3. Users running `pip install h11` on + Python 2 will automatically get the last Python 2-compatible + version. ([#114](https://github.com/python-hyper/h11/issues/114)) + +## v0.11.0 (2020-10-05) + +New features: + +- h11 now stores and makes available the raw header name as + received. In addition h11 will write out header names with the same + casing as passed to it. This allows compatibility with systems that + expect titlecased header names. See [#31](https://github.com/python-hyper/h11/issues/31). +- Multiple content length headers are now merged into a single header + if all the values are equal, if any are unequal a LocalProtocol + error is raised (as before). See [#92](https://github.com/python-hyper/h11/issues/92). + +Backwards **in**compatible changes: + +- Headers added by h11, rather than passed to it, now have titlecased + names. Whilst this should help compatibility it replaces the + previous lowercased header names. + +## v0.10.0 (2020-08-14) + +Other changes: + +- Drop support for Python 3.4. +- Support Python 3.8. +- Make error messages returned by match failures less ambiguous ([#98](https://github.com/python-hyper/h11/issues/98)). + +## v0.9.0 (2019-05-15) + +Bug fixes: + +- Allow a broader range of characters in header values. This violates + the RFC, but is apparently required for compatibility with + real-world code, like Google Analytics cookies ([#57](https://github.com/python-hyper/h11/issues/57), [#58](https://github.com/python-hyper/h11/issues/58)). +- Validate incoming and outgoing request paths for invalid + characters. This prevents a variety of potential security issues + that have affected other HTTP clients. ([#69](https://github.com/python-hyper/h11/pull/69)). +- Force status codes to be integers, thereby allowing stdlib + HTTPStatus IntEnums to be used when constructing responses ([#72](https://github.com/python-hyper/h11/issues/72)). + +Other changes: + +- Make all sentinel values inspectable by IDEs, and split + `SEND_BODY_DONE` into `SEND_BODY`, and `DONE` ([#75](https://github.com/python-hyper/h11/pull/75)). +- Drop support for Python 3.3. +- LocalProtocolError raised in start_next_cycle now shows states for + more informative errors ([#80](https://github.com/python-hyper/h11/issues/80)). + +## v0.8.1 (2018-04-14) + +Bug fixes: + +- Always return headers as `bytes` objects ([#60](https://github.com/python-hyper/h11/issues/60)) + +Other changes: + +- Added proper license notices to the Javascript used in our + documentation ([#61](https://github.com/python-hyper/h11/issues/60)) + +## v0.8.0 (2018-03-20) + +Backwards **in**compatible changes: + +- h11 now performs stricter validation on outgoing header names and + header values: illegal characters are now rejected (example: you + can't put a newline into an HTTP header), and header values with + leading/trailing whitespace are also rejected (previously h11 would + silently discard the whitespace). All these checks were already + performed on incoming headers; this just extends that to outgoing + headers. + +New features: + +- New method `Connection.send_failed()`, to notify a + `Connection` object when data returned from + `Connection.send()` was *not* sent. + +Bug fixes: + +- Make sure that when computing the framing headers for HEAD + responses, we produce the same results as we would for the + corresponding GET. + +- Error out if a request has multiple Host: headers. + +- Send the Host: header first, as recommended by RFC 7230. + +- The Expect: header [is case-insensitive](https://tools.ietf.org/html/rfc7231#section-5.1.1), so use + case-insensitive matching when looking for 100-continue. + +Other changes: + +- Better error messages in several cases. + +- Provide correct `error_status_hint` in exception raised when + encountering an invalid `Transfer-Encoding` header. + +- For better compatibility with broken servers, h11 now tolerates + responses where the reason phrase is missing (not just empty). + +- Various optimizations and documentation improvements. + +## v0.7.0 (2016-11-25) + +New features (backwards compatible): + +- Made it so that sentinels are [instances of themselves](api.md#special-constants), to enable certain dispatch tricks on + the return value of `Connection.next_event()` (see [issue #8](https://github.com/python-hyper/h11/issues/8) for discussion). + +- Added `Data.chunk_start` and `Data.chunk_end` properties + to the `Data` event. These provide the user information + about where chunk delimiters are in the data stream from the remote + peer when chunked transfer encoding is in use. You [probably shouldn't use these](api.md#chunk-delimiters-are-bad), but sometimes + there's no alternative (see [issue #19](https://github.com/python-hyper/h11/issues/19) for discussion). + +- Expose `Response.reason` attribute, making it possible to read + or set the textual "reason phrase" on responses ([issue #13](https://github.com/python-hyper/h11/pull/13)). + +Bug fixes: + +- Fix the error message given when a call to an event constructor is + missing a required keyword argument ([issue #14](https://github.com/python-hyper/h11/issues/14)). + +- Fixed encoding of empty `Data` events (`Data(data=b"")`) + when using chunked encoding ([issue #21](https://github.com/python-hyper/h11/issues/21)). + +## v0.6.0 (2016-10-24) + +This is the first release since we started using h11 to write +non-trivial server code, and this experience triggered a number of +substantial API changes. + +Backwards **in**compatible changes: + +- Split the old `receive_data()` into the new + `Connection.receive_data()` and + `Connection.next_event()`, and replaced the old `Paused` + pseudo-event with the new `NEED_DATA` and `PAUSED` + sentinels. + +- Simplified the API by replacing the old `Connection.state_of()`, + `Connection.client_state`, `Connection.server_state` with + the new `Connection.states`. + +- Renamed the old `prepare_to_reuse()` to the new + `Connection.start_next_cycle()`. + +- Removed the `Paused` pseudo-event. + +Backwards compatible changes: + +- State machine: added a `DONE` -> `MUST_CLOSE` transition + triggered by our peer being in the `ERROR` state. + +- Split `ProtocolError` into `LocalProtocolError` and + `RemoteProtocolError` (see [Error handling](api.md#error-handling)). Use case: HTTP + servers want to be able to distinguish between an error that + originates locally (which produce a 500 status code) versus errors + caused by remote misbehavior (which produce a 4xx status code). + +- Changed the `PRODUCT_ID` from `h11/` to + `python-h11/`. (This is similar to what requests uses, + and much more searchable than plain h11.) + +Other changes: + +- Added a minimal benchmark suite, and used it to make a few small + optimizations (maybe ~20% speedup?). + +## v0.5.0 (2016-05-14) + +- Initial release. diff --git a/docs/src/examples.md b/docs/src/examples.md new file mode 100644 index 0000000..02c33d7 --- /dev/null +++ b/docs/src/examples.md @@ -0,0 +1,21 @@ +# Examples + + + +You can also find these in the +[`examples/` directory of a source checkout](https://github.com/danfimov/h11/tree/master/examples). + +## Minimal client, using synchronous I/O + +```python title="examples/basic-client.py" +--8<-- "basic-client.py" +``` + +## Fairly complete server with error handling, using Trio for async I/O + +```python title="examples/trio-server.py" +--8<-- "trio-server.py" +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..2614e1f --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,60 @@ +# h11-mypyc: An HTTP/1.1 protocol library + +h11-mypyc is an HTTP/1.1 protocol library written in Python, heavily inspired by +[hyper-h2](https://hyper-h2.readthedocs.io/). + +h11-mypyc's goal is to be a simple, robust, complete, and non-hacky implementation of +the first "chapter" of the HTTP/1.1 spec: +[RFC 7230: HTTP/1.1 Message Syntax and Routing](https://tools.ietf.org/html/rfc7230). +That is, it mostly focuses on implementing HTTP at the level of taking bytes on +and off the wire, and the headers related to that, and tries to be picky about +spec conformance when possible. It doesn't know about higher-level concerns like +URL routing, conditional GETs, cross-origin cookie policies, or content +negotiation. But it does know how to take care of framing, cross-version +differences in keep-alive handling, and the "obsolete line folding" rule, and to +use bounded time and space to process even pathological / malicious input, so +that you can focus your energies on the hard / interesting parts for your +application. And it tries to support the full specification in the sense that +any useful HTTP/1.1 conformant application should be able to use h11-mypyc. + +This is a "bring-your-own-I/O" protocol library; like h2, it contains no I/O code +whatsoever. This means you can hook h11-mypyc up to your favorite network API, and that +could be anything you want: synchronous, threaded, asynchronous, or your own +implementation of [RFC 6214](https://tools.ietf.org/html/rfc6214) -- h11-mypyc won't +judge you. This is h11-mypyc's main feature compared to the current state of the art, +where every HTTP library is tightly bound to a particular network framework, and +every time a [new network API](https://trio.readthedocs.io/) comes along then +someone has to start over reimplementing the entire HTTP stack from scratch. We +highly recommend +[Cory Benfield's excellent blog post about the advantages of this approach](https://lukasa.co.uk/2015/10/The_New_Hyper/). + +This also means that h11-mypyc is not immediately useful out of the box: it's a toolkit +for building programs that speak HTTP, not something that could directly replace +`requests` or `twisted.web` or whatever. But h11-mypyc makes it much easier to +implement something like `requests` or `twisted.web`. + +## Vital statistics + +- **Requirements:** Python 3.11+ (PyPy works great) + + The last Python 2-compatible version was h11 0.11.x. + +- **Install:** `pip install h11-mypyc` + +- **Sources and bug tracker:** + +- **Docs:** + +- **License:** MIT + +- **Code of conduct:** Contributors are requested to follow our + [code of conduct](https://github.com/danfimov/h11/blob/master/CODE_OF_CONDUCT.md) + in all project spaces. + +## Contents + +- [Getting started: writing your own HTTP/1.1 client](basic-usage.md) +- [API documentation](api.md) +- [Examples](examples.md) +- [Details of our HTTP support for HTTP nerds](supported-http.md) +- [History of changes](changes.md) diff --git a/docs/src/stylesheets/extra.css b/docs/src/stylesheets/extra.css new file mode 100644 index 0000000..311ba85 --- /dev/null +++ b/docs/src/stylesheets/extra.css @@ -0,0 +1,12 @@ +/* The state-machine diagrams are generated with Mermaid's `useMaxWidth: false`, + because shrinking them to the content column made their edge labels + unreadable. That means they render at their natural size, so the block they + live in has to scroll rather than spill over the page. */ +.mermaid { + max-width: 100%; + overflow-x: auto; +} + +.mermaid > svg { + max-width: none; +} diff --git a/docs/src/supported-http.md b/docs/src/supported-http.md new file mode 100644 index 0000000..07ab4d8 --- /dev/null +++ b/docs/src/supported-http.md @@ -0,0 +1,64 @@ +# Details of our HTTP support for HTTP nerds + +h11-mypyc only speaks HTTP/1.1. It can talk to HTTP/1.0 clients and servers, but it +itself only does HTTP/1.1. + +We fully support HTTP/1.1 keep-alive. + +We have a little bit of support for HTTP/1.1 pipelining -- basically the minimum +that's required by the standard. In server mode we can handle pipelined requests +in a serial manner, responding completely to each request before reading the next +(and our API is designed to make it easy for servers to keep this straight). +Client mode doesn't support pipelining at all. As far as I can tell, this matches +the state of the art in all the major HTTP implementations: the consensus seems +to be that HTTP/1.1 pipelining was a nice try but unworkable in practice, and if +you really need pipelining to work then instead of trying to fix HTTP/1.1 you +should switch to HTTP/2.0. + +The HTTP/1.0 `Connection: keep-alive` pseudo-standard is currently not supported. +(Note that this only affects h11-mypyc as a server, because h11-mypyc as a client always +speaks HTTP/1.1.) Supporting this would be possible, but it's fragile and finicky +and I'm suspicious that if we leave it out then no-one will notice or care. +HTTP/1.1 is now almost old enough to vote in United States elections. I get that +people sometimes write HTTP/1.0 clients because they don't want to deal with +annoying stuff like chunked encoding, and I completely sympathize with that, but +I'm guessing that you're not going to find too many people these days who care +desperately about keep-alive *and at the same time* are too lazy to implement +`Transfer-Encoding: chunked`. Still, this would be my bet as to the missing +feature that people are most likely to eventually complain about... + +Of the headers defined in RFC 7230, the ones h11-mypyc knows and has some special-case +logic to care about are: `Connection:`, `Transfer-Encoding:`, `Content-Length:`, +`Host:`, `Upgrade:`, and `Expect:` (which is really from +[RFC 7231](https://tools.ietf.org/html/rfc7231#section-5.1.1) but whatever). The +other headers in RFC 7230 are `TE:`, `Trailer:`, and `Via:`; h11-mypyc also supports +these in the sense that it ignores them and that's really all it should be doing. + +Transfer-Encoding support: we only know `chunked`, not `gzip` or `deflate`. We're +in good company in this: node.js at least doesn't handle anything besides +`chunked` either. So I'm not too worried about this being a problem in practice. +But I'm not majorly opposed to adding support for more features here either. + +A quirk in our [`Response`][h11_mypyc.Response] encoding: we don't bother including +ascii status messages -- instead of `200 OK` we just say `200`. This is totally +legal and no program should care, and it lets us skip carrying around a pointless +table of status message strings, but I suppose it might be worth fixing at some +point. + +When parsing chunked encoding, we parse but discard "chunk extensions". This is +an extremely obscure feature that allows arbitrary metadata to be interleaved +into a chunked transfer stream. This metadata has no standard uses, and proxies +are allowed to strip it out. I don't think anyone will notice this lack, but it +could be added if someone really wants it; I just ran out of energy for +implementing weirdo features no-one uses. + +Currently we *do* implement support for "obsolete line folding" when reading HTTP +headers. This is an optional part of the spec -- conforming HTTP/1.1 +implementations MUST NOT send continuation lines, and conforming HTTP/1.1 servers +MAY send 400 Bad Request responses back at clients who do send them +([ref](https://tools.ietf.org/html/rfc7230#section-3.2.4)). I'm tempted to remove +this support, since it adds some complicated and ugly code right at the center of +the request/response parsing loop, and I'm not sure whether anyone actually needs +it. Unfortunately a few major implementations that I spot-checked (node.js, go) +do still seem to support reading such headers (but not generating them), so it +might or might not be obsolete in practice -- it's hard to know. diff --git a/examples/basic-client.py b/examples/basic-client.py index 528dbf8..dc13271 100644 --- a/examples/basic-client.py +++ b/examples/basic-client.py @@ -1,17 +1,15 @@ import socket import ssl -import h11 +import h11_mypyc ################################################################ # Setup ################################################################ -conn = h11.Connection(our_role=h11.CLIENT) +conn = h11_mypyc.Connection(our_role=h11_mypyc.CLIENT) ctx = ssl.create_default_context() -sock = ctx.wrap_socket( - socket.create_connection(("httpbin.org", 443)), server_hostname="httpbin.org" -) +sock = ctx.wrap_socket(socket.create_connection(("httpbin.org", 443)), server_hostname="httpbin.org") ################################################################ # Sending a request @@ -22,20 +20,20 @@ def send(event): print("Sending event:") print(event) print() - # Pass the event through h11's state machine and encoding machinery + # Pass the event through h11_mypyc's state machine and encoding machinery data = conn.send(event) # Send the resulting bytes on the wire sock.sendall(data) send( - h11.Request( + h11_mypyc.Request( method="GET", target="/get", headers=[("Host", "httpbin.org"), ("Connection", "close")], ) ) -send(h11.EndOfMessage()) +send(h11_mypyc.EndOfMessage()) ################################################################ # Receiving the response @@ -46,10 +44,10 @@ def next_event(): while True: # Check if an event is already available event = conn.next_event() - if event is h11.NEED_DATA: + if event is h11_mypyc.NEED_DATA: # Nope, so fetch some data from the socket... data = sock.recv(2048) - # ...and give it to h11 to convert back into events... + # ...and give it to h11_mypyc to convert back into events... conn.receive_data(data) # ...and then loop around to try again. continue @@ -61,7 +59,7 @@ def next_event(): print("Received event:") print(event) print() - if type(event) is h11.EndOfMessage: + if type(event) is h11_mypyc.EndOfMessage: break ################################################################ diff --git a/examples/trio-server.py b/examples/trio-server.py index 996afb6..b9b66ed 100644 --- a/examples/trio-server.py +++ b/examples/trio-server.py @@ -1,4 +1,4 @@ -# A simple HTTP server implemented using h11 and Trio: +# A simple HTTP server implemented using h11_mypyc and Trio: # http://trio.readthedocs.io/en/latest/index.html # # All requests get echoed back a JSON document containing information about @@ -79,10 +79,9 @@ import json from itertools import count +import h11_mypyc import trio -import h11 - MAX_RECV = 2**16 TIMEOUT = 10 @@ -98,12 +97,12 @@ def format_date_time(dt=None): """Generate a RFC 7231 / RFC 9110 IMF-fixdate string""" if dt is None: - dt = datetime.datetime.now(datetime.timezone.utc) + dt = datetime.datetime.now(datetime.UTC) return email.utils.format_datetime(dt, usegmt=True) ################################################################ -# I/O adapter: h11 <-> trio +# I/O adapter: h11_mypyc <-> trio ################################################################ @@ -115,11 +114,11 @@ class TrioHTTPWrapper: def __init__(self, stream): self.stream = stream - self.conn = h11.Connection(h11.SERVER) + self.conn = h11_mypyc.Connection(h11_mypyc.SERVER) # Our Server: header - self.ident = " ".join( - [f"h11-example-trio-server/{h11.__version__}", h11.PRODUCT_ID] - ).encode("ascii") + self.ident = " ".join([f"h11-mypyc-example-trio-server/{h11_mypyc.__version__}", h11_mypyc.PRODUCT_ID]).encode( + "ascii" + ) # A unique id for this connection, to include in debugging output # (useful for understanding what's going on if there are multiple # simultaneous clients). @@ -129,7 +128,7 @@ async def send(self, event): # The code below doesn't send ConnectionClosed, so we don't bother # handling it here either -- it would require that we do something # appropriate when 'data' is None. - assert type(event) is not h11.ConnectionClosed + assert type(event) is not h11_mypyc.ConnectionClosed data = self.conn.send(event) try: await self.stream.send_all(data) @@ -142,9 +141,7 @@ async def send(self, event): async def _read_from_peer(self): if self.conn.they_are_waiting_for_100_continue: self.info("Sending 100 Continue") - go_ahead = h11.InformationalResponse( - status_code=100, headers=self.basic_headers() - ) + go_ahead = h11_mypyc.InformationalResponse(status_code=100, headers=self.basic_headers()) await self.send(go_ahead) try: data = await self.stream.receive_some(MAX_RECV) @@ -156,7 +153,7 @@ async def _read_from_peer(self): async def next_event(self): while True: event = self.conn.next_event() - if event is h11.NEED_DATA: + if event is h11_mypyc.NEED_DATA: await self._read_from_peer() continue return event @@ -236,27 +233,27 @@ def info(self, *args): # But these all have one thing in common: they involve us leaving the # nice easy path up above. So we can just proceed on the assumption # that the nice easy thing is what's happening, and whenever something -# goes wrong do our best to get back onto that path, and h11 will keep +# goes wrong do our best to get back onto that path, and h11_mypyc will keep # track of how successful we were and raise new errors if things don't work # out. async def http_serve(stream): wrapper = TrioHTTPWrapper(stream) wrapper.info("Got new connection") while True: - assert wrapper.conn.states == {h11.CLIENT: h11.IDLE, h11.SERVER: h11.IDLE} + assert wrapper.conn.states == {h11_mypyc.CLIENT: h11_mypyc.IDLE, h11_mypyc.SERVER: h11_mypyc.IDLE} try: with trio.fail_after(TIMEOUT): wrapper.info("Server main loop waiting for request") event = await wrapper.next_event() wrapper.info("Server main loop got event:", event) - if type(event) is h11.Request: + if type(event) is h11_mypyc.Request: await send_echo_response(wrapper, event) except Exception as exc: wrapper.info(f"Error during response handler: {exc!r}") await maybe_send_error_response(wrapper, exc) - if wrapper.conn.our_state is h11.MUST_CLOSE: + if wrapper.conn.our_state is h11_mypyc.MUST_CLOSE: wrapper.info("connection is not reusable, so shutting down") await wrapper.shutdown_and_clean_up() return @@ -264,12 +261,10 @@ async def http_serve(stream): try: wrapper.info("trying to re-use connection") wrapper.conn.start_next_cycle() - except h11.ProtocolError: + except h11_mypyc.ProtocolError: states = wrapper.conn.states wrapper.info("unexpected state", states, "-- bailing out") - await maybe_send_error_response( - wrapper, RuntimeError(f"unexpected state {states}") - ) + await maybe_send_error_response(wrapper, RuntimeError(f"unexpected state {states}")) await wrapper.shutdown_and_clean_up() return @@ -285,29 +280,27 @@ async def send_simple_response(wrapper, status_code, content_type, body): headers = wrapper.basic_headers() headers.append(("Content-Type", content_type)) headers.append(("Content-Length", str(len(body)))) - res = h11.Response(status_code=status_code, headers=headers) + res = h11_mypyc.Response(status_code=status_code, headers=headers) await wrapper.send(res) - await wrapper.send(h11.Data(data=body)) - await wrapper.send(h11.EndOfMessage()) + await wrapper.send(h11_mypyc.Data(data=body)) + await wrapper.send(h11_mypyc.EndOfMessage()) async def maybe_send_error_response(wrapper, exc): # If we can't send an error, oh well, nothing to be done wrapper.info("trying to send error response...") - if wrapper.conn.our_state not in {h11.IDLE, h11.SEND_RESPONSE}: + if wrapper.conn.our_state not in {h11_mypyc.IDLE, h11_mypyc.SEND_RESPONSE}: wrapper.info("...but I can't, because our state is", wrapper.conn.our_state) return try: - if isinstance(exc, h11.RemoteProtocolError): + if isinstance(exc, h11_mypyc.RemoteProtocolError): status_code = exc.error_status_hint elif isinstance(exc, trio.TooSlowError): status_code = 408 # Request Timeout else: status_code = 500 body = str(exc).encode("utf-8") - await send_simple_response( - wrapper, status_code, "text/plain; charset=utf-8", body - ) + await send_simple_response(wrapper, status_code, "text/plain; charset=utf-8", body) except Exception as exc: wrapper.info("error while sending error response:", exc) @@ -321,25 +314,18 @@ async def send_echo_response(wrapper, request): response_json = { "method": request.method.decode("ascii"), "target": request.target.decode("ascii"), - "headers": [ - (name.decode("ascii"), value.decode("ascii")) - for (name, value) in request.headers - ], + "headers": [(name.decode("ascii"), value.decode("ascii")) for (name, value) in request.headers], "body": "", } while True: event = await wrapper.next_event() - if type(event) is h11.EndOfMessage: + if type(event) is h11_mypyc.EndOfMessage: break - assert type(event) is h11.Data + assert type(event) is h11_mypyc.Data response_json["body"] += event.data.decode("ascii") - response_body_unicode = json.dumps( - response_json, sort_keys=True, indent=4, separators=(",", ": ") - ) + response_body_unicode = json.dumps(response_json, sort_keys=True, indent=4, separators=(",", ": ")) response_body_bytes = response_body_unicode.encode("utf-8") - await send_simple_response( - wrapper, 200, "application/json; charset=utf-8", response_body_bytes - ) + await send_simple_response(wrapper, 200, "application/json; charset=utf-8", response_body_bytes) async def serve(port): diff --git a/format-requirements.txt b/format-requirements.txt deleted file mode 100644 index a45e8c9..0000000 --- a/format-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -black==23.3.0 -isort==5.12.0 \ No newline at end of file diff --git a/fuzz/afl-server-examples/2 b/fuzz/afl-server-examples/2 index fb7e913..0c4739f 100644 --- a/fuzz/afl-server-examples/2 +++ b/fuzz/afl-server-examples/2 @@ -5,4 +5,3 @@ Transfer-Encoding: chunked 5 abcde 0 - diff --git a/fuzz/afl-server-examples/4 b/fuzz/afl-server-examples/4 index c7e5798..5d11621 100644 --- a/fuzz/afl-server-examples/4 +++ b/fuzz/afl-server-examples/4 @@ -1,2 +1 @@ GET /asdf HTTP/1.0 - diff --git a/fuzz/afl-server.py b/fuzz/afl-server.py index 0ff1947..6da8138 100644 --- a/fuzz/afl-server.py +++ b/fuzz/afl-server.py @@ -6,7 +6,6 @@ import sys import afl - import h11 diff --git a/h11/_events.py b/h11/_events.py deleted file mode 100644 index ca1c3ad..0000000 --- a/h11/_events.py +++ /dev/null @@ -1,369 +0,0 @@ -# High level events that make up HTTP/1.1 conversations. Loosely inspired by -# the corresponding events in hyper-h2: -# -# http://python-hyper.org/h2/en/stable/api.html#events -# -# Don't subclass these. Stuff will break. - -import re -from abc import ABC -from dataclasses import dataclass -from typing import List, Tuple, Union - -from ._abnf import method, request_target -from ._headers import Headers, normalize_and_validate -from ._util import bytesify, LocalProtocolError, validate - -# Everything in __all__ gets re-exported as part of the h11 public API. -__all__ = [ - "Event", - "Request", - "InformationalResponse", - "Response", - "Data", - "EndOfMessage", - "ConnectionClosed", -] - -method_re = re.compile(method.encode("ascii")) -request_target_re = re.compile(request_target.encode("ascii")) - - -class Event(ABC): - """ - Base class for h11 events. - """ - - __slots__ = () - - -@dataclass(init=False, frozen=True) -class Request(Event): - """The beginning of an HTTP request. - - Fields: - - .. attribute:: method - - An HTTP method, e.g. ``b"GET"`` or ``b"POST"``. Always a byte - string. :term:`Bytes-like objects ` and native - strings containing only ascii characters will be automatically - converted to byte strings. - - .. attribute:: target - - The target of an HTTP request, e.g. ``b"/index.html"``, or one of the - more exotic formats described in `RFC 7320, section 5.3 - `_. Always a byte - string. :term:`Bytes-like objects ` and native - strings containing only ascii characters will be automatically - converted to byte strings. - - .. attribute:: headers - - Request headers, represented as a list of (name, value) pairs. See - :ref:`the header normalization rules ` for details. - - .. attribute:: http_version - - The HTTP protocol version, represented as a byte string like - ``b"1.1"``. See :ref:`the HTTP version normalization rules - ` for details. - - """ - - __slots__ = ("method", "headers", "target", "http_version") - - method: bytes - headers: Headers - target: bytes - http_version: bytes - - def __init__( - self, - *, - method: Union[bytes, str], - headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], - target: Union[bytes, str], - http_version: Union[bytes, str] = b"1.1", - _parsed: bool = False, - ) -> None: - super().__init__() - if isinstance(headers, Headers): - object.__setattr__(self, "headers", headers) - else: - object.__setattr__( - self, "headers", normalize_and_validate(headers, _parsed=_parsed) - ) - if not _parsed: - object.__setattr__(self, "method", bytesify(method)) - object.__setattr__(self, "target", bytesify(target)) - object.__setattr__(self, "http_version", bytesify(http_version)) - else: - object.__setattr__(self, "method", method) - object.__setattr__(self, "target", target) - object.__setattr__(self, "http_version", http_version) - - # "A server MUST respond with a 400 (Bad Request) status code to any - # HTTP/1.1 request message that lacks a Host header field and to any - # request message that contains more than one Host header field or a - # Host header field with an invalid field-value." - # -- https://tools.ietf.org/html/rfc7230#section-5.4 - host_count = 0 - for name, value in self.headers: - if name == b"host": - host_count += 1 - if self.http_version == b"1.1" and host_count == 0: - raise LocalProtocolError("Missing mandatory Host: header") - if host_count > 1: - raise LocalProtocolError("Found multiple Host: headers") - - validate(method_re, self.method, "Illegal method characters") - validate(request_target_re, self.target, "Illegal target characters") - - # This is an unhashable type. - __hash__ = None # type: ignore - - -@dataclass(init=False, frozen=True) -class _ResponseBase(Event): - __slots__ = ("headers", "http_version", "reason", "status_code") - - headers: Headers - http_version: bytes - reason: bytes - status_code: int - - def __init__( - self, - *, - headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], - status_code: int, - http_version: Union[bytes, str] = b"1.1", - reason: Union[bytes, str] = b"", - _parsed: bool = False, - ) -> None: - super().__init__() - if isinstance(headers, Headers): - object.__setattr__(self, "headers", headers) - else: - object.__setattr__( - self, "headers", normalize_and_validate(headers, _parsed=_parsed) - ) - if not _parsed: - object.__setattr__(self, "reason", bytesify(reason)) - object.__setattr__(self, "http_version", bytesify(http_version)) - if not isinstance(status_code, int): - raise LocalProtocolError("status code must be integer") - # Because IntEnum objects are instances of int, but aren't - # duck-compatible (sigh), see gh-72. - object.__setattr__(self, "status_code", int(status_code)) - else: - object.__setattr__(self, "reason", reason) - object.__setattr__(self, "http_version", http_version) - object.__setattr__(self, "status_code", status_code) - - self.__post_init__() - - def __post_init__(self) -> None: - pass - - # This is an unhashable type. - __hash__ = None # type: ignore - - -@dataclass(init=False, frozen=True) -class InformationalResponse(_ResponseBase): - """An HTTP informational response. - - Fields: - - .. attribute:: status_code - - The status code of this response, as an integer. For an - :class:`InformationalResponse`, this is always in the range [100, - 200). - - .. attribute:: headers - - Request headers, represented as a list of (name, value) pairs. See - :ref:`the header normalization rules ` for - details. - - .. attribute:: http_version - - The HTTP protocol version, represented as a byte string like - ``b"1.1"``. See :ref:`the HTTP version normalization rules - ` for details. - - .. attribute:: reason - - The reason phrase of this response, as a byte string. For example: - ``b"OK"``, or ``b"Not Found"``. - - """ - - def __post_init__(self) -> None: - if not (100 <= self.status_code < 200): - raise LocalProtocolError( - "InformationalResponse status_code should be in range " - "[100, 200), not {}".format(self.status_code) - ) - - # This is an unhashable type. - __hash__ = None # type: ignore - - -@dataclass(init=False, frozen=True) -class Response(_ResponseBase): - """The beginning of an HTTP response. - - Fields: - - .. attribute:: status_code - - The status code of this response, as an integer. For an - :class:`Response`, this is always in the range [200, - 1000). - - .. attribute:: headers - - Request headers, represented as a list of (name, value) pairs. See - :ref:`the header normalization rules ` for details. - - .. attribute:: http_version - - The HTTP protocol version, represented as a byte string like - ``b"1.1"``. See :ref:`the HTTP version normalization rules - ` for details. - - .. attribute:: reason - - The reason phrase of this response, as a byte string. For example: - ``b"OK"``, or ``b"Not Found"``. - - """ - - def __post_init__(self) -> None: - if not (200 <= self.status_code < 1000): - raise LocalProtocolError( - "Response status_code should be in range [200, 1000), not {}".format( - self.status_code - ) - ) - - # This is an unhashable type. - __hash__ = None # type: ignore - - -@dataclass(init=False, frozen=True) -class Data(Event): - """Part of an HTTP message body. - - Fields: - - .. attribute:: data - - A :term:`bytes-like object` containing part of a message body. Or, if - using the ``combine=False`` argument to :meth:`Connection.send`, then - any object that your socket writing code knows what to do with, and for - which calling :func:`len` returns the number of bytes that will be - written -- see :ref:`sendfile` for details. - - .. attribute:: chunk_start - - A marker that indicates whether this data object is from the start of a - chunked transfer encoding chunk. This field is ignored when when a Data - event is provided to :meth:`Connection.send`: it is only valid on - events emitted from :meth:`Connection.next_event`. You probably - shouldn't use this attribute at all; see - :ref:`chunk-delimiters-are-bad` for details. - - .. attribute:: chunk_end - - A marker that indicates whether this data object is the last for a - given chunked transfer encoding chunk. This field is ignored when when - a Data event is provided to :meth:`Connection.send`: it is only valid - on events emitted from :meth:`Connection.next_event`. You probably - shouldn't use this attribute at all; see - :ref:`chunk-delimiters-are-bad` for details. - - """ - - __slots__ = ("data", "chunk_start", "chunk_end") - - data: bytes - chunk_start: bool - chunk_end: bool - - def __init__( - self, data: bytes, chunk_start: bool = False, chunk_end: bool = False - ) -> None: - object.__setattr__(self, "data", data) - object.__setattr__(self, "chunk_start", chunk_start) - object.__setattr__(self, "chunk_end", chunk_end) - - # This is an unhashable type. - __hash__ = None # type: ignore - - -# XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that -# are forbidden to be sent in a trailer, since processing them as if they were -# present in the header section might bypass external security filters." -# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part -# Unfortunately, the list of forbidden fields is long and vague :-/ -@dataclass(init=False, frozen=True) -class EndOfMessage(Event): - """The end of an HTTP message. - - Fields: - - .. attribute:: headers - - Default value: ``[]`` - - Any trailing headers attached to this message, represented as a list of - (name, value) pairs. See :ref:`the header normalization rules - ` for details. - - Must be empty unless ``Transfer-Encoding: chunked`` is in use. - - """ - - __slots__ = ("headers",) - - headers: Headers - - def __init__( - self, - *, - headers: Union[ - Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None - ] = None, - _parsed: bool = False, - ) -> None: - super().__init__() - if headers is None: - headers = Headers([]) - elif not isinstance(headers, Headers): - headers = normalize_and_validate(headers, _parsed=_parsed) - - object.__setattr__(self, "headers", headers) - - # This is an unhashable type. - __hash__ = None # type: ignore - - -@dataclass(frozen=True) -class ConnectionClosed(Event): - """This event indicates that the sender has closed their outgoing - connection. - - Note that this does not necessarily mean that they can't *receive* further - data, because TCP connections are composed to two one-way channels which - can be closed independently. See :ref:`closing` for details. - - No fields. - """ - - pass diff --git a/h11/_version.py b/h11/_version.py deleted file mode 100644 index c8a0b48..0000000 --- a/h11/_version.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file must be kept very simple, because it is consumed from several -# places -- it is imported by h11/__init__.py, execfile'd by setup.py, etc. - -# We use a simple scheme: -# 1.0.0 -> 1.0.0+dev -> 1.1.0 -> 1.1.0+dev -# where the +dev versions are never released into the wild, they're just what -# we stick into the VCS in between releases. -# -# This is compatible with PEP 440: -# http://legacy.python.org/dev/peps/pep-0440/ -# via the use of the "local suffix" "+dev", which is disallowed on index -# servers and causes 1.0.0+dev to sort after plain 1.0.0, which is what we -# want. (Contrast with the special suffix 1.0.0.dev, which sorts *before* -# 1.0.0.) - -__version__ = "0.16.0+dev" diff --git a/h11/__init__.py b/h11_mypyc/__init__.py similarity index 79% rename from h11/__init__.py rename to h11_mypyc/__init__.py index 989e92c..3801c3a 100644 --- a/h11/__init__.py +++ b/h11_mypyc/__init__.py @@ -6,8 +6,8 @@ # semantics to check that what you're asking to write to the wire is sensible, # but at least it gets you out of dealing with the wire itself. -from h11._connection import Connection, NEED_DATA, PAUSED -from h11._events import ( +from h11_mypyc._connection import NEED_DATA, PAUSED, Connection +from h11_mypyc._events import ( ConnectionClosed, Data, EndOfMessage, @@ -16,7 +16,7 @@ Request, Response, ) -from h11._state import ( +from h11_mypyc._state import ( CLIENT, CLOSED, DONE, @@ -29,10 +29,10 @@ SERVER, SWITCHED_PROTOCOL, ) -from h11._util import LocalProtocolError, ProtocolError, RemoteProtocolError -from h11._version import __version__ +from h11_mypyc._util import LocalProtocolError, ProtocolError, RemoteProtocolError +from h11_mypyc._version import __version__ -PRODUCT_ID = "python-h11/" + __version__ +PRODUCT_ID = "python-h11-mypyc/" + __version__ __all__ = ( @@ -55,6 +55,7 @@ "SEND_BODY", "SEND_RESPONSE", "SERVER", + "MIGHT_SWITCH_PROTOCOL", "SWITCHED_PROTOCOL", "ProtocolError", "LocalProtocolError", diff --git a/h11/_abnf.py b/h11_mypyc/_abnf.py similarity index 97% rename from h11/_abnf.py rename to h11_mypyc/_abnf.py index 933587f..4acb4fa 100644 --- a/h11/_abnf.py +++ b/h11_mypyc/_abnf.py @@ -125,8 +125,6 @@ chunk_header = ( r"(?P{chunk_size})" r"(?P{chunk_ext})?" - r"{OWS}\r\n".format( - **globals() - ) # Even though the specification does not allow for extra whitespaces, + r"{OWS}\r\n".format(**globals()) # Even though the specification does not allow for extra whitespaces, # we are lenient with trailing whitespaces because some servers on the wild use it. ) diff --git a/h11/_connection.py b/h11_mypyc/_connection.py similarity index 70% rename from h11/_connection.py rename to h11_mypyc/_connection.py index e37d82a..060cd35 100644 --- a/h11/_connection.py +++ b/h11_mypyc/_connection.py @@ -1,19 +1,13 @@ # This contains the main Connection class. Everything in h11 revolves around # this. +from collections.abc import Callable from typing import ( Any, - Callable, cast, - Dict, - List, - Optional, overload, - Tuple, - Type, - Union, ) -from ._events import ( +from h11_mypyc._events import ( ConnectionClosed, Data, EndOfMessage, @@ -22,37 +16,37 @@ Request, Response, ) -from ._headers import get_comma_header, has_expect_100_continue, set_comma_header -from ._readers import READERS, ReadersType -from ._receivebuffer import ReceiveBuffer -from ._state import ( +from h11_mypyc._headers import get_comma_header, has_expect_100_continue, set_comma_header +from h11_mypyc._readers import READERS, ReadersType +from h11_mypyc._receivebuffer import ReceiveBuffer +from h11_mypyc._state import ( _SWITCH_CONNECT, _SWITCH_UPGRADE, CLIENT, - ConnectionState, DONE, ERROR, MIGHT_SWITCH_PROTOCOL, SEND_BODY, SERVER, SWITCHED_PROTOCOL, + ConnectionState, ) -from ._util import ( # Import the internal things we need +from h11_mypyc._util import ( # Import the internal things we need LocalProtocolError, RemoteProtocolError, Sentinel, ) -from ._writers import WRITERS, WritersType +from h11_mypyc._writers import WRITERS, WritersType # Everything in __all__ gets re-exported as part of the h11 public API. __all__ = ["Connection", "NEED_DATA", "PAUSED"] -class NEED_DATA(Sentinel, metaclass=Sentinel): +class NEED_DATA(Sentinel): pass -class PAUSED(Sentinel, metaclass=Sentinel): +class PAUSED(Sentinel): pass @@ -81,7 +75,7 @@ class PAUSED(Sentinel, metaclass=Sentinel): # our rule is: # - If someone says Connection: close, we will close # - If someone uses HTTP/1.0, we will close. -def _keep_alive(event: Union[Request, Response]) -> bool: +def _keep_alive(event: Request | Response) -> bool: connection = get_comma_header(event.headers, b"connection") if b"close" in connection: return False @@ -90,9 +84,9 @@ def _keep_alive(event: Union[Request, Response]) -> bool: return True -def _body_framing( - request_method: bytes, event: Union[Request, Response] -) -> Tuple[str, Union[Tuple[()], Tuple[int]]]: +# request_method is None until a Request arrives -- a server may respond before +# that, e.g. 408 Request Timeout. None correctly fails both comparisons below. +def _body_framing(request_method: bytes | None, event: Request | Response) -> tuple[str, tuple[()] | tuple[int]]: # Called when we enter SEND_BODY to figure out framing information for # this body. # @@ -152,21 +146,20 @@ class Connection: """An object encapsulating the state of an HTTP connection. Args: - our_role: If you're implementing a client, pass :data:`h11.CLIENT`. If - you're implementing a server, pass :data:`h11.SERVER`. - - max_incomplete_event_size (int): - The maximum number of bytes we're willing to buffer of an - incomplete event. In practice this mostly sets a limit on the - maximum size of the request/response line + headers. If this is - exceeded, then :meth:`next_event` will raise - :exc:`RemoteProtocolError`. - + our_role: If you're implementing a client, pass + [`h11_mypyc.CLIENT`](#roles). If you're implementing a server, + pass [`h11_mypyc.SERVER`](#roles). + max_incomplete_event_size: The maximum number of bytes we're willing + to buffer of an incomplete event. In practice this mostly sets a + limit on the maximum size of the request/response line + headers. + If this is exceeded, then + [`next_event()`][h11_mypyc.Connection.next_event] will raise + [`RemoteProtocolError`][h11_mypyc.RemoteProtocolError]. """ def __init__( self, - our_role: Type[Sentinel], + our_role: type[Sentinel], max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE, ) -> None: self._max_incomplete_event_size = max_incomplete_event_size @@ -174,7 +167,7 @@ def __init__( if our_role not in (CLIENT, SERVER): raise ValueError(f"expected CLIENT or SERVER, not {our_role!r}") self.our_role = our_role - self.their_role: Type[Sentinel] + self.their_role: type[Sentinel] if our_role is CLIENT: self.their_role = SERVER else: @@ -197,34 +190,37 @@ def __init__( # These two are only used to interpret framing headers for figuring # out how to read/write response bodies. their_http_version is also # made available as a convenient public API. - self.their_http_version: Optional[bytes] = None - self._request_method: Optional[bytes] = None + self.their_http_version: bytes | None = None + self._request_method: bytes | None = None # This is pure flow-control and doesn't at all affect the set of legal # transitions, so no need to bother ConnectionState with it: self.client_is_waiting_for_100_continue = False @property - def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]: - """A dictionary like:: - - {CLIENT: , SERVER: } + def states(self) -> dict[type[Sentinel], type[Sentinel]]: + """A dictionary like: - See :ref:`state-machine` for details. + ```python + {CLIENT: , SERVER: } + ``` + See [The state machine](#state-machine) for details. """ return dict(self._cstate.states) @property - def our_state(self) -> Type[Sentinel]: - """The current state of whichever role we are playing. See - :ref:`state-machine` for details. + def our_state(self) -> type[Sentinel]: + """The current state of whichever role we are playing. + + See [The state machine](#state-machine) for details. """ return self._cstate.states[self.our_role] @property - def their_state(self) -> Type[Sentinel]: - """The current state of whichever role we are NOT playing. See - :ref:`state-machine` for details. + def their_state(self) -> type[Sentinel]: + """The current state of whichever role we are NOT playing. + + See [The state machine](#state-machine) for details. """ return self._cstate.states[self.their_role] @@ -236,13 +232,12 @@ def start_next_cycle(self) -> None: """Attempt to reset our connection state for a new request/response cycle. - If both client and server are in :data:`DONE` state, then resets them - both to :data:`IDLE` state in preparation for a new request/response - cycle on this same connection. Otherwise, raises a - :exc:`LocalProtocolError`. - - See :ref:`keepalive-and-pipelining`. + If both client and server are in `DONE` state, then resets them both + to `IDLE` state in preparation for a new request/response cycle on + this same connection. Otherwise, raises a + [`LocalProtocolError`][h11_mypyc.LocalProtocolError]. + See [Re-using a connection](#keepalive-and-pipelining). """ old_states = dict(self._cstate.states) self._cstate.start_next_cycle() @@ -252,24 +247,21 @@ def start_next_cycle(self) -> None: assert not self.client_is_waiting_for_100_continue self._respond_to_state_changes(old_states) - def _process_error(self, role: Type[Sentinel]) -> None: + def _process_error(self, role: type[Sentinel]) -> None: old_states = dict(self._cstate.states) self._cstate.process_error(role) self._respond_to_state_changes(old_states) - def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]: + def _server_switch_event(self, event: Event) -> type[Sentinel] | None: if type(event) is InformationalResponse and event.status_code == 101: return _SWITCH_UPGRADE if type(event) is Response: - if ( - _SWITCH_CONNECT in self._cstate.pending_switch_proposals - and 200 <= event.status_code < 300 - ): + if _SWITCH_CONNECT in self._cstate.pending_switch_proposals and 200 <= event.status_code < 300: return _SWITCH_CONNECT return None # All events go through here - def _process_event(self, role: Type[Sentinel], event: Event) -> None: + def _process_event(self, role: type[Sentinel], event: Event) -> None: # First, pass the event through the state machine to make sure it # succeeds. old_states = dict(self._cstate.states) @@ -293,7 +285,7 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None: Response, InformationalResponse, ): - event = cast(Union[Request, Response, InformationalResponse], event) + event = cast(Request | Response | InformationalResponse, event) self.their_http_version = event.http_version # Keep alive handling @@ -302,9 +294,7 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None: # shows up on a 1xx InformationalResponse. I think the idea is that # this is not supposed to happen. In any case, if it does happen, we # ignore it. - if type(event) in (Request, Response) and not _keep_alive( - cast(Union[Request, Response], event) - ): + if type(event) in (Request, Response) and not _keep_alive(cast(Request | Response, event)): self._cstate.process_keep_alive_disabled() # 100-continue @@ -319,18 +309,16 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None: def _get_io_object( self, - role: Type[Sentinel], - event: Optional[Event], - io_dict: Union[ReadersType, WritersType], - ) -> Optional[Callable[..., Any]]: + role: type[Sentinel], + event: Event | None, + io_dict: ReadersType | WritersType, + ) -> Callable[..., Any] | None: # event may be None; it's only used when entering SEND_BODY state = self._cstate.states[role] if state is SEND_BODY: # Special case: the io_dict has a dict of reader/writer factories # that depend on the request/response framing. - framing_type, args = _body_framing( - cast(bytes, self._request_method), cast(Union[Request, Response], event) - ) + framing_type, args = _body_framing(self._request_method, cast(Request | Response, event)) return io_dict[SEND_BODY][framing_type](*args) # type: ignore[index] else: # General case: the io_dict just has the appropriate reader/writer @@ -341,8 +329,8 @@ def _get_io_object( # self._cstate.states to change. def _respond_to_state_changes( self, - old_states: Dict[Type[Sentinel], Type[Sentinel]], - event: Optional[Event] = None, + old_states: dict[type[Sentinel], type[Sentinel]], + event: Event | None = None, ) -> None: # Update reader/writer if self.our_state != old_states[self.our_role]: @@ -351,13 +339,15 @@ def _respond_to_state_changes( self._reader = self._get_io_object(self.their_role, event, READERS) @property - def trailing_data(self) -> Tuple[bytes, bool]: - """Data that has been received, but not yet processed, represented as - a tuple with two elements, where the first is a byte-string containing - the unprocessed data itself, and the second is a bool that is True if - the receive connection was closed. + def trailing_data(self) -> tuple[bytes, bool]: + """Data that has been received, but not yet processed. - See :ref:`switching-protocols` for discussion of why you'd want this. + Represented as a tuple with two elements, where the first is a + byte-string containing the unprocessed data itself, and the second is + a bool that is True if the receive connection was closed. + + See [Switching protocols](#switching-protocols) for discussion of why + you'd want this. """ return (bytes(self._receive_buffer), self._receive_buffer_closed) @@ -365,40 +355,40 @@ def receive_data(self, data: bytes) -> None: """Add data to our internal receive buffer. This does not actually do any processing on the data, just stores - it. To trigger processing, you have to call :meth:`next_event`. + it. To trigger processing, you have to call + [`next_event()`][h11_mypyc.Connection.next_event]. Args: - data (:term:`bytes-like object`): - The new data that was just received. + data: The new data that was just received, as a + [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object). - Special case: If *data* is an empty byte-string like ``b""``, + Special case: if *data* is an empty byte-string like `b""`, then this indicates that the remote side has closed the connection (end of file). Normally this is convenient, because - standard Python APIs like :meth:`file.read` or - :meth:`socket.recv` use ``b""`` to indicate end-of-file, while - other failures to read are indicated using other mechanisms - like raising :exc:`TimeoutError`. When using such an API you - can just blindly pass through whatever you get from ``read`` - to :meth:`receive_data`, and everything will work. + standard Python APIs like [`file.read()`][io.RawIOBase.read] or + [`socket.recv()`][socket.socket.recv] use `b""` to indicate + end-of-file, while other failures to read are indicated using + other mechanisms like raising [`TimeoutError`][]. When using + such an API you can just blindly pass through whatever you get + from `read` to `receive_data()`, and everything will work. But, if you have an API where reading an empty string is a valid non-EOF condition, then you need to be aware of this and make sure to check for such strings and avoid passing them to - :meth:`receive_data`. + `receive_data()`. Returns: - Nothing, but after calling this you should call :meth:`next_event` - to parse the newly received data. + Nothing, but after calling this you should call + [`next_event()`][h11_mypyc.Connection.next_event] to parse the newly + received data. Raises: - RuntimeError: - Raised if you pass an empty *data*, indicating EOF, and then - pass a non-empty *data*, indicating more data that somehow - arrived after the EOF. - - (Calling ``receive_data(b"")`` multiple times is fine, - and equivalent to calling it once.) + RuntimeError: Raised if you pass an empty *data*, indicating EOF, + and then pass a non-empty *data*, indicating more data that + somehow arrived after the EOF. + (Calling `receive_data(b"")` multiple times is fine, and + equivalent to calling it once.) """ if data: if self._receive_buffer_closed: @@ -409,7 +399,7 @@ def receive_data(self, data: bytes) -> None: def _extract_next_receive_event( self, - ) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + ) -> Event | type[NEED_DATA] | type[PAUSED]: state = self.their_state # We don't pause immediately when they enter DONE, because even in # DONE state we can still process a ConnectionClosed() event. But @@ -435,44 +425,43 @@ def _extract_next_receive_event( event = NEED_DATA return event # type: ignore[no-any-return] - def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + def next_event(self) -> Event | type[NEED_DATA] | type[PAUSED]: """Parse the next event out of our receive buffer, update our internal state, and return it. - This is a mutating operation -- think of it like calling :func:`next` - on an iterator. + This is a mutating operation -- think of it like calling + [`next()`][next] on an iterator. Returns: - : One of three things: - - 1) An event object -- see :ref:`events`. - - 2) The special constant :data:`NEED_DATA`, which indicates that - you need to read more data from your socket and pass it to - :meth:`receive_data` before this method will be able to return - any more events. - - 3) The special constant :data:`PAUSED`, which indicates that we - are not in a state where we can process incoming data (usually - because the peer has finished their part of the current - request/response cycle, and you have not yet called - :meth:`start_next_cycle`). See :ref:`flow-control` for details. + One of three things: + + 1. An event object -- see [Events](#events). + 2. The special constant `NEED_DATA`, which indicates that you + need to read more data from your socket and pass it to + [`receive_data()`][h11_mypyc.Connection.receive_data] before this + method will be able to return any more events. + 3. The special constant `PAUSED`, which indicates that we are + not in a state where we can process incoming data (usually + because the peer has finished their part of the current + request/response cycle, and you have not yet called + [`start_next_cycle()`][h11_mypyc.Connection.start_next_cycle]). + See [Flow control](#flow-control) for details. Raises: - RemoteProtocolError: - The peer has misbehaved. You should close the connection - (possibly after sending some kind of 4xx response). + RemoteProtocolError: The peer has misbehaved. You should close the + connection (possibly after sending some kind of 4xx response). - Once this method returns :class:`ConnectionClosed` once, then all - subsequent calls will also return :class:`ConnectionClosed`. + Once this method returns [`ConnectionClosed`][h11_mypyc.ConnectionClosed] + once, then all subsequent calls will also return + [`ConnectionClosed`][h11_mypyc.ConnectionClosed]. - If this method raises any exception besides :exc:`RemoteProtocolError` - then that's a bug -- if it happens please file a bug report! + If this method raises any exception besides + [`RemoteProtocolError`][h11_mypyc.RemoteProtocolError] then that's a bug -- + if it happens please file a bug report! If this method raises any exception then it also sets - :attr:`Connection.their_state` to :data:`ERROR` -- see - :ref:`error-handling` for discussion. - + [`their_state`][h11_mypyc.Connection.their_state] to `ERROR` -- see + [Error handling](#error-handling) for discussion. """ if self.their_state is ERROR: @@ -485,9 +474,7 @@ def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: if len(self._receive_buffer) > self._max_incomplete_event_size: # 431 is "Request header fields too large" which is pretty # much the only situation where we can get here - raise RemoteProtocolError( - "Receive buffer too long", error_status_hint=431 - ) + raise RemoteProtocolError("Receive buffer too long", error_status_hint=431) if self._receive_buffer_closed: # We're still trying to complete some event, but that's # never going to happen because no more data is coming @@ -501,39 +488,32 @@ def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: raise @overload - def send(self, event: ConnectionClosed) -> None: - ... + def send(self, event: ConnectionClosed) -> None: ... @overload - def send( - self, event: Union[Request, InformationalResponse, Response, Data, EndOfMessage] - ) -> bytes: - ... + def send(self, event: Request | InformationalResponse | Response | Data | EndOfMessage) -> bytes: ... @overload - def send(self, event: Event) -> Optional[bytes]: - ... + def send(self, event: Event) -> bytes | None: ... - def send(self, event: Event) -> Optional[bytes]: + def send(self, event: Event) -> bytes | None: """Convert a high-level event into bytes that can be sent to the peer, while updating our internal state machine. Args: - event: The :ref:`event ` to send. + event: The [event](#events) to send. Returns: - If ``type(event) is ConnectionClosed``, then returns - ``None``. Otherwise, returns a :term:`bytes-like object`. + `None` if `type(event) is ConnectionClosed`. Otherwise, a + [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object). Raises: - LocalProtocolError: - Sending this event at this time would violate our - understanding of the HTTP/1.1 protocol. + LocalProtocolError: Sending this event at this time would violate + our understanding of the HTTP/1.1 protocol. If this method raises any exception then it also sets - :attr:`Connection.our_state` to :data:`ERROR` -- see - :ref:`error-handling` for discussion. - + [`our_state`][h11_mypyc.Connection.our_state] to `ERROR` -- see + [Error handling](#error-handling) for discussion. """ data_list = self.send_with_data_passthrough(event) if data_list is None: @@ -541,13 +521,15 @@ def send(self, event: Event) -> Optional[bytes]: else: return b"".join(data_list) - def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]: - """Identical to :meth:`send`, except that in situations where - :meth:`send` returns a single :term:`bytes-like object`, this instead - returns a list of them -- and when sending a :class:`Data` event, this - list is guaranteed to contain the exact object you passed in as - :attr:`Data.data`. See :ref:`sendfile` for discussion. + def send_with_data_passthrough(self, event: Event) -> list[bytes] | None: + """Identical to [`send()`][h11_mypyc.Connection.send], except that in + situations where [`send()`][h11_mypyc.Connection.send] returns a single + [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object), + this instead returns a list of them -- and when sending a + [`Data`][h11_mypyc.Data] event, this list is guaranteed to contain the exact + object you passed in as `Data.data`. + See [Support for `sendfile()`](#sendfile) for discussion. """ if self.our_state is ERROR: raise LocalProtocolError("Can't send data when our state is ERROR") @@ -567,7 +549,7 @@ def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]: # In any situation where writer is None, process_event should # have raised ProtocolError assert writer is not None - data_list: List[bytes] = [] + data_list: list[bytes] = [] writer(event, data_list.append) return data_list except: @@ -578,9 +560,8 @@ def send_failed(self) -> None: """Notify the state machine that we failed to send the data it gave us. - This causes :attr:`Connection.our_state` to immediately become - :data:`ERROR` -- see :ref:`error-handling` for discussion. - + This causes [`our_state`][h11_mypyc.Connection.our_state] to immediately + become `ERROR` -- see [Error handling](#error-handling) for discussion. """ self._process_error(self.our_role) @@ -612,7 +593,7 @@ def _clean_up_response_headers_for_sending(self, response: Response) -> Response # we're allowed to leave out the framing headers -- see # https://tools.ietf.org/html/rfc7231#section-4.3.2 . But it's just as # easy to get them right.) - method_for_choosing_headers = cast(bytes, self._request_method) + method_for_choosing_headers = self._request_method if method_for_choosing_headers == b"HEAD": method_for_choosing_headers = b"GET" framing_type, _ = _body_framing(method_for_choosing_headers, response) diff --git a/h11_mypyc/_events.py b/h11_mypyc/_events.py new file mode 100644 index 0000000..5252aa7 --- /dev/null +++ b/h11_mypyc/_events.py @@ -0,0 +1,316 @@ +# High level events that make up HTTP/1.1 conversations. Loosely inspired by +# the corresponding events in hyper-h2: +# +# http://python-hyper.org/h2/en/stable/api.html#events +# +# Don't subclass these. Stuff will break -- @final says so to mypyc too, +# which lets it call methods directly instead of through a vtable. + +import re +from dataclasses import dataclass +from typing import Any, final + +from h11_mypyc._abnf import method, request_target +from h11_mypyc._headers import Headers, normalize_and_validate +from h11_mypyc._util import Bytesifiable, LocalProtocolError, bytesify, validate + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = [ + "Event", + "Request", + "InformationalResponse", + "Response", + "Data", + "EndOfMessage", + "ConnectionClosed", +] + +method_re = re.compile(method.encode("ascii")) +request_target_re = re.compile(request_target.encode("ascii")) + + +class Event: + """ + Base class for h11-mypyc events. + """ + + __slots__ = () + + +@final +@dataclass(init=False, frozen=True, slots=True) +class Request(Event): + """The beginning of an HTTP request. + + Attributes: + method: An HTTP method, e.g. `b"GET"` or `b"POST"`. Always a byte + string. Native strings containing only ascii characters and + [bytes-like objects](https://docs.python.org/3/glossary.html#term-bytes-like-object) are automatically + converted to byte strings. + target: The target of an HTTP request, e.g. `b"/index.html"`, or one + of the more exotic formats described in + [RFC 7230, section 5.3](https://tools.ietf.org/html/rfc7230#section-5.3). + Always a byte string. Native strings containing only ascii + characters and [bytes-like objects](https://docs.python.org/3/glossary.html#term-bytes-like-object) are + automatically converted to byte strings. + headers: Request headers, represented as a list of (name, value) + pairs. See [the header normalization rules](#headers-format) for + details. + http_version: The HTTP protocol version, represented as a byte string + like `b"1.1"`. See + [the HTTP version normalization rules](#http-version-format) for + details. + """ + + method: bytes + headers: Headers + target: bytes + http_version: bytes + + def __init__( + self, + *, + method: Bytesifiable, + headers: Headers | list[tuple[bytes, bytes]] | list[tuple[str, str]], + target: Bytesifiable, + http_version: Bytesifiable = b"1.1", + _parsed: bool = False, + ) -> None: + if isinstance(headers, Headers): + object.__setattr__(self, "headers", headers) + else: + object.__setattr__(self, "headers", normalize_and_validate(headers, _parsed=_parsed)) + if not _parsed: + object.__setattr__(self, "method", bytesify(method)) + object.__setattr__(self, "target", bytesify(target)) + object.__setattr__(self, "http_version", bytesify(http_version)) + else: + object.__setattr__(self, "method", method) + object.__setattr__(self, "target", target) + object.__setattr__(self, "http_version", http_version) + + # "A server MUST respond with a 400 (Bad Request) status code to any + # HTTP/1.1 request message that lacks a Host header field and to any + # request message that contains more than one Host header field or a + # Host header field with an invalid field-value." + # -- https://tools.ietf.org/html/rfc7230#section-5.4 + host_count = 0 + for name, value in self.headers: + if name == b"host": + host_count += 1 + if self.http_version == b"1.1" and host_count == 0: + raise LocalProtocolError("Missing mandatory Host: header") + if host_count > 1: + raise LocalProtocolError("Found multiple Host: headers") + + validate(method_re, self.method, "Illegal method characters") + validate(request_target_re, self.target, "Illegal target characters") + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True, slots=True) +class _ResponseBase(Event): + headers: Headers + http_version: bytes + reason: bytes + status_code: int + + def __init__( + self, + *, + headers: Headers | list[tuple[bytes, bytes]] | list[tuple[str, str]], + # Not `int`: validated below to raise LocalProtocolError, which a + # compiled signature would pre-empt with TypeError. + status_code: Any, + http_version: Bytesifiable = b"1.1", + reason: Bytesifiable = b"", + _parsed: bool = False, + ) -> None: + if isinstance(headers, Headers): + object.__setattr__(self, "headers", headers) + else: + object.__setattr__(self, "headers", normalize_and_validate(headers, _parsed=_parsed)) + if not _parsed: + object.__setattr__(self, "reason", bytesify(reason)) + object.__setattr__(self, "http_version", bytesify(http_version)) + if not isinstance(status_code, int): + raise LocalProtocolError("status code must be integer") + # Because IntEnum objects are instances of int, but aren't + # duck-compatible (sigh), see gh-72. + object.__setattr__(self, "status_code", int(status_code)) + else: + object.__setattr__(self, "reason", reason) + object.__setattr__(self, "http_version", http_version) + object.__setattr__(self, "status_code", status_code) + + self.__post_init__() + + def __post_init__(self) -> None: + pass + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@final +@dataclass(init=False, frozen=True, slots=True) +class InformationalResponse(_ResponseBase): + """An HTTP informational response. + + Attributes: + status_code: The status code of this response, as an integer. For + an [`InformationalResponse`][h11_mypyc.InformationalResponse], this is + always in the range [100, 200). + headers: Request headers, represented as a list of (name, value) + pairs. See [the header normalization rules](#headers-format) for + details. + http_version: The HTTP protocol version, represented as a byte string + like `b"1.1"`. See + [the HTTP version normalization rules](#http-version-format) for + details. + reason: The reason phrase of this response, as a byte string. For + example: `b"OK"`, or `b"Not Found"`. + """ + + def __post_init__(self) -> None: + if not (100 <= self.status_code < 200): + raise LocalProtocolError( + f"InformationalResponse status_code should be in range [100, 200), not {self.status_code}" + ) + + # This is an unhashable type. + __hash__ = None + + +@final +@dataclass(init=False, frozen=True, slots=True) +class Response(_ResponseBase): + """The beginning of an HTTP response. + + Attributes: + status_code: The status code of this response, as an integer. For a + [`Response`][h11_mypyc.Response], this is always in the range + [200, 1000). + headers: Request headers, represented as a list of (name, value) + pairs. See [the header normalization rules](#headers-format) for + details. + http_version: The HTTP protocol version, represented as a byte string + like `b"1.1"`. See + [the HTTP version normalization rules](#http-version-format) for + details. + reason: The reason phrase of this response, as a byte string. For + example: `b"OK"`, or `b"Not Found"`. + """ + + def __post_init__(self) -> None: + if not (200 <= self.status_code < 1000): + raise LocalProtocolError(f"Response status_code should be in range [200, 1000), not {self.status_code}") + + # This is an unhashable type. + __hash__ = None + + +@final +@dataclass(init=False, frozen=True, slots=True) +class Data(Event): + """Part of an HTTP message body. + + Attributes: + data: + A [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object) + containing part of a message body. Or, if using the + `combine=False` argument to + [`Connection.send()`][h11_mypyc.Connection.send], then any object that + your socket writing code knows what to do with, and for which + calling [`len()`][len] returns the number of bytes that will be + written -- see [Support for `sendfile()`](#sendfile) for details. + chunk_start: A marker that indicates whether this data object is from + the start of a chunked transfer encoding chunk. This field is + ignored when a `Data` event is provided to + [`Connection.send()`][h11_mypyc.Connection.send]: it is only valid on + events emitted from + [`Connection.next_event()`][h11_mypyc.Connection.next_event]. You + probably shouldn't use this attribute at all; see + [Chunked Transfer Encoding Delimiters](#chunk-delimiters-are-bad) + for details. + chunk_end: A marker that indicates whether this data object is the + last for a given chunked transfer encoding chunk. This field is + ignored when a `Data` event is provided to + [`Connection.send()`][h11_mypyc.Connection.send]: it is only valid on + events emitted from + [`Connection.next_event()`][h11_mypyc.Connection.next_event]. You + probably shouldn't use this attribute at all; see + [Chunked Transfer Encoding Delimiters](#chunk-delimiters-are-bad) + for details. + """ + + # Untyped on purpose: send_with_data_passthrough accepts an arbitrary + # placeholder object here for sendfile (see the "sendfile" docs), and a narrower + # annotation would be enforced at runtime and reject it. + data: Any + chunk_start: bool + chunk_end: bool + + def __init__(self, data: Any, chunk_start: bool = False, chunk_end: bool = False) -> None: + object.__setattr__(self, "data", data) + object.__setattr__(self, "chunk_start", chunk_start) + object.__setattr__(self, "chunk_end", chunk_end) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +# XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that +# are forbidden to be sent in a trailer, since processing them as if they were +# present in the header section might bypass external security filters." +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part +# Unfortunately, the list of forbidden fields is long and vague :-/ +@final +@dataclass(init=False, frozen=True, slots=True) +class EndOfMessage(Event): + """The end of an HTTP message. + + Attributes: + headers: Any trailing headers attached to this message, represented as + a list of (name, value) pairs; defaults to `[]`. See + [the header normalization rules](#headers-format) for details. + + Must be empty unless `Transfer-Encoding: chunked` is in use. + """ + + headers: Headers + + def __init__( + self, + *, + headers: Headers | list[tuple[bytes, bytes]] | list[tuple[str, str]] | None = None, + _parsed: bool = False, + ) -> None: + if headers is None: + headers = Headers([]) + elif not isinstance(headers, Headers): + headers = normalize_and_validate(headers, _parsed=_parsed) + + object.__setattr__(self, "headers", headers) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@final +@dataclass(frozen=True, slots=True) +class ConnectionClosed(Event): + """This event indicates that the sender has closed their outgoing + connection. + + Note that this does not necessarily mean that they can't *receive* further + data, because TCP connections are composed of two one-way channels which + can be closed independently. See [Closing connections](#closing) for + details. + + This event has no fields. + """ + + pass diff --git a/h11/_headers.py b/h11_mypyc/_headers.py similarity index 73% rename from h11/_headers.py rename to h11_mypyc/_headers.py index 31da3e2..e7355cc 100644 --- a/h11/_headers.py +++ b/h11_mypyc/_headers.py @@ -1,16 +1,12 @@ import re -from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union +from collections.abc import Iterator, Sequence +from typing import TYPE_CHECKING, Literal, final, overload -from ._abnf import field_name, field_value -from ._util import bytesify, LocalProtocolError, validate +from h11_mypyc._abnf import field_name, field_value +from h11_mypyc._util import LocalProtocolError, bytesify, validate if TYPE_CHECKING: - from ._events import Request - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore + from h11_mypyc._events import Request CONTENT_LENGTH_MAX_DIGITS = 20 # allow up to 1 billion TB - 1 @@ -73,8 +69,17 @@ _field_name_re = re.compile(field_name.encode("ascii")) _field_value_re = re.compile(field_value.encode("ascii")) +_FIELD_NAME_OK = bytes( + b + for b in range(256) + if bytes([b]) in b"-!#$%&'*+.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) +_FIELD_VALUE_OK = bytes(b for b in range(256) if b not in (0x00, 0x0A, 0x0B, 0x0C, 0x0D)) +_WS_BYTES = (0x20, 0x09) + -class Headers(Sequence[Tuple[bytes, bytes]]): +@final +class Headers: """ A list-like interface that allows iterating over headers as byte-pairs of (lowercased-name, value). @@ -101,7 +106,7 @@ class Headers(Sequence[Tuple[bytes, bytes]]): __slots__ = "_full_items" - def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None: + def __init__(self, full_items: list[tuple[bytes, bytes, bytes]]) -> None: self._full_items = full_items def __bool__(self) -> bool: @@ -114,44 +119,57 @@ def __len__(self) -> int: return len(self._full_items) def __repr__(self) -> str: - return "" % repr(list(self)) + return f"" - def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override] + def __getitem__(self, idx: int) -> tuple[bytes, bytes]: _, name, value = self._full_items[idx] return (name, value) - def raw_items(self) -> List[Tuple[bytes, bytes]]: + # Supplied by hand because Headers cannot inherit Sequence -- see below. + def __iter__(self) -> Iterator[tuple[bytes, bytes]]: + for _, name, value in self._full_items: + yield (name, value) + + def __reversed__(self) -> Iterator[tuple[bytes, bytes]]: + for _, name, value in reversed(self._full_items): + yield (name, value) + + def __contains__(self, item: object) -> bool: + return any(pair == item for pair in self) + + def index(self, value: tuple[bytes, bytes], start: int = 0, stop: int | None = None) -> int: + pairs = list(self)[start : len(self) if stop is None else stop] + return pairs.index(value) + start + + def count(self, value: tuple[bytes, bytes]) -> int: + return sum(1 for pair in self if pair == value) + + def raw_items(self) -> list[tuple[bytes, bytes]]: return [(raw_name, value) for raw_name, _, value in self._full_items] -HeaderTypes = Union[ - List[Tuple[bytes, bytes]], - List[Tuple[bytes, str]], - List[Tuple[str, bytes]], - List[Tuple[str, str]], -] +# Registered rather than inherited: a compiled class inheriting an ABC shares +# the base's _abc_impl, and so its isinstance cache, which silently corrupts +# isinstance in both directions process-wide. Do not turn this into a base class. +Sequence.register(Headers) + + +HeaderTypes = list[tuple[bytes, bytes]] | list[tuple[bytes, str]] | list[tuple[str, bytes]] | list[tuple[str, str]] @overload -def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers: - ... +def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers: ... @overload -def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers: - ... +def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers: ... @overload -def normalize_and_validate( - headers: Union[Headers, HeaderTypes], _parsed: bool = False -) -> Headers: - ... +def normalize_and_validate(headers: Headers | HeaderTypes, _parsed: bool = False) -> Headers: ... -def normalize_and_validate( - headers: Union[Headers, HeaderTypes], _parsed: bool = False -) -> Headers: +def normalize_and_validate(headers: Headers | HeaderTypes, _parsed: bool = False) -> Headers: new_headers = [] seen_content_length = None saw_transfer_encoding = False @@ -162,8 +180,15 @@ def normalize_and_validate( if not _parsed: name = bytesify(name) value = bytesify(value) - validate(_field_name_re, name, "Illegal header name {!r}", name) - validate(_field_value_re, value, "Illegal header value {!r}", value) + # Byte-class equivalents of _field_name_re / _field_value_re, which + # are pure character classes and so can be checked with a single + # C-level pass. _field_value_re additionally forbids leading and + # trailing whitespace. tests/test_headers.py pins this to the ABNF + # regexes -- do not change one side without the other. + if not name or name.translate(None, _FIELD_NAME_OK): + raise LocalProtocolError(f"Illegal header name {name!r}") + if value and (value.translate(None, _FIELD_VALUE_OK) or value[0] in _WS_BYTES or value[-1] in _WS_BYTES): + raise LocalProtocolError(f"Illegal header value {value!r}") assert isinstance(name, bytes) assert isinstance(value, bytes) @@ -188,9 +213,7 @@ def normalize_and_validate( # Implemented)." # https://tools.ietf.org/html/rfc7230#section-3.3.1 if saw_transfer_encoding: - raise LocalProtocolError( - "multiple Transfer-Encoding headers", error_status_hint=501 - ) + raise LocalProtocolError("multiple Transfer-Encoding headers", error_status_hint=501) # "All transfer-coding names are case-insensitive" # -- https://tools.ietf.org/html/rfc7230#section-4 value = value.lower() @@ -206,7 +229,7 @@ def normalize_and_validate( return Headers(new_headers) -def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: +def get_comma_header(headers: Headers, name: bytes) -> list[bytes]: # Should only be used for headers whose value is a list of # comma-separated, case-insensitive values. # @@ -242,7 +265,7 @@ def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: # Expect: the only legal value is the literal string # "100-continue". Splitting on commas is harmless. Case insensitive. # - out: List[bytes] = [] + out: list[bytes] = [] for _, found_name, found_raw_value in headers._full_items: if found_name == name: found_raw_value = found_raw_value.lower() @@ -253,7 +276,7 @@ def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: return out -def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> Headers: +def set_comma_header(headers: Headers, name: bytes, new_values: list[bytes]) -> Headers: # The header name `name` is expected to be lower-case bytes. # # Note that when we store the header we use title casing for the header @@ -263,7 +286,7 @@ def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> # here given the cases where we're using `set_comma_header`... # # Connection, Content-Length, Transfer-Encoding. - new_headers: List[Tuple[bytes, bytes]] = [] + new_headers: list[tuple[bytes, bytes]] = [] for found_raw_name, found_name, found_raw_value in headers._full_items: if found_name != name: new_headers.append((found_raw_name, found_raw_value)) diff --git a/h11/_readers.py b/h11_mypyc/_readers.py similarity index 70% rename from h11/_readers.py rename to h11_mypyc/_readers.py index 576804c..59716ba 100644 --- a/h11/_readers.py +++ b/h11_mypyc/_readers.py @@ -17,12 +17,13 @@ # - or, for body readers, a dict of per-framing reader factories import re -from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union +from collections.abc import Callable, Iterable +from typing import Any, NoReturn -from ._abnf import chunk_header, header_field, request_line, status_line -from ._events import Data, EndOfMessage, InformationalResponse, Request, Response -from ._receivebuffer import ReceiveBuffer -from ._state import ( +from h11_mypyc._abnf import chunk_header, header_field, request_line, status_line +from h11_mypyc._events import Data, EndOfMessage, InformationalResponse, Request, Response +from h11_mypyc._receivebuffer import ReceiveBuffer +from h11_mypyc._state import ( CLIENT, CLOSED, DONE, @@ -32,27 +33,36 @@ SEND_RESPONSE, SERVER, ) -from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate +from h11_mypyc._util import ( + ByteLike, + LocalProtocolError, + RemoteProtocolError, + Sentinel, + match_or_raise, + validate_and_group, +) __all__ = ["READERS"] header_field_re = re.compile(header_field.encode("ascii")) -obs_fold_re = re.compile(rb"[ \t]+") -def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]: +def _obsolete_line_fold(lines: Iterable[ByteLike]) -> Iterable[ByteLike]: it = iter(lines) - last: Optional[bytes] = None + last: ByteLike | None = None for line in it: - match = obs_fold_re.match(line) - if match: + # obs-fold continuation lines start with a space or tab. They are + # deprecated and vanishingly rare, so test the first byte rather than + # run a regex over every ordinary header line. lstrip() then removes + # exactly the run that the old `[ \t]+` match consumed. + if line and line[0] in (0x20, 0x09): if last is None: raise LocalProtocolError("continuation line at start of headers") if not isinstance(last, bytearray): # Cast to a mutable type, avoiding copy on append to ensure O(n) time last = bytearray(last) last += b" " - last += line[match.end() :] + last += line.lstrip(b" \t") else: if last is not None: yield last @@ -62,17 +72,17 @@ def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]: def _decode_header_lines( - lines: Iterable[bytes], -) -> Iterable[Tuple[bytes, bytes]]: + lines: Iterable[ByteLike], +) -> Iterable[tuple[bytes, bytes]]: for line in _obsolete_line_fold(lines): - matches = validate(header_field_re, line, "illegal header line: {!r}", line) - yield (matches["field_name"], matches["field_value"]) + match = match_or_raise(header_field_re, line, "illegal header line: {!r}", line) + yield (match["field_name"], match["field_value"]) request_line_re = re.compile(request_line.encode("ascii")) -def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: +def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Request | None: lines = buf.maybe_extract_lines() if lines is None: if buf.is_next_line_obviously_invalid_request_line(): @@ -80,12 +90,8 @@ def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: return None if not lines: raise LocalProtocolError("no request line received") - matches = validate( - request_line_re, lines[0], "illegal request line: {!r}", lines[0] - ) - return Request( - headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches - ) + matches = validate_and_group(request_line_re, lines[0], "illegal request line: {!r}", lines[0]) + return Request(headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches) status_line_re = re.compile(status_line.encode("ascii")) @@ -93,7 +99,7 @@ def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: def maybe_read_from_SEND_RESPONSE_server( buf: ReceiveBuffer, -) -> Union[InformationalResponse, Response, None]: +) -> InformationalResponse | Response | None: lines = buf.maybe_extract_lines() if lines is None: if buf.is_next_line_obviously_invalid_request_line(): @@ -101,15 +107,12 @@ def maybe_read_from_SEND_RESPONSE_server( return None if not lines: raise LocalProtocolError("no response line received") - matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0]) - http_version = ( - b"1.1" if matches["http_version"] is None else matches["http_version"] - ) - reason = b"" if matches["reason"] is None else matches["reason"] + matches = validate_and_group(status_line_re, lines[0], "illegal status line: {!r}", lines[0]) + # reason is optional -- some servers omit the phrase. See _abnf.status_line. + http_version = matches.get("http_version", b"1.1") + reason = matches.get("reason", b"") status_code = int(matches["status_code"]) - class_: Union[Type[InformationalResponse], Type[Response]] = ( - InformationalResponse if status_code < 200 else Response - ) + class_: type[InformationalResponse] | type[Response] = InformationalResponse if status_code < 200 else Response return class_( headers=list(_decode_header_lines(lines[1:])), _parsed=True, @@ -124,7 +127,7 @@ def __init__(self, length: int) -> None: self._length = length self._remaining = length - def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + def __call__(self, buf: ReceiveBuffer) -> Data | EndOfMessage | None: if self._remaining == 0: return EndOfMessage() data = buf.maybe_extract_at_most(self._remaining) @@ -136,9 +139,7 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: def read_eof(self) -> NoReturn: raise RemoteProtocolError( "peer closed connection without sending complete message body " - "(received {} bytes, expected {})".format( - self._length - self._remaining, self._length - ) + f"(received {self._length - self._remaining} bytes, expected {self._length})" ) @@ -153,7 +154,7 @@ def __init__(self) -> None: self._bytes_to_discard = b"" self._reading_trailer = False - def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + def __call__(self, buf: ReceiveBuffer) -> Data | EndOfMessage | None: if self._reading_trailer: lines = buf.maybe_extract_lines() if lines is None: @@ -164,9 +165,7 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: if data is None: return None if data != self._bytes_to_discard[: len(data)]: - raise LocalProtocolError( - f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})" - ) + raise LocalProtocolError(f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})") self._bytes_to_discard = self._bytes_to_discard[len(data) :] if self._bytes_to_discard: return None @@ -177,14 +176,14 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: chunk_header = buf.maybe_extract_next_line() if chunk_header is None: return None - matches = validate( + match = match_or_raise( chunk_header_re, chunk_header, "illegal chunk header: {!r}", chunk_header, ) # XX FIXME: we discard chunk extensions. Does anyone care? - self._bytes_in_chunk = int(matches["chunk_size"], base=16) + self._bytes_in_chunk = int(match["chunk_size"], base=16) if self._bytes_in_chunk == 0: self._reading_trailer = True return self(buf) @@ -205,13 +204,12 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: def read_eof(self) -> NoReturn: raise RemoteProtocolError( - "peer closed connection without sending complete message body " - "(incomplete chunked read)" + "peer closed connection without sending complete message body (incomplete chunked read)" ) class Http10Reader: - def __call__(self, buf: ReceiveBuffer) -> Optional[Data]: + def __call__(self, buf: ReceiveBuffer) -> Data | None: data = buf.maybe_extract_at_most(999999999) if data is None: return None @@ -227,9 +225,9 @@ def expect_nothing(buf: ReceiveBuffer) -> None: return None -ReadersType = Dict[ - Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]], - Union[Callable[..., Any], Dict[str, Callable[..., Any]]], +ReadersType = dict[ + type[Sentinel] | tuple[type[Sentinel], type[Sentinel]], + Callable[..., Any] | dict[str, Callable[..., Any]], ] READERS: ReadersType = { diff --git a/h11/_receivebuffer.py b/h11_mypyc/_receivebuffer.py similarity index 93% rename from h11/_receivebuffer.py rename to h11_mypyc/_receivebuffer.py index e5c4e08..318a571 100644 --- a/h11/_receivebuffer.py +++ b/h11_mypyc/_receivebuffer.py @@ -1,6 +1,5 @@ import re -import sys -from typing import List, Optional, Union +from typing import final __all__ = ["ReceiveBuffer"] @@ -44,13 +43,14 @@ blank_line_regex = re.compile(b"\n\r?\n", re.MULTILINE) +@final class ReceiveBuffer: def __init__(self) -> None: self._data = bytearray() self._next_line_search = 0 self._multiple_lines_search = 0 - def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer": + def __iadd__(self, byteslike: bytes | bytearray) -> "ReceiveBuffer": self._data += byteslike return self @@ -74,7 +74,7 @@ def _extract(self, count: int) -> bytearray: return out - def maybe_extract_at_most(self, count: int) -> Optional[bytearray]: + def maybe_extract_at_most(self, count: int) -> bytearray | None: """ Extract a fixed number of bytes from the buffer. """ @@ -84,7 +84,7 @@ def maybe_extract_at_most(self, count: int) -> Optional[bytearray]: return self._extract(count) - def maybe_extract_next_line(self) -> Optional[bytearray]: + def maybe_extract_next_line(self) -> bytearray | None: """ Extract the first line, if it is completed in the buffer. """ @@ -101,7 +101,7 @@ def maybe_extract_next_line(self) -> Optional[bytearray]: return self._extract(idx) - def maybe_extract_lines(self) -> Optional[List[bytearray]]: + def maybe_extract_lines(self) -> list[bytearray] | None: """ Extract everything up to the first blank line, and return a list of lines. """ diff --git a/h11/_state.py b/h11_mypyc/_state.py similarity index 84% rename from h11/_state.py rename to h11_mypyc/_state.py index 3ad444b..89fa434 100644 --- a/h11/_state.py +++ b/h11_mypyc/_state.py @@ -110,10 +110,18 @@ # tables. But it can't automatically read the transitions that are written # directly in Python code. So if you touch those, you need to also update the # script to keep it in sync! -from typing import cast, Dict, Optional, Set, Tuple, Type, Union - -from ._events import * -from ._util import LocalProtocolError, Sentinel +from typing import cast, final + +from h11_mypyc._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from h11_mypyc._util import LocalProtocolError, Sentinel # Everything in __all__ gets re-exported as part of the h11 public API. __all__ = [ @@ -131,65 +139,65 @@ ] -class CLIENT(Sentinel, metaclass=Sentinel): +class CLIENT(Sentinel): pass -class SERVER(Sentinel, metaclass=Sentinel): +class SERVER(Sentinel): pass # States -class IDLE(Sentinel, metaclass=Sentinel): +class IDLE(Sentinel): pass -class SEND_RESPONSE(Sentinel, metaclass=Sentinel): +class SEND_RESPONSE(Sentinel): pass -class SEND_BODY(Sentinel, metaclass=Sentinel): +class SEND_BODY(Sentinel): pass -class DONE(Sentinel, metaclass=Sentinel): +class DONE(Sentinel): pass -class MUST_CLOSE(Sentinel, metaclass=Sentinel): +class MUST_CLOSE(Sentinel): pass -class CLOSED(Sentinel, metaclass=Sentinel): +class CLOSED(Sentinel): pass -class ERROR(Sentinel, metaclass=Sentinel): +class ERROR(Sentinel): pass # Switch types -class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel): +class MIGHT_SWITCH_PROTOCOL(Sentinel): pass -class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel): +class SWITCHED_PROTOCOL(Sentinel): pass -class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel): +class _SWITCH_UPGRADE(Sentinel): pass -class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): +class _SWITCH_CONNECT(Sentinel): pass -EventTransitionType = Dict[ - Type[Sentinel], - Dict[ - Type[Sentinel], - Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]], +EventTransitionType = dict[ + type[Sentinel], + dict[ + type[Sentinel], + dict[type[Event] | tuple[type[Event], type[Sentinel]], type[Sentinel]], ], ] @@ -226,9 +234,7 @@ class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): }, } -StateTransitionType = Dict[ - Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]] -] +StateTransitionType = dict[tuple[type[Sentinel], type[Sentinel]], dict[type[Sentinel], type[Sentinel]]] # NB: there are also some special-case state-triggered transitions hard-coded # into _fire_state_triggered_transitions below. @@ -246,6 +252,7 @@ class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): } +@final class ConnectionState: def __init__(self) -> None: # Extra bits of state that don't quite fit into the state model. @@ -256,11 +263,11 @@ def __init__(self) -> None: # This is a subset of {UPGRADE, CONNECT}, containing the proposals # made by the client for switching protocols. - self.pending_switch_proposals: Set[Type[Sentinel]] = set() + self.pending_switch_proposals: set[type[Sentinel]] = set() - self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE} + self.states: dict[type[Sentinel], type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE} - def process_error(self, role: Type[Sentinel]) -> None: + def process_error(self, role: type[Sentinel]) -> None: self.states[role] = ERROR self._fire_state_triggered_transitions() @@ -268,23 +275,21 @@ def process_keep_alive_disabled(self) -> None: self.keep_alive = False self._fire_state_triggered_transitions() - def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None: + def process_client_switch_proposal(self, switch_event: type[Sentinel]) -> None: self.pending_switch_proposals.add(switch_event) self._fire_state_triggered_transitions() def process_event( self, - role: Type[Sentinel], - event_type: Type[Event], - server_switch_event: Optional[Type[Sentinel]] = None, + role: type[Sentinel], + event_type: type[Event], + server_switch_event: type[Sentinel] | None = None, ) -> None: - _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type + _event_type: type[Event] | tuple[type[Event], type[Sentinel]] = event_type if server_switch_event is not None: assert role is SERVER if server_switch_event not in self.pending_switch_proposals: - raise LocalProtocolError( - "Received server _SWITCH_UPGRADE event without a pending proposal" - ) + raise LocalProtocolError("Received server _SWITCH_UPGRADE event without a pending proposal") _event_type = (event_type, server_switch_event) if server_switch_event is None and _event_type is Response: self.pending_switch_proposals = set() @@ -298,18 +303,17 @@ def process_event( def _fire_event_triggered_transitions( self, - role: Type[Sentinel], - event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], + role: type[Sentinel], + event_type: type[Event] | tuple[type[Event], type[Sentinel]], ) -> None: state = self.states[role] try: new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type] except KeyError: - event_type = cast(Type[Event], event_type) + event_type = cast(type[Event], event_type) raise LocalProtocolError( - "can't handle event type {} when role={} and state={}".format( - event_type.__name__, role, self.states[role] - ) + f"can't handle event type {event_type.__name__} " + f"when role={role.__name__} and state={self.states[role].__name__}" ) from None self.states[role] = new_state @@ -355,9 +359,8 @@ def _fire_state_triggered_transitions(self) -> None: def start_next_cycle(self) -> None: if self.states != {CLIENT: DONE, SERVER: DONE}: - raise LocalProtocolError( - f"not in a reusable state. self.states={self.states}" - ) + states = ", ".join(f"{role.__name__}: {state.__name__}" for role, state in self.states.items()) + raise LocalProtocolError(f"not in a reusable state. self.states={{{states}}}") # Can't reach DONE/DONE with any of these active, but still, let's be # sure. assert self.keep_alive diff --git a/h11/_util.py b/h11_mypyc/_util.py similarity index 50% rename from h11/_util.py rename to h11_mypyc/_util.py index 6718445..5e0519a 100644 --- a/h11/_util.py +++ b/h11_mypyc/_util.py @@ -1,41 +1,51 @@ -from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union +from re import Match, Pattern +from typing import Any, NoReturn, TypeAlias __all__ = [ + "ByteLike", + "Bytesifiable", "ProtocolError", "LocalProtocolError", "RemoteProtocolError", + "match_or_raise", "validate", + "validate_and_group", "bytesify", ] +# Data off the wire is bytearray: ReceiveBuffer slices without copying. +ByteLike: TypeAlias = bytes | bytearray -class ProtocolError(Exception): - """Exception indicating a violation of the HTTP/1.1 protocol. +# What the event constructors accept and hand to bytesify(). +Bytesifiable: TypeAlias = bytes | bytearray | memoryview | str - This as an abstract base class, with two concrete base classes: - :exc:`LocalProtocolError`, which indicates that you tried to do something - that HTTP/1.1 says is illegal, and :exc:`RemoteProtocolError`, which - indicates that the remote peer tried to do something that HTTP/1.1 says is - illegal. See :ref:`error-handling` for details. - In addition to the normal :exc:`Exception` features, it has one attribute: +class ProtocolError(Exception): + """Exception indicating a violation of the HTTP/1.1 protocol. - .. attribute:: error_status_hint + This is an abstract base class, with two concrete subclasses: + [`LocalProtocolError`][h11_mypyc.LocalProtocolError], which indicates that you + tried to do something that HTTP/1.1 says is illegal, and + [`RemoteProtocolError`][h11_mypyc.RemoteProtocolError], which indicates that the + remote peer tried to do something that HTTP/1.1 says is illegal. See + [Error handling](#error-handling) for details. - This gives a suggestion as to what status code a server might use if - this error occurred as part of a request. + In addition to the normal [`Exception`][] features, it has one attribute. - For a :exc:`RemoteProtocolError`, this is useful as a suggestion for - how you might want to respond to a misbehaving peer, if you're - implementing a server. + Attributes: + error_status_hint: A suggestion as to what status code a server might + use if this error occurred as part of a request. - For a :exc:`LocalProtocolError`, this can be taken as a suggestion for - how your peer might have responded to *you* if h11 had allowed you to - continue. + For a [`RemoteProtocolError`][h11_mypyc.RemoteProtocolError], this is + useful as a suggestion for how you might want to respond to a + misbehaving peer, if you're implementing a server. - The default is 400 Bad Request, a generic catch-all for protocol - violations. + For a [`LocalProtocolError`][h11_mypyc.LocalProtocolError], this can be + taken as a suggestion for how your peer might have responded to + *you* if h11-mypyc had allowed you to continue. + The default is 400 Bad Request, a generic catch-all for protocol + violations. """ def __init__(self, msg: str, error_status_hint: int = 400) -> None: @@ -81,50 +91,52 @@ class RemoteProtocolError(ProtocolError): pass -def validate( - regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any -) -> Dict[str, bytes]: +def validate(regex: Pattern[bytes], data: ByteLike, msg: str = "malformed data", *format_args: Any) -> None: + if not regex.fullmatch(data): + if format_args: + msg = msg.format(*format_args) + raise LocalProtocolError(msg) + + +def match_or_raise( + regex: Pattern[bytes], data: ByteLike, msg: str = "malformed data", *format_args: Any +) -> Match[bytes]: + # For callers that want one or two named groups. Reading them off the match + # avoids the dict that validate_and_group() builds per call. match = regex.fullmatch(data) if not match: if format_args: msg = msg.format(*format_args) raise LocalProtocolError(msg) - return match.groupdict() + return match + + +def validate_and_group( + regex: Pattern[bytes], data: ByteLike, msg: str = "malformed data", *format_args: Any +) -> dict[str, bytes]: + match = match_or_raise(regex, data, msg, *format_args) + # Unmatched optional groups are None; drop them so the return type holds. + # Callers read optional groups with .get() and a default. + return {name: value for name, value in match.groupdict().items() if value is not None} # Sentinel values # # - Inherit identity-based comparison and hashing from object -# - Have a nice repr -# - Have a *bonus property*: type(sentinel) is sentinel +# - Are used as classes, never instantiated: `conn.our_state is IDLE` # -# The bonus property is useful if you want to take the return value from -# next_event() and do some sort of dispatch based on type(event). +# Plain base class, not a metaclass: mypyc cannot compile custom metaclasses. +# Error messages spell the names out with __name__ instead of relying on repr. -_T_Sentinel = TypeVar("_T_Sentinel", bound="Sentinel") - -class Sentinel(type): - def __new__( - cls: Type[_T_Sentinel], - name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - **kwds: Any - ) -> _T_Sentinel: - assert bases == (Sentinel,) - v = super().__new__(cls, name, bases, namespace, **kwds) - v.__class__ = v # type: ignore - return v - - def __repr__(self) -> str: - return self.__name__ +class Sentinel: + pass # Used for methods, request targets, HTTP versions, header names, and header # values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always # returns bytes. -def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes: +def bytesify(s: Bytesifiable | int) -> bytes: # Fast-path: if type(s) is bytes: return s diff --git a/h11_mypyc/_version.py b/h11_mypyc/_version.py new file mode 100644 index 0000000..fd86b3e --- /dev/null +++ b/h11_mypyc/_version.py @@ -0,0 +1 @@ +__version__ = "0.17.0" diff --git a/h11/_writers.py b/h11_mypyc/_writers.py similarity index 79% rename from h11/_writers.py rename to h11_mypyc/_writers.py index 939cdb9..b85fe89 100644 --- a/h11/_writers.py +++ b/h11_mypyc/_writers.py @@ -7,16 +7,19 @@ # - a writer # - or, for body writers, a dict of framin-dependent writer factories -from typing import Any, Callable, Dict, List, Tuple, Type, Union +from collections.abc import Callable +from typing import Any -from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response -from ._headers import Headers -from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER -from ._util import LocalProtocolError, Sentinel +from h11_mypyc._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response +from h11_mypyc._headers import Headers +from h11_mypyc._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER +from h11_mypyc._util import LocalProtocolError, Sentinel __all__ = ["WRITERS"] -Writer = Callable[[bytes], Any] +# Not narrowed to bytes: the sendfile placeholder from Data.data is handed +# straight to `write`, and a narrower annotation is enforced at runtime. +Writer = Callable[[Any], Any] def write_headers(headers: Headers, write: Writer) -> None: @@ -41,9 +44,7 @@ def write_request(request: Request, write: Writer) -> None: # Shared between InformationalResponse and Response -def write_any_response( - response: Union[InformationalResponse, Response], write: Writer -) -> None: +def write_any_response(response: InformationalResponse | Response, write: Writer) -> None: if response.http_version != b"1.1": raise LocalProtocolError("I only send HTTP/1.1") status_bytes = str(response.status_code).encode("ascii") @@ -68,7 +69,7 @@ def __call__(self, event: Event, write: Writer) -> None: else: # pragma: no cover assert False - def send_data(self, data: bytes, write: Writer) -> None: + def send_data(self, data: Any, write: Writer) -> None: pass def send_eom(self, headers: Headers, write: Writer) -> None: @@ -85,7 +86,7 @@ class ContentLengthWriter(BodyWriter): def __init__(self, length: int) -> None: self._length = length - def send_data(self, data: bytes, write: Writer) -> None: + def send_data(self, data: Any, write: Writer) -> None: self._length -= len(data) if self._length < 0: raise LocalProtocolError("Too much data for declared Content-Length") @@ -99,7 +100,7 @@ def send_eom(self, headers: Headers, write: Writer) -> None: class ChunkedWriter(BodyWriter): - def send_data(self, data: bytes, write: Writer) -> None: + def send_data(self, data: Any, write: Writer) -> None: # if we encoded 0-length data in the naive way, it would look like an # end-of-message. if not data: @@ -114,7 +115,7 @@ def send_eom(self, headers: Headers, write: Writer) -> None: class Http10Writer(BodyWriter): - def send_data(self, data: bytes, write: Writer) -> None: + def send_data(self, data: Any, write: Writer) -> None: write(data) def send_eom(self, headers: Headers, write: Writer) -> None: @@ -124,13 +125,11 @@ def send_eom(self, headers: Headers, write: Writer) -> None: # Connection: close machinery -WritersType = Dict[ - Union[Tuple[Type[Sentinel], Type[Sentinel]], Type[Sentinel]], - Union[ - Dict[str, Type[BodyWriter]], - Callable[[Union[InformationalResponse, Response], Writer], None], - Callable[[Request, Writer], None], - ], +WritersType = dict[ + tuple[type[Sentinel], type[Sentinel]] | type[Sentinel], + dict[str, type[BodyWriter]] + | Callable[[InformationalResponse | Response, Writer], None] + | Callable[[Request, Writer], None], ] WRITERS: WritersType = { diff --git a/h11/py.typed b/h11_mypyc/py.typed similarity index 100% rename from h11/py.typed rename to h11_mypyc/py.typed diff --git a/newsfragments/.gitkeep b/newsfragments/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/newsfragments/README.rst b/newsfragments/README.rst deleted file mode 100644 index d01e930..0000000 --- a/newsfragments/README.rst +++ /dev/null @@ -1,27 +0,0 @@ -This directory collects "newsfragments": short files that each contain -a snippet of ReST-formatted text that will be added to the next -release notes. This should be a description of aspects of the change -(if any) that are relevant to users. (This contrasts with your commit -message and PR description, which are a description of the change as -relevant to people working on the code itself.) - -Each file should be named like ``..rst``, where -```` is an issue numbers, and ```` is one of: - -* ``feature`` -* ``bugfix`` -* ``doc`` -* ``removal`` -* ``misc`` - -So for example: ``123.feature.rst``, ``456.bugfix.rst`` - -If your PR fixes an issue, use that number here. If there is no issue, -then after you submit the PR and get the PR number you can add a -newsfragment using that instead. - -Note that the ``towncrier`` tool will automatically -reflow your text, so don't try to do any fancy formatting. You can -install ``towncrier`` and then run ``towncrier --draft`` if you want -to get a preview of how your change will look in the final release -notes. diff --git a/notes.org b/notes.org deleted file mode 100644 index 36fd741..0000000 --- a/notes.org +++ /dev/null @@ -1,144 +0,0 @@ -Possible API breaking changes: - -- pondering moving headers to be (default)dict of lowercase bytestrings -> ordered lists of bytestrings - - I guess we should get some benchmarks/profiles first, since one of the motivations would be to eliminate all these linear scans and reallocations we use when dealing with headers - - - orrrrr... join most headers on "," and join Set-Cookie on ";" (HTTP/2 spec explicitly allows this!), and then we can just use a freakin' (case insensitive) dict. Terrible idea? or awesome idea? - - - argh, no, HTTP/2 allows joining *Cookie:* on ";". Set-Cookie header syntax makes it impossible to join them in any way :-( - -- pondering whether to adopt the HTTP/2 style of sticking request/response line information directly into the header dict. - - Advantages: - - code that wants to handle HTTP/2 will need to handle this anyway, might make it easier to write dual-stack clients/servers - - - provides a more useful downstream representation for request targets that are in full-fledged http://... form. - - I'm of mixed mind about how much these matter though -- HTTP/1.1 servers are supposedly required to support them, but HTTP/1.1 clients are forbidden to send them, and in practice the transition that the HTTP/1.1 spec envisions to clients sending these all the time is... just never going to happen. So I like following specs, but in reality servers never have and never will need to support these, making it feel a bit silly. They do get sent to proxies, though -- maybe someone wants to use h11 to implement a proxy? - -for better tests: -https://github.com/kevin1024/pytest-httpbin -http://pathod.net/ - -XX TODO: - A server MUST NOT send a Transfer-Encoding header field in any - response with a status code of 1xx (Informational) or 204 (No - Content). A server MUST NOT send a Transfer-Encoding header field in - any 2xx (Successful) response to a CONNECT request (Section 4.3.6 of - [RFC7231]). - - A server MUST NOT send a Content-Length header field in any response - with a status code of 1xx (Informational) or 204 (No Content). A - server MUST NOT send a Content-Length header field in any 2xx - (Successful) response to a CONNECT request (Section 4.3.6 of - [RFC7231]). - -http://coad.measurement-factory.com/details.html - -* notes on URLs - -there are multiple not fully consistent specs - -[[https://tools.ietf.org/html/rfc3986][RFC 3986]] is the basic spec that RFC 7230 refers to -RFC 3987 adds "internationalized" support -RFC 6874 revises RFC 3986 a bit for "IPv6 zone support" -- golang has some code to handle this - -and then there's the [[https://url.spec.whatwg.org/][WHATWG URL spec]] - -some commentary on this: -https://daniel.haxx.se/blog/2016/05/11/my-url-isnt-your-url/ - -note that curl has been forced to handle non-RFC 3986-compliant (but WHATWG URL-compliant) URLs in Location: headers -- specifically ones containing weird numbers of slashes, and ones containing spaces (!), and maybe UTF-8 and other such fun - -https://news.ycombinator.com/item?id=11673058 -"I don't think cURL implements this percent encoding yet - instead, it sends out binary paths on UTF-8 locale and Linux likewise." -- https://news.ycombinator.com/item?id=11674778 - -also: -https://github.com/bagder/docs/blob/master/URL-interop.md -"This document is an attempt to describe where and how RFC 3986 (86), RFC 3987 (87) and the WHATWG URL Specification (TWUS) differ. This might be useful input when trying to interop with URLs on the modern Internet." - -** looking at the go http parser - -spaces in HTTP/1.1 request-lines are definitely verboten -- e.g. here's the go http server code for splitting a request line (parseRequestLine), which assumes the second space represents the end of the target: - https://golang.org/src/net/http/request.go#L680 - -OTOH if we scroll down to readRequest, we see that they have a special case where for CONNECT targets, they accept either host:port OR /path/with/slash (wtf): - - // CONNECT requests are used two different ways, and neither uses a full URL: - // The standard use is to tunnel HTTPS through an HTTP proxy. - // It looks like "CONNECT www.google.com:443 HTTP/1.1", and the parameter is - // just the authority section of a URL. This information should go in req.URL.Host. - // - // The net/rpc package also uses CONNECT, but there the parameter is a path - // that starts with a slash. It can be parsed with the regular URL parser, - // and the path will end up in req.URL.Path, where it needs to be in order for - // RPC to work. - -other interesting things: -- they have a special removeZone function to handle [[https://tools.ietf.org/html/rfc6874][RFC 6874]], which revises RFC 3986 -- they provide both a parsed URL and a raw string containing whatever was in the request line - -** experiment to check how firefox handles UTF-8 in URLs: - -$ socat - TCP-LISTEN:12345 -then browse to http://localhost:12345/✔ - -produces: - -GET /%E2%9C%94 HTTP/1.1 -Host: localhost:12345 -User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0 -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -Accept-Language: en-US,en;q=0.5 -Accept-Encoding: gzip, deflate -DNT: 1 -Connection: keep-alive - -* notes for building something on top of this - -headers to consider auto-supporting at the high-level: -- Date: https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#header.date - MUST be sent by origin servers who know what time it is - (clients don't bother) -- Server -- automagic compression - -should let handlers control timeouts - -################################################################ - -Higher level stuff: -- Timeouts: waiting for 100-continue, killing idle keepalive connections, - killing idle connections in general - basically just need a timeout when we block on read, and if it times out - then we close. should be settable in the APIs that block on read - (e.g. iterating over body). -- Expect: - https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1.1 - This is tightly integrated with flow control, not a lot we can do, except - maybe provide a method to be called before blocking waiting for the - request body? -- Sending an error when things go wrong (esp. 400 Bad Request) - -Connection shutdown is tricky. Quoth RFC 7230: - -"If a server performs an immediate close of a TCP connection, there is a -significant risk that the client will not be able to read the last HTTP -response. If the server receives additional data from the client on a fully -closed connection, such as another request that was sent by the client -before receiving the server's response, the server's TCP stack will send a -reset packet to the client; unfortunately, the reset packet might erase the -client's unacknowledged input buffers before they can be read and -interpreted by the client's HTTP parser. - -"To avoid the TCP reset problem, servers typically close a connection in -stages. First, the server performs a half-close by closing only the write -side of the read/write connection. The server then continues to read from -the connection until it receives a corresponding close by the client, or -until the server is reasonably certain that its own TCP stack has received -the client's acknowledgement of the packet(s) containing the server's last -response. Finally, the server fully closes the connection." - -So this needs shutdown(2). This is what data_to_send's close means -- this -complicated close dance. diff --git a/pyproject.toml b/pyproject.toml index 64a6883..7287d48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,46 +1,111 @@ -[tool.towncrier] -# Usage: -# - PRs should drop a file like "issuenumber.feature" in newsfragments -# (or "bugfix", "doc", "removal", "misc"; misc gets no text, we can -# customize this) -# - At release time after bumping version number, run: towncrier -# (or towncrier --draft) -package = "h11" -filename = "docs/source/changes.rst" -directory = "newsfragments" -underlines = ["-", "~", "^"] -issue_format = "`#{issue} `__" - -# Unfortunately there's no way to simply override -# tool.towncrier.type.misc.showcontent - -[[tool.towncrier.type]] -directory = "feature" -name = "Features" -showcontent = true - -[[tool.towncrier.type]] -directory = "bugfix" -name = "Bugfixes" -showcontent = true - -[[tool.towncrier.type]] -directory = "doc" -name = "Improved Documentation" -showcontent = true - -[[tool.towncrier.type]] -directory = "removal" -name = "Deprecations and Removals" -showcontent = true - -[[tool.towncrier.type]] -directory = "misc" -name = "Miscellaneous internal changes" -showcontent = true +[build-system] +requires = ["setuptools>=77", "mypy==2.3.1"] +build-backend = "setuptools.build_meta" + +[project] +name = 'h11-mypyc' +description = "A bring-your-own-I/O implementation of HTTP/1.1, compiled with mypyc" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE.txt"] +authors = [ + { name = "Nathaniel J. Smith", email = "njs@pobox.com" }, + { name = "Dima Anfimov", email = "lovesolaristics@gmail.com" }, +] +classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP", + "Topic :: System :: Networking", +] +dynamic = ["version"] +requires-python = '>=3.10' +dependencies = [] + +[project.urls] +Repository = "https://github.com/danfimov/h11" +Homepage = "https://danfimov.github.io/h11/" +Documentation = "https://danfimov.github.io/h11/" +Changelog = "https://danfimov.github.io/h11/changes/" + +[dependency-groups] +dev = [ + "prek>=0.4.14", + {include-group = "build"}, + {include-group = "lint"}, + {include-group = "test"}, + {include-group = "docs"}, +] +build = [ + "mypy>=2.3.1", + "setuptools>=77", +] +lint = [ + "ruff>=0.16.4", + "zizmor>=1.29.0", +] +test = [ + "pytest>=9.1.1", + "pytest-cov>=7.1.0", + "pytest-memray>=1.10.0", + "pytest-codspeed>=5.0.3", +] +docs = [ + "mkdocstrings-python>=1.20.0", + "zensical>=0.0.57", +] + +[tool.setuptools] +packages = ["h11_mypyc"] + +[tool.setuptools.dynamic] +version = { attr = "h11_mypyc._version.__version__" } + +[tool.setuptools.package-data] +h11_mypyc = ["py.typed"] [tool.mypy] strict = true warn_unused_configs = true warn_unused_ignores = true show_error_codes = true + +[tool.ruff] +line-length = 120 +src = ["h11_mypyc", "tests"] + +[tool.ruff.lint] +select = [ + "I", # isort + "E", # pycodestyle (errors) + "F", # pyflakes + "UP", # pyupgrade +] + +[tool.cibuildwheel] +# PyPy is excluded on purpose: mypyc-compiled code is slower there than the +# pure-Python build, which PyPy will get from the py3-none-any wheel instead. +build = "cp310-* cp311-* cp312-* cp313-* cp314-*" +skip = "*_i686 *-win32 *-musllinux_i686" +build-frontend = "build[uv]" +environment = { H11_MYPYC = "1" } +# The compiled build is where annotations become runtime checks, so a wheel that +# imports fine can still fail on real traffic. Run the suite against each one. +# +# test-sources copies the tests into the test working directory rather than +# running them from the checkout. Pointing pytest at {project}/tests instead +# would make it treat the project as rootdir and put the h11_mypyc/ sources on +# sys.path ahead of site-packages, so the tests would silently exercise the +# interpreted source rather than the wheel just built. +test-sources = ["tests"] +test-command = "pytest tests -q" +test-requires = ["pytest"] diff --git a/pytest.toml b/pytest.toml new file mode 100644 index 0000000..c68e547 --- /dev/null +++ b/pytest.toml @@ -0,0 +1,11 @@ +[pytest] +pythonpath = ["h11_mypyc", "."] +testpaths = ["tests"] +addopts = [ + "--durations=10", + "--durations-min=1.0", + + "-p no:pastebin", + "-p no:doctest", + "-p no:legacypath", +] diff --git a/setup.py b/setup.py index 73713e2..0158693 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,24 @@ -from setuptools import setup, find_packages +import os -# defines __version__ -exec(open("h11/_version.py").read()) +from setuptools import setup -setup( - name="h11", - version=__version__, - description= - "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1", - long_description=open("README.rst").read(), - author="Nathaniel J. Smith", - author_email="njs@pobox.com", - license="MIT", - packages=find_packages(exclude=["h11.tests"]), - package_data={'h11': ['py.typed']}, - url="https://github.com/python-hyper/h11", - python_requires=">=3.8", - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Internet :: WWW/HTTP", - "Topic :: System :: Networking", - ], -) +MYPYC_MODULES = [ + "h11_mypyc/_abnf.py", + "h11_mypyc/_util.py", + "h11_mypyc/_receivebuffer.py", + "h11_mypyc/_headers.py", + "h11_mypyc/_events.py", + "h11_mypyc/_readers.py", + "h11_mypyc/_writers.py", + "h11_mypyc/_state.py", + "h11_mypyc/_connection.py", +] + +if os.environ.get("H11_MYPYC") == "1": + # Imported lazily: mypyc is not in build-system.requires, so it is absent + # from the isolated build env used for the pure-Python path. + from mypyc.build import mypycify + + setup(ext_modules=mypycify(MYPYC_MODULES)) +else: + setup() diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 9955dec..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -pytest-cov diff --git a/h11/tests/__init__.py b/tests/__init__.py similarity index 100% rename from h11/tests/__init__.py rename to tests/__init__.py diff --git a/h11/tests/data/test-file b/tests/data/test-file similarity index 100% rename from h11/tests/data/test-file rename to tests/data/test-file diff --git a/h11/tests/helpers.py b/tests/helpers.py similarity index 76% rename from h11/tests/helpers.py rename to tests/helpers.py index 571be44..3e0280d 100644 --- a/h11/tests/helpers.py +++ b/tests/helpers.py @@ -1,25 +1,17 @@ -from typing import cast, List, Type, Union, ValuesView +from collections.abc import ValuesView +from typing import Literal, cast -from .._connection import Connection, NEED_DATA, PAUSED -from .._events import ( +from h11_mypyc._connection import NEED_DATA, PAUSED, Connection +from h11_mypyc._events import ( ConnectionClosed, Data, - EndOfMessage, Event, - InformationalResponse, - Request, - Response, ) -from .._state import CLIENT, CLOSED, DONE, MUST_CLOSE, SERVER -from .._util import Sentinel +from h11_mypyc._state import CLIENT, SERVER +from h11_mypyc._util import Sentinel -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - -def get_all_events(conn: Connection) -> List[Event]: +def get_all_events(conn: Connection) -> list[Event]: got_events = [] while True: event = conn.next_event() @@ -32,15 +24,15 @@ def get_all_events(conn: Connection) -> List[Event]: return got_events -def receive_and_get(conn: Connection, data: bytes) -> List[Event]: +def receive_and_get(conn: Connection, data: bytes) -> list[Event]: conn.receive_data(data) return get_all_events(conn) # Merges adjacent Data events, converts payloads to bytestrings, and removes # chunk boundaries. -def normalize_data_events(in_events: List[Event]) -> List[Event]: - out_events: List[Event] = [] +def normalize_data_events(in_events: list[Event]) -> list[Event]: + out_events: list[Event] = [] for event in in_events: if type(event) is Data: event = Data(data=bytes(event.data), chunk_start=False, chunk_end=False) @@ -71,9 +63,9 @@ def conns(self) -> ValuesView[Connection]: # expect="match" if expect=send_events; expect=[...] to say what expected def send( self, - role: Type[Sentinel], - send_events: Union[List[Event], Event], - expect: Union[List[Event], Event, Literal["match"]] = "match", + role: type[Sentinel], + send_events: list[Event] | Event, + expect: list[Event] | Event | Literal["match"] = "match", ) -> bytes: if not isinstance(send_events, list): send_events = [send_events] diff --git a/h11/tests/test_against_stdlib_http.py b/tests/test_against_stdlib_http.py similarity index 85% rename from h11/tests/test_against_stdlib_http.py rename to tests/test_against_stdlib_http.py index 3f66a10..42f0b28 100644 --- a/h11/tests/test_against_stdlib_http.py +++ b/tests/test_against_stdlib_http.py @@ -3,12 +3,12 @@ import socket import socketserver import threading +from collections.abc import Callable, Generator from contextlib import closing, contextmanager from http.server import SimpleHTTPRequestHandler -from typing import Callable, Generator from urllib.request import urlopen -import h11 +import h11_mypyc as h11 @contextmanager @@ -16,9 +16,7 @@ def socket_server( handler: Callable[..., socketserver.BaseRequestHandler], ) -> Generator[socketserver.TCPServer, None, None]: httpd = socketserver.TCPServer(("127.0.0.1", 0), handler) - thread = threading.Thread( - target=httpd.serve_forever, kwargs={"poll_interval": 0.01} - ) + thread = threading.Thread(target=httpd.serve_forever, kwargs={"poll_interval": 0.01}) thread.daemon = True try: thread.start() @@ -42,13 +40,7 @@ def test_h11_as_client() -> None: with closing(socket.create_connection(httpd.server_address)) as s: # type: ignore[arg-type] c = h11.Connection(h11.CLIENT) - s.sendall( - c.send( - h11.Request( - method="GET", target="/foo", headers=[("Host", "localhost")] - ) - ) - ) + s.sendall(c.send(h11.Request(method="GET", target="/foo", headers=[("Host", "localhost")]))) s.sendall(c.send(h11.EndOfMessage())) data = bytearray() @@ -90,10 +82,7 @@ def handle(self) -> None: { "method": request.method.decode("ascii"), "target": request.target.decode("ascii"), - "headers": { - name.decode("ascii"): value.decode("ascii") - for (name, value) in request.headers - }, + "headers": {name.decode("ascii"): value.decode("ascii") for (name, value) in request.headers}, } ) s.sendall(c.send(h11.Response(status_code=200, headers=[]))) diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 0000000..6596bf8 --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,252 @@ +import pytest +from h11_mypyc import ( + CLIENT, + DONE, + NEED_DATA, + SERVER, + Connection, + Data, + EndOfMessage, + Request, + Response, +) +from h11_mypyc._headers import Headers, get_comma_header, normalize_and_validate +from h11_mypyc._readers import _decode_header_lines +from h11_mypyc._receivebuffer import ReceiveBuffer + +# A realistic browser request header set. Realistic data matters here: header +# validation cost depends on value length and on how many values contain +# separators, so synthetic b"a: b" pairs would flatter the benchmark. +BROWSER_HEADERS: list[tuple[bytes, bytes]] = [ + (b"Host", b"example.com"), + (b"User-Agent", b"Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"), + (b"Accept", b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), + (b"Accept-Language", b"en-US,en;q=0.5"), + (b"Accept-Encoding", b"gzip, deflate, br"), + (b"DNT", b"1"), + (b"Cookie", b"ID=" + b"A" * 200), + (b"Connection", b"keep-alive"), +] + +RESPONSE_HEADERS: list[tuple[bytes, bytes]] = [ + (b"Cache-Control", b"private, max-age=0"), + (b"Content-Encoding", b"gzip"), + (b"Content-Type", b"text/html; charset=UTF-8"), + (b"Date", b"Fri, 20 May 2016 09:23:41 GMT"), + (b"Expires", b"-1"), + (b"Server", b"gws"), + (b"X-Frame-Options", b"SAMEORIGIN"), + (b"X-XSS-Protection", b"1; mode=block"), +] + +TYPICAL_HEADER_COUNT = 8 +TYPICAL_BODY_SIZE = 1024 + +HEADER_COUNTS = [ + pytest.param(3, id="3headers"), + pytest.param(TYPICAL_HEADER_COUNT, id="8headers"), + pytest.param(24, id="24headers"), +] + +BODY_SIZES = [ + pytest.param(0, id="nobody"), + pytest.param(TYPICAL_BODY_SIZE, id="1KiB"), + pytest.param(64 * 1024, id="64KiB"), +] + + +def _sized_headers(count: int) -> list[tuple[bytes, bytes]]: + """A header list of exactly `count` pairs, Host first so Requests stay legal.""" + out = list(BROWSER_HEADERS[:count]) + while len(out) < count: + i = len(out) + out.append((b"X-Custom-%d" % i, b"value-%d; q=0.5, other" % i)) + return out + + +def _raw_request(count: int) -> bytes: + lines = [b"%s: %s" % pair for pair in _sized_headers(count)] + return b"GET /path/to/thing HTTP/1.1\r\n" + b"\r\n".join(lines) + b"\r\n\r\n" + + +def _response_headers(body_length: int) -> list[tuple[bytes, bytes]]: + return [*RESPONSE_HEADERS, (b"Content-Length", b"%d" % body_length)] + + +def _drain_request(conn: Connection) -> None: + while type(conn.next_event()) is not EndOfMessage: + pass + + +@pytest.fixture(scope="session", params=HEADER_COUNTS) +def header_pairs(request: pytest.FixtureRequest) -> list[tuple[bytes, bytes]]: + return _sized_headers(request.param) + + +@pytest.fixture(scope="session", params=HEADER_COUNTS) +def header_lines(request: pytest.FixtureRequest) -> list[bytes]: + """Raw `name: value` lines, as the reader sees them off the wire.""" + return [b"%s: %s" % pair for pair in _sized_headers(request.param)] + + +@pytest.fixture(scope="session", params=HEADER_COUNTS) +def raw_request(request: pytest.FixtureRequest) -> bytes: + return _raw_request(request.param) + + +@pytest.fixture(scope="session") +def typical_request() -> bytes: + return _raw_request(TYPICAL_HEADER_COUNT) + + +@pytest.fixture(scope="session", params=BODY_SIZES) +def body(request: pytest.FixtureRequest) -> bytes: + return b"x" * request.param + + +@pytest.fixture(scope="session") +def typical_body() -> bytes: + return b"x" * TYPICAL_BODY_SIZE + + +@pytest.fixture(scope="session") +def normalized_headers() -> Headers: + return normalize_and_validate(BROWSER_HEADERS) + + +@pytest.fixture(scope="session") +def raw_response(body: bytes) -> bytes: + lines = [b"%s: %s" % pair for pair in _response_headers(len(body))] + return b"HTTP/1.1 200 OK\r\n" + b"\r\n".join(lines) + b"\r\n\r\n" + body + + +class TestHotSpots: + """Individual functions the profiler singled out.""" + + @pytest.mark.benchmark + def test_decode_header_lines(self, header_lines: list[bytes]) -> None: + """Receive path: a regex per header line, plus obs-fold handling.""" + decoded = list(_decode_header_lines(header_lines)) + assert len(decoded) == len(header_lines) + + @pytest.mark.benchmark + def test_normalize_and_validate(self, header_pairs: list[tuple[bytes, bytes]]) -> None: + """Send path: bytesify, syntax validation and lowercasing, per header.""" + headers = normalize_and_validate(header_pairs) + assert len(headers) == len(header_pairs) + + @pytest.mark.benchmark + def test_normalize_and_validate_parsed(self, header_pairs: list[tuple[bytes, bytes]]) -> None: + """The _parsed=True path, which skips validation already done on the wire. + + The gap against test_normalize_and_validate is what validation costs. + """ + lowered = [(name.lower(), value) for name, value in header_pairs] + headers = normalize_and_validate(lowered, _parsed=True) + assert len(headers) == len(header_pairs) + + @pytest.mark.benchmark + def test_get_comma_header_hit(self, normalized_headers: Headers) -> None: + """A lookup that finds something: full scan, then split and strip.""" + assert get_comma_header(normalized_headers, b"connection") == [b"keep-alive"] + + @pytest.mark.benchmark + def test_get_comma_header_miss(self, normalized_headers: Headers) -> None: + """A lookup that finds nothing -- half of the 10 per round-trip look like this.""" + assert get_comma_header(normalized_headers, b"transfer-encoding") == [] + + @pytest.mark.benchmark + def test_receivebuffer_extract_lines(self, raw_request: bytes) -> None: + """Splitting the header block out of the receive buffer.""" + buf = ReceiveBuffer() + buf += raw_request + lines = buf.maybe_extract_lines() + assert lines is not None + + +class TestPipelineHalves: + """Reading and writing measured apart, to tell which side moved.""" + + @pytest.mark.benchmark + def test_parse_request(self, raw_request: bytes) -> None: + conn = Connection(SERVER) + conn.receive_data(raw_request) + assert type(conn.next_event()) is Request + + @pytest.mark.benchmark + def test_send_response(self, typical_request: bytes, typical_body: bytes) -> None: + conn = Connection(SERVER) + conn.receive_data(typical_request) + _drain_request(conn) + written = conn.send(Response(status_code=200, headers=_response_headers(len(typical_body)))) + assert written is not None and written.startswith(b"HTTP/1.1 200") + + +class TestPipeline: + """Whole request/response cycles.""" + + @pytest.mark.benchmark + def test_server_roundtrip(self, raw_request: bytes, typical_body: bytes) -> None: + """Header count is the axis: parse a request, then write a full response.""" + conn = Connection(SERVER) + conn.receive_data(raw_request) + _drain_request(conn) + conn.send(Response(status_code=200, headers=_response_headers(len(typical_body)))) + conn.send(Data(data=typical_body)) + conn.send(EndOfMessage()) + assert conn.our_state is DONE + + @pytest.mark.benchmark + def test_server_roundtrip_body(self, typical_request: bytes, body: bytes) -> None: + """Body size is the axis, so this measures framing and buffer copies.""" + conn = Connection(SERVER) + conn.receive_data(typical_request) + _drain_request(conn) + conn.send(Response(status_code=200, headers=_response_headers(len(body)))) + conn.send(Data(data=body)) + conn.send(EndOfMessage()) + assert conn.our_state is DONE + + @pytest.mark.benchmark + def test_server_roundtrip_chunked(self, typical_request: bytes, body: bytes) -> None: + """Chunked framing rather than Content-Length: a different writer path.""" + conn = Connection(SERVER) + conn.receive_data(typical_request) + _drain_request(conn) + conn.send(Response(status_code=200, headers=[*RESPONSE_HEADERS, (b"Transfer-Encoding", b"chunked")])) + conn.send(Data(data=body)) + conn.send(EndOfMessage()) + assert conn.our_state is DONE + + @pytest.mark.benchmark + def test_server_roundtrip_keepalive(self, typical_request: bytes, typical_body: bytes) -> None: + """Ten round-trips on one connection, as a keep-alive server does. + + Amortizes connection setup away and exercises start_next_cycle(). + """ + conn = Connection(SERVER) + headers = _response_headers(len(typical_body)) + for _ in range(10): + conn.receive_data(typical_request) + _drain_request(conn) + conn.send(Response(status_code=200, headers=headers)) + conn.send(Data(data=typical_body)) + conn.send(EndOfMessage()) + conn.start_next_cycle() + assert conn.our_state is not DONE + + @pytest.mark.benchmark + def test_client_roundtrip(self, raw_response: bytes, body: bytes) -> None: + """The client half: serialize a request, then parse a whole response.""" + conn = Connection(CLIENT) + conn.send(Request(method="GET", target="/path/to/thing", headers=BROWSER_HEADERS)) + conn.send(EndOfMessage()) + conn.receive_data(raw_response) + received = 0 + while True: + event = conn.next_event() + if event is NEED_DATA or type(event) is EndOfMessage: + break + if type(event) is Data: + received += len(event.data) + assert received == len(body) diff --git a/h11/tests/test_connection.py b/tests/test_connection.py similarity index 90% rename from h11/tests/test_connection.py rename to tests/test_connection.py index 01260dc..b5157c5 100644 --- a/h11/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,9 +1,8 @@ -from typing import Any, cast, Dict, List, Optional, Tuple, Type +from typing import Any, cast import pytest - -from .._connection import _body_framing, _keep_alive, Connection, NEED_DATA, PAUSED -from .._events import ( +from h11_mypyc._connection import NEED_DATA, PAUSED, Connection, _body_framing, _keep_alive +from h11_mypyc._events import ( ConnectionClosed, Data, EndOfMessage, @@ -11,7 +10,7 @@ Request, Response, ) -from .._state import ( +from h11_mypyc._state import ( CLIENT, CLOSED, DONE, @@ -23,14 +22,13 @@ SERVER, SWITCHED_PROTOCOL, ) -from .._util import LocalProtocolError, RemoteProtocolError, Sentinel +from h11_mypyc._util import LocalProtocolError, RemoteProtocolError, Sentinel + from .helpers import ConnectionPair, get_all_events, receive_and_get def test__keep_alive() -> None: - assert _keep_alive( - Request(method="GET", target="/", headers=[("Host", "Example.com")]) - ) + assert _keep_alive(Request(method="GET", target="/", headers=[("Host", "Example.com")])) assert not _keep_alive( Request( method="GET", @@ -45,20 +43,16 @@ def test__keep_alive() -> None: headers=[("Host", "Example.com"), ("Connection", "a, b, cLOse, foo")], ) ) - assert not _keep_alive( - Request(method="GET", target="/", headers=[], http_version="1.0") - ) + assert not _keep_alive(Request(method="GET", target="/", headers=[], http_version="1.0")) assert _keep_alive(Response(status_code=200, headers=[])) assert not _keep_alive(Response(status_code=200, headers=[("Connection", "close")])) - assert not _keep_alive( - Response(status_code=200, headers=[("Connection", "a, b, cLOse, foo")]) - ) + assert not _keep_alive(Response(status_code=200, headers=[("Connection", "a, b, cLOse, foo")])) assert not _keep_alive(Response(status_code=200, headers=[], http_version="1.0")) def test__body_framing() -> None: - def headers(cl: Optional[int], te: bool) -> List[Tuple[str, str]]: + def headers(cl: int | None, te: bool) -> list[tuple[str, str]]: headers = [] if cl is not None: headers.append(("Content-Length", str(cl))) @@ -66,19 +60,17 @@ def headers(cl: Optional[int], te: bool) -> List[Tuple[str, str]]: headers.append(("Transfer-Encoding", "chunked")) return headers - def resp( - status_code: int = 200, cl: Optional[int] = None, te: bool = False - ) -> Response: + def resp(status_code: int = 200, cl: int | None = None, te: bool = False) -> Response: return Response(status_code=status_code, headers=headers(cl, te)) - def req(cl: Optional[int] = None, te: bool = False) -> Request: + def req(cl: int | None = None, te: bool = False) -> Request: h = headers(cl, te) h += [("Host", "example.com")] return Request(method="GET", target="/", headers=h) # Special cases where the headers are ignored: for kwargs in [{}, {"cl": 100}, {"te": True}, {"cl": 100, "te": True}]: - kwargs = cast(Dict[str, Any], kwargs) + kwargs = cast(dict[str, Any], kwargs) for meth, r in [ (b"HEAD", resp(**kwargs)), (b"GET", resp(status_code=204, **kwargs)), @@ -88,7 +80,7 @@ def req(cl: Optional[int] = None, te: bool = False) -> Request: # Transfer-encoding for kwargs in [{"te": True}, {"cl": 100, "te": True}]: - kwargs = cast(Dict[str, Any], kwargs) + kwargs = cast(dict[str, Any], kwargs) for meth, r in [(None, req(**kwargs)), (b"GET", resp(**kwargs))]: # type: ignore assert _body_framing(meth, r) == ("chunked", ()) @@ -97,7 +89,7 @@ def req(cl: Optional[int] = None, te: bool = False) -> Request: assert _body_framing(meth, r) == ("content-length", (100,)) # No headers - assert _body_framing(None, req()) == ("content-length", (0,)) # type: ignore + assert _body_framing(None, req()) == ("content-length", (0,)) assert _body_framing(b"GET", resp()) == ("http/1.0", ()) @@ -119,9 +111,7 @@ def test_Connection_basics_and_content_length() -> None: headers=[("Host", "example.com"), ("Content-Length", "10")], ), ) - assert data == ( - b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 10\r\n\r\n" - ) + assert data == (b"GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\n\r\n") for conn in p.conns: assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} @@ -147,9 +137,7 @@ def test_Connection_basics_and_content_length() -> None: data = p.send(CLIENT, Data(data=b"12345")) assert data == b"12345" - data = p.send( - CLIENT, Data(data=b"67890"), expect=[Data(data=b"67890"), EndOfMessage()] - ) + data = p.send(CLIENT, Data(data=b"67890"), expect=[Data(data=b"67890"), EndOfMessage()]) assert data == b"67890" data = p.send(CLIENT, EndOfMessage(), expect=[]) assert data == b"" @@ -188,9 +176,7 @@ def test_chunked() -> None: data = p.send(CLIENT, EndOfMessage(headers=[("hello", "there")])) assert data == b"0\r\nhello: there\r\n\r\n" - p.send( - SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]) - ) + p.send(SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")])) p.send(SERVER, Data(data=b"54321", chunk_start=True, chunk_end=True)) p.send(SERVER, Data(data=b"12345", chunk_start=True, chunk_end=True)) p.send(SERVER, EndOfMessage()) @@ -202,12 +188,7 @@ def test_chunked() -> None: def test_chunk_boundaries() -> None: conn = Connection(our_role=SERVER) - request = ( - b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Transfer-Encoding: chunked\r\n" - b"\r\n" - ) + request = b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n" conn.receive_data(request) assert conn.next_event() == Request( method="POST", @@ -265,10 +246,7 @@ def test_server_talking_to_http10_client() -> None: assert c.their_state is MUST_CLOSE # We automatically Connection: close back at them - assert ( - c.send(Response(status_code=200, headers=[])) - == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" - ) + assert c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" assert c.send(Data(data=b"12345")) == b"12345" assert c.send(EndOfMessage()) == b"" @@ -303,7 +281,7 @@ def test_automatic_transfer_encoding_in_response() -> None: # because if both are set then Transfer-Encoding wins [("Transfer-Encoding", "chunked"), ("Content-Length", "100")], ]: - user_headers = cast(List[Tuple[str, str]], user_headers) + user_headers = cast(list[tuple[str, str]], user_headers) p = ConnectionPair() p.send( CLIENT, @@ -317,19 +295,14 @@ def test_automatic_transfer_encoding_in_response() -> None: p.send( SERVER, Response(status_code=200, headers=user_headers), - expect=Response( - status_code=200, headers=[("Transfer-Encoding", "chunked")] - ), + expect=Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]), ) # When speaking to HTTP/1.0 client, all of the above cases get # normalized to no-framing-headers c = Connection(SERVER) receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") - assert ( - c.send(Response(status_code=200, headers=user_headers)) - == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" - ) + assert c.send(Response(status_code=200, headers=user_headers)) == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" assert c.send(Data(data=b"12345")) == b"12345" @@ -395,9 +368,7 @@ def setup() -> ConnectionPair: # Disabled by a real response p = setup() - p.send( - SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]) - ) + p.send(SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")])) for conn in p.conns: assert not conn.client_is_waiting_for_100_continue assert not conn.they_are_waiting_for_100_continue @@ -427,9 +398,7 @@ def test_max_incomplete_event_size_countermeasure() -> None: c.receive_data(b"a" * 4000) c.receive_data(b"\r\n\r\n") assert get_all_events(c) == [ - Request( - method="GET", target="/", http_version="1.0", headers=[("big", "a" * 4000)] - ), + Request(method="GET", target="/", http_version="1.0", headers=[("big", "a" * 4000)]), EndOfMessage(), ] @@ -458,10 +427,7 @@ def test_max_incomplete_event_size_countermeasure() -> None: c = Connection(SERVER, max_incomplete_event_size=100) # Two pipelined requests to create a way-too-big receive buffer... but # it's fine because we're not checking - c.receive_data( - b"GET /1 HTTP/1.1\r\nHost: a\r\n\r\n" - b"GET /2 HTTP/1.1\r\nHost: b\r\n\r\n" + b"X" * 1000 - ) + c.receive_data(b"GET /1 HTTP/1.1\r\nHost: a\r\n\r\nGET /2 HTTP/1.1\r\nHost: b\r\n\r\n" + b"X" * 1000) assert get_all_events(c) == [ Request(method="GET", target="/1", headers=[("host", "a")]), EndOfMessage(), @@ -707,9 +673,7 @@ def setup() -> ConnectionPair: # protocol switch p = setup() with pytest.raises(LocalProtocolError): - p.conn[CLIENT].send( - Request(method="GET", target="/", headers=[("Host", "a")]) - ) + p.conn[CLIENT].send(Request(method="GET", target="/", headers=[("Host", "a")])) p = setup() p.send(SERVER, accept) with pytest.raises(LocalProtocolError): @@ -810,9 +774,7 @@ def test_close_different_states() -> None: p = ConnectionPair() p.send( CLIENT, - Request( - method="GET", target="/", headers=[("Host", "a"), ("Content-Length", "10")] - ), + Request(method="GET", target="/", headers=[("Host", "a"), ("Content-Length", "10")]), ) with pytest.raises(LocalProtocolError): p.conn[CLIENT].send(ConnectionClosed()) @@ -872,18 +834,14 @@ def __len__(self) -> int: placeholder = SendfilePlaceholder() - def setup( - header: Tuple[str, str], http_version: str - ) -> Tuple[Connection, Optional[List[bytes]]]: + def setup(header: tuple[str, str], http_version: str) -> tuple[Connection, list[bytes] | None]: c = Connection(SERVER) - receive_and_get( - c, f"GET / HTTP/{http_version}\r\nHost: a\r\n\r\n".encode("ascii") - ) + receive_and_get(c, f"GET / HTTP/{http_version}\r\nHost: a\r\n\r\n".encode("ascii")) headers = [] if header: headers.append(header) c.send(Response(status_code=200, headers=headers)) - return c, c.send_with_data_passthrough(Data(data=placeholder)) # type: ignore + return c, c.send_with_data_passthrough(Data(data=placeholder)) c, data = setup(("Content-Length", "10"), "1.1") assert data == [placeholder] # type: ignore @@ -916,15 +874,12 @@ def test_errors() -> None: c.next_event() # But we can still yell at the client for sending us gibberish if role is SERVER: - assert ( - c.send(Response(status_code=400, headers=[])) - == b"HTTP/1.1 400 \r\nConnection: close\r\n\r\n" - ) + assert c.send(Response(status_code=400, headers=[])) == b"HTTP/1.1 400 \r\nConnection: close\r\n\r\n" # After an error sending, you can no longer send # (This is especially important for things like content-length errors, # where there's complex internal state being modified) - def conn(role: Type[Sentinel]) -> Connection: + def conn(role: type[Sentinel]) -> Connection: c = Connection(our_role=role) if role is SERVER: # Put it into the state where it *could* send a response... @@ -1050,9 +1005,7 @@ def test_early_detection_of_invalid_response(data: bytes) -> None: def test_HEAD_framing_headers() -> None: def setup(method: bytes, http_version: bytes) -> Connection: c = Connection(SERVER) - c.receive_data( - method + b" / HTTP/" + http_version + b"\r\n" + b"Host: example.com\r\n\r\n" - ) + c.receive_data(method + b" / HTTP/" + http_version + b"\r\n" + b"Host: example.com\r\n\r\n") assert type(c.next_event()) is Request assert type(c.next_event()) is EndOfMessage return c @@ -1060,17 +1013,11 @@ def setup(method: bytes, http_version: bytes) -> Connection: for method in [b"GET", b"HEAD"]: # No Content-Length, HTTP/1.1 peer, should use chunked c = setup(method, b"1.1") - assert ( - c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - ) + assert c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\nTransfer-Encoding: chunked\r\n\r\n" # No Content-Length, HTTP/1.0 peer, frame with connection: close c = setup(method, b"1.0") - assert ( - c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" - b"Connection: close\r\n\r\n" - ) + assert c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" # Content-Length + Transfer-Encoding, TE wins c = setup(method, b"1.1") @@ -1091,9 +1038,7 @@ def setup(method: bytes, http_version: bytes) -> Connection: def test_special_exceptions_for_lost_connection_in_message_body() -> None: c = Connection(SERVER) - c.receive_data( - b"POST / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 100\r\n\r\n" - ) + c.receive_data(b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 100\r\n\r\n") assert type(c.next_event()) is Request assert c.next_event() is NEED_DATA c.receive_data(b"12345") @@ -1105,11 +1050,7 @@ def test_special_exceptions_for_lost_connection_in_message_body() -> None: assert "expected 100" in str(excinfo.value) c = Connection(SERVER) - c.receive_data( - b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - ) + c.receive_data(b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n") assert type(c.next_event()) is Request assert c.next_event() is NEED_DATA c.receive_data(b"8\r\n012345") diff --git a/h11/tests/test_events.py b/tests/test_events.py similarity index 93% rename from h11/tests/test_events.py rename to tests/test_events.py index d691545..860cf92 100644 --- a/h11/tests/test_events.py +++ b/tests/test_events.py @@ -1,8 +1,7 @@ from http import HTTPStatus import pytest - -from .._events import ( +from h11_mypyc._events import ( ConnectionClosed, Data, EndOfMessage, @@ -10,15 +9,13 @@ Request, Response, ) -from .._util import LocalProtocolError +from h11_mypyc._util import LocalProtocolError def test_events() -> None: with pytest.raises(LocalProtocolError): # Missing Host: - req = Request( - method="GET", target="/", headers=[("a", "b")], http_version="1.1" - ) + req = Request(method="GET", target="/", headers=[("a", "b")], http_version="1.1") # But this is okay (HTTP/1.0) req = Request(method="GET", target="/", headers=[("a", "b")], http_version="1.0") # fields are normalized @@ -78,9 +75,7 @@ def test_events() -> None: target = bytearray(b"/") target.append(bad_byte) with pytest.raises(LocalProtocolError): - Request( - method="GET", target=target, headers=[("Host", "a")], http_version="1.1" - ) + Request(method="GET", target=target, headers=[("Host", "a")], http_version="1.1") # Request method is validated with pytest.raises(LocalProtocolError): diff --git a/h11/tests/test_headers.py b/tests/test_headers.py similarity index 68% rename from h11/tests/test_headers.py rename to tests/test_headers.py index b57274c..b78c49b 100644 --- a/h11/tests/test_headers.py +++ b/tests/test_headers.py @@ -1,14 +1,15 @@ -import pytest +import re -from .._events import Request -from .._headers import ( +import pytest +from h11_mypyc._abnf import field_name, field_value +from h11_mypyc._events import Request +from h11_mypyc._headers import ( get_comma_header, has_expect_100_continue, - Headers, normalize_and_validate, set_comma_header, ) -from .._util import LocalProtocolError +from h11_mypyc._util import LocalProtocolError def test_normalize_and_validate() -> None: @@ -53,44 +54,30 @@ def test_normalize_and_validate() -> None: normalize_and_validate([("foo", "\tbarbaz")]) # content-length - assert normalize_and_validate([("Content-Length", "1")]) == [ - (b"content-length", b"1") - ] + assert normalize_and_validate([("Content-Length", "1")]) == [(b"content-length", b"1")] with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "asdf")]) with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "1x")]) with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "1"), ("Content-Length", "2")]) - assert normalize_and_validate( - [("Content-Length", "0"), ("Content-Length", "0")] - ) == [(b"content-length", b"0")] - assert normalize_and_validate([("Content-Length", "0 , 0")]) == [ - (b"content-length", b"0") - ] + assert normalize_and_validate([("Content-Length", "0"), ("Content-Length", "0")]) == [(b"content-length", b"0")] + assert normalize_and_validate([("Content-Length", "0 , 0")]) == [(b"content-length", b"0")] with pytest.raises(LocalProtocolError): - normalize_and_validate( - [("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")] - ) + normalize_and_validate([("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")]) with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "1 , 1,2")]) with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "1" * 21)]) # 1 billion TB # transfer-encoding - assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [ - (b"transfer-encoding", b"chunked") - ] - assert normalize_and_validate([("Transfer-Encoding", "cHuNkEd")]) == [ - (b"transfer-encoding", b"chunked") - ] + assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [(b"transfer-encoding", b"chunked")] + assert normalize_and_validate([("Transfer-Encoding", "cHuNkEd")]) == [(b"transfer-encoding", b"chunked")] with pytest.raises(LocalProtocolError) as excinfo: normalize_and_validate([("Transfer-Encoding", "gzip")]) assert excinfo.value.error_status_hint == 501 # Not Implemented with pytest.raises(LocalProtocolError) as excinfo: - normalize_and_validate( - [("Transfer-Encoding", "chunked"), ("Transfer-Encoding", "gzip")] - ) + normalize_and_validate([("Transfer-Encoding", "chunked"), ("Transfer-Encoding", "gzip")]) assert excinfo.value.error_status_hint == 501 # Not Implemented @@ -105,10 +92,10 @@ def test_get_set_comma_header() -> None: assert get_comma_header(headers, b"connection") == [b"close", b"foo", b"bar"] - headers = set_comma_header(headers, b"newthing", ["a", "b"]) # type: ignore + headers = set_comma_header(headers, b"newthing", [b"a", b"b"]) with pytest.raises(LocalProtocolError): - set_comma_header(headers, b"newthing", [" a", "b"]) # type: ignore + set_comma_header(headers, b"newthing", [b" a", b"b"]) assert headers == [ (b"connection", b"close"), @@ -118,7 +105,7 @@ def test_get_set_comma_header() -> None: (b"newthing", b"b"), ] - headers = set_comma_header(headers, b"whatever", ["different thing"]) # type: ignore + headers = set_comma_header(headers, b"whatever", [b"different thing"]) assert headers == [ (b"connection", b"close"), @@ -137,9 +124,7 @@ def test_has_100_continue() -> None: headers=[("Host", "example.com"), ("Expect", "100-continue")], ) ) - assert not has_expect_100_continue( - Request(method="GET", target="/", headers=[("Host", "example.com")]) - ) + assert not has_expect_100_continue(Request(method="GET", target="/", headers=[("Host", "example.com")])) # Case insensitive assert has_expect_100_continue( Request( @@ -157,3 +142,27 @@ def test_has_100_continue() -> None: http_version="1.0", ) ) + + +def test_field_validation_matches_abnf() -> None: + # normalize_and_validate checks header names and values with byte-class + # tests rather than the ABNF regexes, for speed. This pins the two + # together: if _abnf.field_name / field_value ever change, or the fast + # path drifts, the sets of accepted byte strings must still be identical. + name_re = re.compile(field_name.encode("ascii")) + value_re = re.compile(field_value.encode("ascii")) + + def accepted(name: bytes, value: bytes) -> bool: + try: + normalize_and_validate([(name, value)]) + except LocalProtocolError: + return False + return True + + candidates = [bytes([i]) for i in range(256)] + candidates += [bytes([i, j]) for i in range(256) for j in range(0, 256, 7)] + candidates += [b"", b"abc", b"a b", b"a b", b"a\tb", b" ab", b"ab ", b"\tab", b"ab\t"] + + for candidate in candidates: + assert accepted(candidate, b"ok") == bool(name_re.fullmatch(candidate)), f"header name {candidate!r}" + assert accepted(b"x-test", candidate) == bool(value_re.fullmatch(candidate)), f"header value {candidate!r}" diff --git a/h11/tests/test_helpers.py b/tests/test_helpers.py similarity index 90% rename from h11/tests/test_helpers.py rename to tests/test_helpers.py index 9a30dc6..a5aa50f 100644 --- a/h11/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,5 @@ -from .._events import Data, EndOfMessage, Response +from h11_mypyc._events import Data, EndOfMessage, Response + from .helpers import normalize_data_events diff --git a/h11/tests/test_io.py b/tests/test_io.py similarity index 86% rename from h11/tests/test_io.py rename to tests/test_io.py index 407e044..4e47fa3 100644 --- a/h11/tests/test_io.py +++ b/tests/test_io.py @@ -1,8 +1,8 @@ -from typing import Any, Callable, Generator, List +from collections.abc import Callable, Generator +from typing import Any import pytest - -from .._events import ( +from h11_mypyc._events import ( Data, EndOfMessage, Event, @@ -10,26 +10,27 @@ Request, Response, ) -from .._headers import Headers, normalize_and_validate -from .._readers import ( - _obsolete_line_fold, +from h11_mypyc._headers import Headers, normalize_and_validate +from h11_mypyc._readers import ( + READERS, ChunkedReader, ContentLengthReader, Http10Reader, - READERS, + _obsolete_line_fold, ) -from .._receivebuffer import ReceiveBuffer -from .._state import CLIENT, IDLE, SEND_RESPONSE, SERVER -from .._util import LocalProtocolError -from .._writers import ( +from h11_mypyc._receivebuffer import ReceiveBuffer +from h11_mypyc._state import CLIENT, IDLE, SEND_RESPONSE, SERVER +from h11_mypyc._util import LocalProtocolError +from h11_mypyc._writers import ( + WRITERS, ChunkedWriter, ContentLengthWriter, Http10Writer, write_any_response, write_headers, write_request, - WRITERS, ) + from .helpers import normalize_data_events SIMPLE_CASES = [ @@ -54,9 +55,7 @@ ), ( (SERVER, SEND_RESPONSE), - InformationalResponse( - status_code=101, headers=[("Upgrade", "websocket")], reason=b"Upgrade" - ), + InformationalResponse(status_code=101, headers=[("Upgrade", "websocket")], reason=b"Upgrade"), b"HTTP/1.1 101 Upgrade\r\nUpgrade: websocket\r\n\r\n", ), ( @@ -68,7 +67,7 @@ def dowrite(writer: Callable[..., None], obj: Any) -> bytes: - got_list: List[bytes] = [] + got_list: list[bytes] = [] writer(obj, got_list.append) return b"".join(got_list) @@ -146,9 +145,7 @@ def test_writers_unusual() -> None: with pytest.raises(LocalProtocolError): tw( write_any_response, - Response( - status_code=200, headers=[("Connection", "close")], http_version="1.0" - ), + Response(status_code=200, headers=[("Connection", "close")], http_version="1.0"), None, ) @@ -188,7 +185,7 @@ def test_readers_unusual() -> None: # 7230 -- this is a bug in the standard that we originally copied...) tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200 OK\r\n" b"Foo: a a a a a \r\n\r\n", + b"HTTP/1.0 200 OK\r\nFoo: a a a a a \r\n\r\n", Response( status_code=200, headers=[("Foo", "a a a a a")], @@ -200,27 +197,21 @@ def test_readers_unusual() -> None: # Empty headers -- also legal tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200 OK\r\n" b"Foo:\r\n\r\n", - Response( - status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK" - ), + b"HTTP/1.0 200 OK\r\nFoo:\r\n\r\n", + Response(status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"), ) tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200 OK\r\n" b"Foo: \t \t \r\n\r\n", - Response( - status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK" - ), + b"HTTP/1.0 200 OK\r\nFoo: \t \t \r\n\r\n", + Response(status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"), ) # Tolerate broken servers that leave off the response code tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200\r\n" b"Foo: bar\r\n\r\n", - Response( - status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b"" - ), + b"HTTP/1.0 200\r\nFoo: bar\r\n\r\n", + Response(status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b""), ) # Tolerate headers line endings (\r\n and \n) @@ -287,30 +278,30 @@ def test_readers_unusual() -> None: with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b" folded: line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\n folded: line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"foo : line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nfoo : line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nfoo\t: line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nfoo\t: line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): - tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n" b": line\r\n\r\n", None) + tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n: line\r\n\r\n", None) def test__obsolete_line_fold_bytes() -> None: @@ -326,9 +317,7 @@ def test__obsolete_line_fold_bytes() -> None: ] -def _run_reader_iter( - reader: Any, buf: bytes, do_eof: bool -) -> Generator[Any, None, None]: +def _run_reader_iter(reader: Any, buf: bytes, do_eof: bool) -> Generator[Any, None, None]: while True: event = reader(buf) if event is None: @@ -343,7 +332,7 @@ def _run_reader_iter( yield reader.read_eof() -def _run_reader(*args: Any) -> List[Event]: +def _run_reader(*args: Any) -> list[Event]: events = list(_run_reader_iter(*args)) return normalize_data_events(events) @@ -396,9 +385,7 @@ def test_ContentLengthReader() -> None: def test_Http10Reader() -> None: t_body_reader(Http10Reader, b"", [EndOfMessage()], do_eof=True) t_body_reader(Http10Reader, b"asdf", [Data(data=b"asdf")], do_eof=False) - t_body_reader( - Http10Reader, b"asdf", [Data(data=b"asdf"), EndOfMessage()], do_eof=True - ) + t_body_reader(Http10Reader, b"asdf", [Data(data=b"asdf"), EndOfMessage()], do_eof=True) def test_ChunkedReader() -> None: @@ -412,10 +399,7 @@ def test_ChunkedReader() -> None: t_body_reader( ChunkedReader, - b"5\r\n01234\r\n" - + b"10\r\n0123456789abcdef\r\n" - + b"0\r\n" - + b"Some: header\r\n\r\n", + b"5\r\n01234\r\n" + b"10\r\n0123456789abcdef\r\n" + b"0\r\n" + b"Some: header\r\n\r\n", [ Data(data=b"012340123456789abcdef"), EndOfMessage(headers=[("Some", "header")]), @@ -446,10 +430,7 @@ def test_ChunkedReader() -> None: # handles (and discards) "chunk extensions" omg wtf t_body_reader( ChunkedReader, - b"5; hello=there\r\n" - + b"xxxxx" - + b"\r\n" - + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n', + b"5; hello=there\r\n" + b"xxxxx" + b"\r\n" + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n', [Data(data=b"xxxxx"), EndOfMessage()], ) @@ -509,10 +490,7 @@ def test_ChunkedWriter() -> None: assert dowrite(w, EndOfMessage()) == b"0\r\n\r\n" - assert ( - dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")])) - == b"0\r\nEtag: asdf\r\na: b\r\n\r\n" - ) + assert dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")])) == b"0\r\nEtag: asdf\r\na: b\r\n\r\n" def test_Http10Writer() -> None: @@ -533,7 +511,7 @@ def test_reject_garbage_after_response_line() -> None: with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1 xxxxxx\r\n" b"Host: a\r\n\r\n", + b"HEAD /foo HTTP/1.1 xxxxxx\r\nHost: a\r\n\r\n", None, ) @@ -542,7 +520,7 @@ def test_reject_garbage_in_header_line() -> None: with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"Host: foo\x00bar\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nHost: foo\x00bar\r\n\r\n", None, ) @@ -560,10 +538,7 @@ def test_reject_non_vchar_in_path() -> None: def test_allow_some_garbage_in_cookies() -> None: tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" - b"Host: foo\r\n" - b"Set-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n" - b"\r\n", + b"HEAD /foo HTTP/1.1\r\nHost: foo\r\nSet-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n\r\n", Request( method="HEAD", target="/foo", diff --git a/h11/tests/test_receivebuffer.py b/tests/test_receivebuffer.py similarity index 95% rename from h11/tests/test_receivebuffer.py rename to tests/test_receivebuffer.py index 21a3870..34093c7 100644 --- a/h11/tests/test_receivebuffer.py +++ b/tests/test_receivebuffer.py @@ -1,9 +1,5 @@ -import re -from typing import Tuple - import pytest - -from .._receivebuffer import ReceiveBuffer +from h11_mypyc._receivebuffer import ReceiveBuffer def test_receivebuffer() -> None: @@ -119,7 +115,7 @@ def test_receivebuffer() -> None: ), ], ) -def test_receivebuffer_for_invalid_delimiter(data: Tuple[bytes]) -> None: +def test_receivebuffer_for_invalid_delimiter(data: tuple[bytes]) -> None: b = ReceiveBuffer() for line in data: diff --git a/h11/tests/test_state.py b/tests/test_state.py similarity index 97% rename from h11/tests/test_state.py rename to tests/test_state.py index bc974e6..4ab539e 100644 --- a/h11/tests/test_state.py +++ b/tests/test_state.py @@ -1,20 +1,17 @@ import pytest - -from .._events import ( +from h11_mypyc._events import ( ConnectionClosed, Data, EndOfMessage, - Event, InformationalResponse, Request, Response, ) -from .._state import ( +from h11_mypyc._state import ( _SWITCH_CONNECT, _SWITCH_UPGRADE, CLIENT, CLOSED, - ConnectionState, DONE, IDLE, MIGHT_SWITCH_PROTOCOL, @@ -23,8 +20,9 @@ SEND_RESPONSE, SERVER, SWITCHED_PROTOCOL, + ConnectionState, ) -from .._util import LocalProtocolError +from h11_mypyc._util import LocalProtocolError def test_ConnectionState() -> None: @@ -156,9 +154,7 @@ def test_ConnectionState_double_protocol_switch() -> None: cs.process_event(CLIENT, Request) cs.process_event(CLIENT, EndOfMessage) assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE} - cs.process_event( - SERVER, _response_type_for_switch[server_switch], server_switch - ) + cs.process_event(SERVER, _response_type_for_switch[server_switch], server_switch) if server_switch is None: assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY} else: diff --git a/h11/tests/test_util.py b/tests/test_util.py similarity index 88% rename from h11/tests/test_util.py rename to tests/test_util.py index 79bc095..09fcb0d 100644 --- a/h11/tests/test_util.py +++ b/tests/test_util.py @@ -4,14 +4,14 @@ from typing import NoReturn import pytest - -from .._util import ( - bytesify, +from h11_mypyc._util import ( LocalProtocolError, ProtocolError, RemoteProtocolError, Sentinel, + bytesify, validate, + validate_and_group, ) @@ -55,7 +55,7 @@ def test_validate() -> None: with pytest.raises(LocalProtocolError): validate(my_re, b"0.") - groups = validate(my_re, b"0.1") + groups = validate_and_group(my_re, b"0.1") assert groups == {"group1": b"0", "group2": b"1"} # successful partial matches are an error - must match whole string @@ -82,22 +82,19 @@ def test_validate_formatting() -> None: def test_make_sentinel() -> None: - class S(Sentinel, metaclass=Sentinel): + class S(Sentinel): pass - assert repr(S) == "S" - assert S == S - assert type(S).__name__ == "S" + assert S.__name__ == "S" + assert S is S assert S in {S} - assert type(S) is S - class S2(Sentinel, metaclass=Sentinel): + class S2(Sentinel): pass - assert repr(S2) == "S2" - assert S != S2 + assert S2.__name__ == "S2" + assert S is not S2 assert S not in {S2} - assert type(S) is not type(S2) def test_bytesify() -> None: diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6614ecf..0000000 --- a/tox.ini +++ /dev/null @@ -1,32 +0,0 @@ -[tox] -envlist = format, py{38, 39, 310, 311, 312, py3}, mypy - -[gh-actions] -python = - 3.8: py38, format, mypy - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 - 3.13: py313 - pypy-3.9: pypy3 - pypy-3.10: pypy3 - -[testenv] -deps = -r{toxinidir}/test-requirements.txt -commands = pytest --cov=h11 --cov-config=.coveragerc h11 - -[testenv:format] -basepython = python3.8 -deps = -r{toxinidir}/format-requirements.txt -commands = - black --check --diff h11/ bench/ examples/ fuzz/ - isort --check --diff --profile black --dt h11 bench examples fuzz - -[testenv:mypy] -basepython = python3.8 -deps = - mypy==1.8.0 - pytest -commands = - mypy h11/ diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ef34e72 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1349 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, + { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, + { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, + { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, + { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, + { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, + { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, + { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, + { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, + { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, + { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, + { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, + { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[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 = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" }, + { url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" }, + { url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" }, + { url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, + { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, + { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "deepmerge" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/6c/9f4577a36d5f463a3a3f8322bd65d33e1a1a6b6ba1d692a5ebc3cba19015/deepmerge-3.0.tar.gz", hash = "sha256:14ed69f063de64b7743985c732ccff5d6c34ff4560946e7fbfd99086b853b9ce", size = 22279, upload-time = "2026-08-17T05:50:53.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/d7/7f19bedd30b90b72865aeec3a29127bed6dee6c9ef0324bb5b4d424bb0e3/deepmerge-3.0-py3-none-any.whl", hash = "sha256:c8541c3e186dc88d19a5513ad3a0b2d0b22beaa780969fc0c13b995a64265365", size = 14855, upload-time = "2026-08-17T05:50:52.218Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, +] + +[[package]] +name = "h11-mypyc" +source = { editable = "." } + +[package.dev-dependencies] +build = [ + { name = "mypy" }, + { name = "setuptools" }, +] +dev = [ + { name = "mkdocstrings-python" }, + { name = "mypy" }, + { name = "prek" }, + { name = "pytest" }, + { name = "pytest-codspeed" }, + { name = "pytest-cov" }, + { name = "pytest-memray" }, + { name = "ruff" }, + { name = "setuptools" }, + { name = "zensical" }, + { name = "zizmor" }, +] +docs = [ + { name = "mkdocstrings-python" }, + { name = "zensical" }, +] +lint = [ + { name = "ruff" }, + { name = "zizmor" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-codspeed" }, + { name = "pytest-cov" }, + { name = "pytest-memray" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +build = [ + { name = "mypy", specifier = ">=2.3.1" }, + { name = "setuptools", specifier = ">=77" }, +] +dev = [ + { name = "mkdocstrings-python", specifier = ">=1.20.0" }, + { name = "mypy", specifier = ">=2.3.1" }, + { name = "prek", specifier = ">=0.4.14" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-codspeed", specifier = ">=5.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-memray", specifier = ">=1.10.0" }, + { name = "ruff", specifier = ">=0.16.4" }, + { name = "setuptools", specifier = ">=77" }, + { name = "zensical", specifier = ">=0.0.57" }, + { name = "zizmor", specifier = ">=1.29.0" }, +] +docs = [ + { name = "mkdocstrings-python", specifier = ">=1.20.0" }, + { name = "zensical", specifier = ">=0.0.57" }, +] +lint = [ + { name = "ruff", specifier = ">=0.16.4" }, + { name = "zizmor", specifier = ">=1.29.0" }, +] +test = [ + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-codspeed", specifier = ">=5.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-memray", specifier = ">=1.10.0" }, +] + +[[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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/12/e2e9ca532cf5a0e08c9489826c4a35c6958c92ba0313fda70e8c6c3912be/librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489", size = 148673, upload-time = "2026-08-07T10:46:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7c/02005e23478bd5950618d9712e0fd2b4c511657857f3efd8ba6a5feabcdd/librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52", size = 153547, upload-time = "2026-08-07T10:46:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/d8848a735f5642077fc4b3b4bebcdb08edf10178e3add45597f5201a368f/librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8", size = 494355, upload-time = "2026-08-07T10:46:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0b/8604f41ea02feace490e9e405a338a15f9905369f55b239a9ce31c946f24/librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1", size = 485459, upload-time = "2026-08-07T10:46:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ac/84153bda1ce0da609182527ab92b40d961809e544eefdc5a1c2422971416/librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d", size = 498398, upload-time = "2026-08-07T10:46:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3a/5ca6cd282b2c244bec8ec84102e09773264e9c02891d56ab3a8f0e4d7083/librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702", size = 515474, upload-time = "2026-08-07T10:46:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/73/d3/bd34110234779eb843c6ed66aba7c9b2091d3dd85989f1fb9922f564cb7a/librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9", size = 509484, upload-time = "2026-08-07T10:46:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/43c3f7f071d71631a7daa3b835ef2168ea39f20692d81464d4e47fbaa6d6/librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b", size = 532534, upload-time = "2026-08-07T10:46:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1c/b854adf036ea817c40408873a5b794d65a91d9f0f39826f2ad2a2d5d7f48/librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db", size = 537087, upload-time = "2026-08-07T10:46:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/c9a890e244e7dd725d3bd8b560e41f0aec787eaf343b46956a290ab7b841/librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451", size = 536575, upload-time = "2026-08-07T10:46:33.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c5/c8e70b60b704299555f55db468eb46b1c81bfc60201ffbfe20407d89870c/librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389", size = 517142, upload-time = "2026-08-07T10:46:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/767a90c41f5d381b3195bc88ac0ec4afda35777c9c781e1f9848fedd965e/librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c", size = 558714, upload-time = "2026-08-07T10:46:36.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/3c0624b8dc8301ab808f2b3a910995bcabe28df070fb9a0e5505ae997dae/librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e", size = 104426, upload-time = "2026-08-07T10:46:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/e91c0382304bedb2db9c6801897319a9dcb68daac5e975819b562362f20d/librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053", size = 125057, upload-time = "2026-08-07T10:46:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/79f35b8c31a1881893b7e62be80b2573f06e38db47c33065749293ee1b97/linkify_it_py-2.1.1.tar.gz", hash = "sha256:a78f40fee177eb912e9d2375074108378523c38d3fde5d3ee804f465b6cfbfee", size = 30889, upload-time = "2026-08-24T17:16:57.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/3d/e34b19cd144071c583317268c4feb2c59c03ac57eef69753410c7abb11c0/linkify_it_py-2.1.1-py3-none-any.whl", hash = "sha256:8539a6b470efce90ba9b69e39b848e5b15b7ad89f7f98ca17d3532c243f987dc", size = 20532, upload-time = "2026-08-24T17:16:55.965Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "memray" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "rich" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/3b/8f9736cbf698e62cb7efb0e1715a30d6d06b3cffd980b435209eb522d5f8/memray-1.20.0.tar.gz", hash = "sha256:ce1f1d900948d57d7db5b5d8d81f4ebfcb798eff674493503ce5bfaafcea84dc", size = 2416480, upload-time = "2026-08-07T20:16:20.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/65/2d782edb420aa21ab1a27de559a82f67a9ebb0eb09c49b9f254debb27b6b/memray-1.20.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e073ef4685c7f7c2e9c5dabcff8ff46cae66daf6aeaea4d017239b2940b4ca82", size = 2225253, upload-time = "2026-08-07T20:14:19.232Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/56a08b6f4534d54888f5455b7f31ba752c6ba264c927941554e2cc431e98/memray-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83eeeadb3a14ecd8180fe53fe7f0287a84d4146f7fdca986291a5fc1f4eeee97", size = 2195549, upload-time = "2026-08-07T20:14:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/df611ca2dc051830e2bcc71aec3295e1aabc6b6733335029b4a435401b62/memray-1.20.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9b21b9cb85699f9b8df664f6c1633e2a9ce9072e8a7ca15700075e4111b001ed", size = 9682176, upload-time = "2026-08-07T20:14:22.702Z" }, + { url = "https://files.pythonhosted.org/packages/26/ad/3871c5b92312f16c00bf1c445f2b9d65386ea44c0ff3a581a315c6849fa4/memray-1.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e430f8f8533d17bbf45ad0e0c2f16c0e0b767729234771b06745670b61ea5f26", size = 9921428, upload-time = "2026-08-07T20:14:24.499Z" }, + { url = "https://files.pythonhosted.org/packages/31/2f/37f33be92bad8197f15915f4b49b6484981f1018b591fe52ccde720e9f5e/memray-1.20.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c80ecc9259acd888647a385a2f429f453f7ba5c0b1af363babd1ad8a1210da6", size = 9376862, upload-time = "2026-08-07T20:14:26.646Z" }, + { url = "https://files.pythonhosted.org/packages/fe/77/a169cfdaf4bab551150f1e70ae7c3aa7fc85f3107b0a9a4ce7a6adbd8a4b/memray-1.20.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5dd0d4220b607d85aaf6a7275d032915ad43a170b6ae9c73943952d39c224b9b", size = 9571223, upload-time = "2026-08-07T20:14:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/00/f3/d924067be6716782b3a2978b00cce03383b033396e788d5453546f687198/memray-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b41289728ec76b48e83a1af9e1e45e997c4e4b692bfbdeb07cfd814bccb24c40", size = 12227745, upload-time = "2026-08-07T20:14:30.911Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/88237bdf1a6cdff01a07ec6cf46a5816eadd1ac5a72924a87485ebf471e0/memray-1.20.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:996db26e0451301ed935d357854bf84ff1e41bc43580901bf37528a09a8dac3c", size = 2224212, upload-time = "2026-08-07T20:14:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/24/d5/a742f55f70d36c2a88ba644a8fea0933dbe77a5ca65f679ba531a36daba0/memray-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5edcfdd7071628dbcfe5334875cb1d615e26f39a613a2cc2767c193a544b91de", size = 2194748, upload-time = "2026-08-07T20:14:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/2c9bfc4a3cbf88fb8c1763fa8f2866232737814dde55172a55e2b0f0398e/memray-1.20.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3d40d606d0f1d248bb5a00701de84d8faab8ec5b6b383305a36f14c1849a7125", size = 9738014, upload-time = "2026-08-07T20:14:36.466Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/3ed35c62736666602c8f025f9c8fc5651effb710f1e03bb142071e06ec57/memray-1.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53231e49d55b10ac10ac8739a2592230f0794320892756f957e0026f99f38618", size = 9952402, upload-time = "2026-08-07T20:14:38.543Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/5179062328a85a00e8733b00b894828a47ba1635003edb7ec81e36bf28bd/memray-1.20.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0601310f1be3fd5012d888fa73354c204d7eb85cd42a89bd91160d3b8a1ada9e", size = 9439556, upload-time = "2026-08-07T20:14:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ac/8a969428762b0264efdfe81d101d4d2b5e07452258908e570b22e3221d89/memray-1.20.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64121d1b3ca248e0af39df09593bd2b04779ff8ee5261ea9b46ae5e47bc1c7e", size = 9642447, upload-time = "2026-08-07T20:14:42.584Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d9/6481df3b019e3b10661be7216bc457c79c819542a58cad4682cf065b6fc6/memray-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d55e9627b6d6a868aa45995a965f3396c56ac7df5bc567b5293068412049557", size = 12287959, upload-time = "2026-08-07T20:14:44.654Z" }, + { url = "https://files.pythonhosted.org/packages/83/39/3038dd5a10a1512ab7a0ad30c09e13ea1ad26e09ee70d319d037dafbe63c/memray-1.20.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:f5d6980770a2d5d1a4f3e8d04d6f92309119671c1f830959a8bea868fab0b01f", size = 2226078, upload-time = "2026-08-07T20:14:46.831Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b5b7e0ed44298d521ac0d6d334fae7d934f7a7b63be5c52933ebfcf7403d/memray-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7bcd5094cbb9bd1d11a5dfcc523daaa99f743e645a8b0b0805beaa2bbddbf356", size = 2194301, upload-time = "2026-08-07T20:14:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/a1afee889818ba9633638707bea93ef0db644dd4ad6be13bcd4bcaa3b739/memray-1.20.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f6653ac2fbd49e66e2883575fa8c5dde127f4699205415d1170a8cf7d1db1c86", size = 9873569, upload-time = "2026-08-07T20:14:49.563Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b6/807399cc9e4e733b15206be18244a34b5c7606c47a803cca2c55062ce487/memray-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57981abd8c526d4375c7f640a9d27d5e319b888e79b51688a8fae2f947fe8294", size = 10141659, upload-time = "2026-08-07T20:14:51.588Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3e/ae31597901b5e3bffeca5c5db916f3ad6d1a45ec1547f72821025bd98501/memray-1.20.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a6d63a3df7eb96809b82cbd9445033ec1d51574be0f9b3eddf4d64632af7162", size = 9555506, upload-time = "2026-08-07T20:14:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f3/54f218a7c7d604f6b11cf23ae74988bc7749e861e7e91dab5ee615d3293a/memray-1.20.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68dca0caa0b1d1b8474ee54efb98eeb33f548d28bb246113caac45125da7b9c3", size = 9794360, upload-time = "2026-08-07T20:14:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8e/b35e61137dac319394809cabfb5405c98bade1f129368aef2d81990706e1/memray-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b40bf09977f84afe815cca1f34f8a29040a9e757b55467d513ecb9262e6bd809", size = 12438571, upload-time = "2026-08-07T20:14:57.725Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/f9f775da2a08e6bf877b38c9ad83ae140852e2f98a23685f6a04ff4bda2a/memray-1.20.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:359824cc26c9a208e83ab058850e9bf16592d7928307e8c2dc6c7b55e6b6cfa6", size = 2225759, upload-time = "2026-08-07T20:14:59.731Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f2/351f0d534b8df45ac3cea2c90a14050351304f47a019db5ceab3363aa090/memray-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60f3bf8762adc15a2214f44ad631cddb9e4fa4f4a0dda6aa0ccac6ea71239283", size = 2193524, upload-time = "2026-08-07T20:15:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/b6fe8e3120a28013709b34de654ce90b0fec1c89dec4392cd1f3d8d8a1c5/memray-1.20.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:546a60027fc9c8a5fbeea62dce45e830d78769a45192098469ee39b2d75c97fc", size = 9871149, upload-time = "2026-08-07T20:15:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/24/8b/e59d5144428046bd43f6f7fbfd04cbbebdba94ca42798fc370abd9b833da/memray-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d2f3f681ce8acd713a7216d6c362387e70122527e741685e82f85cfdf6aedf0b", size = 10129435, upload-time = "2026-08-07T20:15:04.566Z" }, + { url = "https://files.pythonhosted.org/packages/3b/91/1aa88c9f9dda541265aac80bcdddffcc641374b8fe05a8caa6b3f02245bf/memray-1.20.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97539fd71c565c55e208df9ea28ac158cd156b8c33564853685688595c5a991b", size = 9554666, upload-time = "2026-08-07T20:15:06.516Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/d31b07fa7aa757c3d2cf9bc4850eb4cbfbbd3bff1f6dd710620248b57739/memray-1.20.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c3aec7a4d0b0d0eb55d72e815a492ff060a3a46b014f6ae216ae68a006ea304", size = 9791047, upload-time = "2026-08-07T20:15:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/14/0e/65901e28faeb25a27655a43dfdf0e4a36dae5b12f3ee66d7ad849ae8754c/memray-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c18705b70d20161616d3fc9d0b0c7796211b3ab16499dfb29c8f3ab7ece46c2", size = 12431537, upload-time = "2026-08-07T20:15:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/7976092700eecf4e36ae5793020d18f3feab96aabc2f50d8c7669f751326/memray-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:90fc0ebfc6a264fe3b77d5c263678eaf4a47a0f4288e1b6a04ae3439743cd4be", size = 2227009, upload-time = "2026-08-07T20:15:12.733Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/0f44b1b626169d40defaa2ae9286a70d40653ac26865f0604a882a10ffb2/memray-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1503ac18928d2493662af95935b9ccaadbf9f2579d86f6757304c23291a077b", size = 2195287, upload-time = "2026-08-07T20:15:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/09/45b74d8750ddccb9ad76a8ed639b46b2c3990f99dbe77f800f6f524257fe/memray-1.20.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d92692fb5266aca4322e2001e18810207b170dfc7ce2e8f0570c7ee181574063", size = 9871353, upload-time = "2026-08-07T20:15:15.595Z" }, + { url = "https://files.pythonhosted.org/packages/54/b6/d0b4eb7324bf47b52165aea947bb8942807a40684a64f9decdf4b2a34387/memray-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37bc107afee942f162d30160dfd7f28c2b989251c574570f2f920f5bbbb323e4", size = 10112228, upload-time = "2026-08-07T20:15:18.15Z" }, + { url = "https://files.pythonhosted.org/packages/96/18/b70db3a617ba0be54730a0ddf72822df62551d503bba5c131afea1b47da1/memray-1.20.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83cfa504ed4e1061b85af4b3c40228616a852851ebf5968ff1c3d85e363f8d24", size = 9550151, upload-time = "2026-08-07T20:15:20.623Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/3a972ce61e83648f970dbff8d54dcb0a6a54ab298e283cce139c40c8615b/memray-1.20.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5588174a24ac150100faa7ee60b535b0f09eae82bf672133443425a00704b769", size = 9776012, upload-time = "2026-08-07T20:15:22.822Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d3/7d59a88509a301aa02c033b95460266553a689fc99f5b1b0d0f138675fc9/memray-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f51218744259983ad0ca66366ca2ff80158513132ca9bd835b1874a453199dd", size = 12425490, upload-time = "2026-08-07T20:15:24.977Z" }, + { url = "https://files.pythonhosted.org/packages/27/71/3560aa204af1771cb25ab91489c48643dfe1ca0ce93783fb70ad677ca058/memray-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:610643cfa186f7958a476e5589f6e8a1b0b7b3008ab10250a0f02575a692ef1d", size = 2239309, upload-time = "2026-08-07T20:15:27.025Z" }, + { url = "https://files.pythonhosted.org/packages/02/e4/f3981abfecc1572fd24274386f3cbbc36b861704a3e3a10aebe2775aa1a0/memray-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:420bc47528fedf1a92832290c7a7b8a940c2e30bd9337a85811da18a27e07220", size = 2213281, upload-time = "2026-08-07T20:15:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/ee/a04a21557e50d76eef59f0fd4d5f293a2334142540a7180bb627d8ef5b73/memray-1.20.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:59e44042f2495698f6a17fe0a831051ea515242e567f08457ee8546283372993", size = 9836577, upload-time = "2026-08-07T20:15:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/31/66/13ec31746d61855bbe05a090b26a0a9aa6fd741bb3a9a2c0122061d3b985/memray-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffd3c7c53419465922be92654c17ebff577708fa1f74168dbef9eb5efa6bcb02", size = 10087796, upload-time = "2026-08-07T20:15:32.042Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/27da92dcca4c19ca19fdd5c43de474755eac9d16255a0d8f2104d5976998/memray-1.20.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9a5dd9bef7f44331d98174625167bf0a8f61785ed05440d99f581c93790acf", size = 9602483, upload-time = "2026-08-07T20:15:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cc/6b682e4287dfb4bba6d0e0fd8e685fc3b1f0b769707f09c6305217cf8e1e/memray-1.20.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70010acd3d07dbf41aaca0787de3c81c5d8e22e07a6c0fdf04d66c4a376c0193", size = 9758476, upload-time = "2026-08-07T20:15:36.179Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/ffbba95b0c468d831c63aa6e7dc9e7bb21074d1899c310604567d10ca204/memray-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3270aad45dfa0bc7d32bb86b352aefabee765a8658aace8fb9dc8112a07f2e3c", size = 12388833, upload-time = "2026-08-07T20:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/e76c9efcc466273294a5ec41a518de77c8a2e9ee6ac05049f69fff087bc5/memray-1.20.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6b79c356f7caeed51cbd25dee85485b6210803199f769da8bc3d061bd0e0ada", size = 2227064, upload-time = "2026-08-07T20:15:40.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/78/5c481eecbbe2b954631dfa9fea383971aa34d186743f74279df644287649/memray-1.20.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:a5fc5251b02d99a98eb5874cabfcb74b492ab28e1d5c8b55d481ba0d5e273bf0", size = 2195169, upload-time = "2026-08-07T20:15:41.498Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2e/6713da0bec71d2074c6c6a2269b9c69a209090591f277a6db1982cc580a9/memray-1.20.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08b4d1e17219caabc2a01aa17e8e602b9db67a6aad3ba88b134dfc22bace420d", size = 9874606, upload-time = "2026-08-07T20:15:43.27Z" }, + { url = "https://files.pythonhosted.org/packages/f1/40/451ac23a92d9cd0e11901c00377589ba64b0b8c5cfd595ae403dd14f1a88/memray-1.20.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b71fe846cbabce7f33017ac94ba2f157b39e22dbc4f723aad0407dd38520f855", size = 10114282, upload-time = "2026-08-07T20:15:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/a6/88/a426f7229b8e72b5d45a51781018d6c696330633bee149a5b2effbc7d426/memray-1.20.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab77856a592b04586ffb4a87e26d4de440ee3aba6facbaeabdb20855950fdebc", size = 9553111, upload-time = "2026-08-07T20:15:47.15Z" }, + { url = "https://files.pythonhosted.org/packages/82/24/cae0361d483c6816ad4e2607aaa8864902c6d7cd808dafc6c0ad9c5f99c2/memray-1.20.0-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27dc015d1bfb9e43dc0030b412ba435ccff89cd5b7b28d7da9c8e630e7cfe3ec", size = 9778397, upload-time = "2026-08-07T20:15:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a1/9be6ff8023790e2e89f6882add459e6aceb45ea7d8170f460e3431c95fd2/memray-1.20.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:25e4a802488d54030c30a6bdb969fd79f701228db35e6a834eac75259c69018b", size = 12429015, upload-time = "2026-08-07T20:15:51.769Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fa/af4f576cce0516e8eec2d25ed83106bbf1da866a6d275b9ea6a6d64efa81/memray-1.20.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:9d0c12433fde039a594b6bc254758c236db5532a4ee2ed758b9ec73f8fc78f62", size = 2239298, upload-time = "2026-08-07T20:15:53.916Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/71d39e6ebd1ee3ebb3c0037c1b0c32460bd413623fc67ac50ebfb4ee936a/memray-1.20.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cdf08e64a9bcb598ae76ca467273b580b4658de49b73bd78dd8345a736d78816", size = 2213507, upload-time = "2026-08-07T20:15:55.473Z" }, + { url = "https://files.pythonhosted.org/packages/28/7d/607c4dda5f3752d0696bc2c1f5f24970542bb82ccc337244b2b6a7831885/memray-1.20.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7e106a6dfd3823194a5ecd0b147ea7cffae900e422402cfac9ff4b2c774b4695", size = 9836320, upload-time = "2026-08-07T20:15:57.098Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bf/b7bff1759a95230bf4191b7ee4a66867fd4e78e02849fa7032464edf853d/memray-1.20.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b9ec82a31783a6e468135a6f8a7a996fbc31530a5b725faffb5a8be7e2431a2b", size = 10082346, upload-time = "2026-08-07T20:15:59.545Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/0873448e1445a67679a7168a32890dd75b0d63dfe7f07bca3a58d11fe72b/memray-1.20.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8c6ee6c3df06abf2d4230d0f2c0440e91e0c8fd51c1fe9b32599485254d1f56", size = 9593794, upload-time = "2026-08-07T20:16:01.611Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/2eb1717d80308240aa41d97915fa5fb372c1f71ef68c844f162350295a4f/memray-1.20.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734a082db18947f8afa609b2b4daca20a8b45100a3e6b6c0e4321d59ed781288", size = 9749626, upload-time = "2026-08-07T20:16:03.523Z" }, + { url = "https://files.pythonhosted.org/packages/43/50/76598b0e0bf727f4bcdd1a65ac50d609d4952c7d736c27da6a4196468b61/memray-1.20.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c19934e6b713dbbf35f15d3c84f289e048be613e85039913a500393f10272b9f", size = 12377327, upload-time = "2026-08-07T20:16:05.636Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/5d/1be1c7a49d8fa13dc80f66a85f53333d52cf5206911412006ffdff8fb9a0/mkdocstrings_python-2.0.7.tar.gz", hash = "sha256:8c49faf66d243072d7590a1b5dea028d9d7425fac191f54f096123a4a9c1a783", size = 201598, upload-time = "2026-08-17T16:56:18.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6d/77546d8c26f038fce314a507106954f76270f6c182488bcf9ac9721175df/mkdocstrings_python-2.0.7-py3-none-any.whl", hash = "sha256:1fce5fbfe4ffa6e8136a35351cdc97c3bf55219c7efbd3f92a82260f93235d60", size = 105387, upload-time = "2026-08-17T16:56:16.813Z" }, +] + +[[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 = "tomli", marker = "python_full_version < '3.11'" }, + { 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/eb/b9/de8f67e12d721cdcc8ba6cfc440b989a4ba4dfabe4402ae94dfdd8bb30a4/mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41", size = 14015541, upload-time = "2026-08-15T03:01:53.104Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8a/9e746ab012c67ed8ea3232a613716c306ee8c0b5682c80d8103b4f04568e/mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0", size = 14248142, upload-time = "2026-08-15T03:02:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5c/c99ff2d8d0e2c53393e32dfe22d9aa43a5d959d30db46c786dafd24527d3/mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167", size = 15193309, upload-time = "2026-08-15T03:01:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/124638f745243faae1ff4b37d5426fe41c0f0454535edc82fe8102b56a3c/mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13", size = 15498246, upload-time = "2026-08-15T03:02:46.29Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/31c0781e243836505c0fb5f4e865487d6df1023e4ad959f4ebd4b84a0226/mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53", size = 11155028, upload-time = "2026-08-15T03:01:39.08Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ab/bc2eb0129e72d7d7d93d5e981a78084a9abefda7efa732a7e02f97d6e27d/mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90", size = 10151438, upload-time = "2026-08-15T03:02:19.04Z" }, + { 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 = "platformdirs" +version = "4.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/bb/ebc6636e1ae41314f796ebb7215fd28febb45f9aac72f2b04cb74b5071dc/platformdirs-4.11.4.tar.gz", hash = "sha256:f3373be828247211d0febabea97e238c3dfde8a60b3c90c32756fb52cb21556d", size = 34079, upload-time = "2026-08-24T14:53:49.676Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/be/0ff05fcd2938fb58ad9219bd54135968342d214737e012d62d43f06a2dd6/platformdirs-4.11.4-py3-none-any.whl", hash = "sha256:e34ff91a24bcddc6d939b878bdf3f5c437c9c46fe9e212b1bf455fdf1ee57586", size = 23741, upload-time = "2026-08-24T14:53:48.406Z" }, +] + +[[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 = "prek" +version = "0.4.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/51/135dc6ba2c021ce32b40700c8c337db72d802893e15291f4b3056076582f/prek-0.4.14.tar.gz", hash = "sha256:f6d0952e31ffd6e508660749dd51b8d8de96e955ed12c40e411f3224f502fed2", size = 537668, upload-time = "2026-08-17T04:27:55.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/75/727724174d419cab6e11e0c22c3fbcdd083312b0a0202dfa6befdb7f1bcb/prek-0.4.14-py3-none-linux_armv6l.whl", hash = "sha256:cf7fe2e07c99948ca3326fbd4254054cf4caa71a69cf9032be09c3938544e1e3", size = 5878406, upload-time = "2026-08-17T04:27:30.135Z" }, + { url = "https://files.pythonhosted.org/packages/34/b6/82dfb41347342b4c53c1ddd27cfa81d3e1cf4ad0d5d76493c51e8da58dc9/prek-0.4.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6c8043aa5555c2ada561f4c69429e5117f370ad92f7d708340613b0965e725bd", size = 6216669, upload-time = "2026-08-17T04:27:31.795Z" }, + { url = "https://files.pythonhosted.org/packages/5b/68/ade0ba8b8c0044a3f7ca1a5cd7c97bc10656398c67466b04235f0f0b7418/prek-0.4.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:355570f0d8366a56817e55f44edf736ffc9ff2323894e21cd739ab519d343541", size = 5728085, upload-time = "2026-08-17T04:27:33.322Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/8ddb67fb000d1ad4c95288e6c7e27989d3c172cd88893e3a6f9cdd7199fb/prek-0.4.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:334f48a2b19e63c5ab741e601cb6088caaa11d1b75146900dc298b055452d551", size = 6043650, upload-time = "2026-08-17T04:27:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/5b/49/2780489798147fa1e81fa017af384a40229802cf36680cf72c74478d143c/prek-0.4.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fbb17270df2dcb3c1aa6edfaa68f850d7968862413e58b1572e41c981f644f9", size = 5786094, upload-time = "2026-08-17T04:27:36.282Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/27a4cdfa767b46663eb32a2b5f78ebaaeefc6e5caa87a21587216a504e31/prek-0.4.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ec46be0b1b45943a0746fdf69feeede274b0230bad32b11e2489bb600846081", size = 6239874, upload-time = "2026-08-17T04:27:37.984Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/5ae58d95bc9dbad75ea13aea83fe0acfaca37d027c6bd7f9f1d1d97f3666/prek-0.4.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a21eea6ee996dbba351c1af8c6bd1959a61c37949757c0b97aef3edde82c126a", size = 6968009, upload-time = "2026-08-17T04:27:39.48Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1c/c6a7800406f987559fc8e7c2cfd257a5a68096e806e953f143b488e00e4f/prek-0.4.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34169cb1c8dfbe4b9cb7af164f5c1c3dff68929c61ca1e1de28d378b32720836", size = 6443370, upload-time = "2026-08-17T04:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/92/38/4bf84223216ce2d31b500691d1644a9f35d6e468cb8ecb9a7dfbc66bc82c/prek-0.4.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e39772ebf579f957fdbbc66ef6a7f7433bf603128cd60d5c804c70caac5f69c0", size = 6056449, upload-time = "2026-08-17T04:27:42.722Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/f1980039ab7bf8342c4aa4da2a4cafe40e6a840f456d5bbf86ba024fcdca/prek-0.4.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a79e5940354a789e1311172a960c5daa75e3f643a47ee2be85b7d2ca1d7d980a", size = 5839941, upload-time = "2026-08-17T04:27:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e2/67f067dcb912e157bc7f4cd2aeb422a0de65e7744c675d7f439676f6c832/prek-0.4.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60b6539c1b28804807849173a6b49d467f6f36ca98bbb4536f5e34114d79942a", size = 5762723, upload-time = "2026-08-17T04:27:45.737Z" }, + { url = "https://files.pythonhosted.org/packages/84/39/6669fff5c3336a2b79cf853c86b95547cf9be720a2ee7e4ae5eaa6cb46a1/prek-0.4.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:24cfabd9e5b8c5546ecaef562841a41a255a9dec1b7ae7761ce5a7a024ab9dd9", size = 6090040, upload-time = "2026-08-17T04:27:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/2a/23/c23210be4c89795a854e76146c2465aaffc3d85ea83df61c1d4d5bb41bca/prek-0.4.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1f08d368da45439f949885bb0b637bc7dfe6910140d5285fa8bae058697ddd06", size = 6570273, upload-time = "2026-08-17T04:27:48.673Z" }, + { url = "https://files.pythonhosted.org/packages/00/9c/4f10728ed36295347e84f01bb3e4c9078c9e337edc66215f5e2066b30551/prek-0.4.14-py3-none-win32.whl", hash = "sha256:5bfb30808ce2099c67d2a2d4cd68dc031e3fec1eeb61d73ef51a8f7c04d021cb", size = 5586715, upload-time = "2026-08-17T04:27:50.471Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/d080a157d4e92927f7618d62d6a23979ef64a10c76a040cb8a8a194c50d7/prek-0.4.14-py3-none-win_amd64.whl", hash = "sha256:29364012d5704475d1092eb8a96ea30b163279096ad5e0c80a620fffa79bc639", size = 5969946, upload-time = "2026-08-17T04:27:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/37/e1/6fc64bb82e7270f61707e00b6d3154a4592ae0b2d3bd308173aa7aabe0a1/prek-0.4.14-py3-none-win_arm64.whl", hash = "sha256:ff588c02e10c8d05150763607671a22d5585c0ff7036c884d3489c2726eb215c", size = 5729906, upload-time = "2026-08-17T04:27:53.52Z" }, +] + +[[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 = "pymdown-extensions" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/2db4b414de89659144488e0d9c6c0bf0c8395841dc12d81d0532cc6ef310/pymdown_extensions-11.0.2.tar.gz", hash = "sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d", size = 855419, upload-time = "2026-08-22T19:28:47.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/43/9f45ec4d14e596efc32c925a78104934790438b0c0628b70d741016734ad/pymdown_extensions-11.0.2-py3-none-any.whl", hash = "sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5", size = 269929, upload-time = "2026-08-22T19:28:45.389Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +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 = "pytest-codspeed" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/b4/cf932fcd1960a2fd6d9b09eb403253a8709aeee975961afa6299239a830e/pytest_codspeed-5.0.3.tar.gz", hash = "sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f", size = 324571, upload-time = "2026-05-22T16:20:49.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f5/a8f70147216e4b84046ca406d03ecc8e83e3ea56ba1bdca0bb79cca79fee/pytest_codspeed-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63", size = 366249, upload-time = "2026-05-22T16:20:39.985Z" }, + { url = "https://files.pythonhosted.org/packages/f6/bd/7a4dbcf457fcc3ed788c55d402f3af2671e0e342b6098090fd590aa8712e/pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510", size = 932229, upload-time = "2026-05-22T16:20:37.283Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/414ea4c66559f24ec06aeb6db62bfc7079582dac1452e648affe1eb5cfb4/pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693", size = 934647, upload-time = "2026-05-22T16:20:31.997Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ef/32ce60d42a4aa43e728d988e13eb6568fbc7b10a514517b459bafd3f2b94/pytest_codspeed-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093", size = 366253, upload-time = "2026-05-22T16:21:10.013Z" }, + { url = "https://files.pythonhosted.org/packages/2a/15/c66ef90a793c5d2c039e63a1726a5e55c678be2618b0f5f1660d0f79e25f/pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c", size = 932465, upload-time = "2026-05-22T16:20:34.265Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7b/d231279301967f05b7909160489e85ee3a1b9da76094ea25343faba1abc2/pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9", size = 934925, upload-time = "2026-05-22T16:20:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/c2/22/456c48160b761d5028c8afa119f085a9fc42855a783a13d73918078969f0/pytest_codspeed-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee", size = 366255, upload-time = "2026-05-22T16:20:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/74/33/ac7441fa937c9d9f158083a8c46920a5a5c81ed3c5f96240fc8d650db5c2/pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7", size = 932325, upload-time = "2026-05-22T16:21:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/77/bc/8b994adcb9e9016e7d9a808056a3dd9cca21441e432ef456eae2b697d7fe/pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf", size = 934885, upload-time = "2026-05-22T16:21:01.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/e032451e9e0a06b0c4bff53105f62b693d9a54595dd8c024693741ce3380/pytest_codspeed-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4", size = 366253, upload-time = "2026-05-22T16:20:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7b/ae76fd8ac656b9695806a6aafd5f22ec32e6ce20e266a58f9112e01d3cd8/pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901", size = 932360, upload-time = "2026-05-22T16:20:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4a/dfd43d943fdb143be4fd62f34c2793ba349dc27aa188e521d19d629aa7ab/pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c", size = 934928, upload-time = "2026-05-22T16:20:38.62Z" }, + { url = "https://files.pythonhosted.org/packages/04/6a/fdcec19c7f267c195f147c51d3fd2245f6b8d09b80495ed0a90c008e0842/pytest_codspeed-5.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d", size = 366259, upload-time = "2026-05-22T16:21:06.039Z" }, + { url = "https://files.pythonhosted.org/packages/6a/96/c6b03b81dcd21ae3d6b32cca0b3c10149fa378eb21b338d4b63c9eb8050b/pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40", size = 932395, upload-time = "2026-05-22T16:21:04.804Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/56ad8f1cc7d6962f8a680141b361e93467a2abc53d976cd9d5e1edd740e3/pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e", size = 934994, upload-time = "2026-05-22T16:20:28.809Z" }, + { url = "https://files.pythonhosted.org/packages/0b/54/9096c4545f09da94b1b00f3be2fe4952949e86c9bcafca9a29b26aed1a75/pytest_codspeed-5.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb", size = 366311, upload-time = "2026-05-22T16:20:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3c/24c53f67a38ad48cb087105ac30a8aa0923223ee274ea9bf2dc705edaa59/pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853", size = 933169, upload-time = "2026-05-22T16:20:43.178Z" }, + { url = "https://files.pythonhosted.org/packages/d1/de/2213f868fa7694f743f96cccbc07e757f45c920c523cccc2da97bc8652df/pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3", size = 935522, upload-time = "2026-05-22T16:21:03.398Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/5dfea1c031d6cccc11653464828edf205c30f798caf5b2a85375aacd914a/pytest_codspeed-5.0.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5", size = 366275, upload-time = "2026-05-22T16:20:59.463Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2b/af4d1b612f03b98a6cf3c7d5f62678917a60110a8bf380d49ab408b31137/pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7", size = 932537, upload-time = "2026-05-22T16:20:54.945Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a2/c7ec45e36a61b418efb2a3cccaa67a0c2fcf1f21d5880f64c33114f0c249/pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792", size = 934153, upload-time = "2026-05-22T16:21:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c7/d5bada9618a0af56a5c8065fc61280849cab8e7c1e24025807a51c3157ce/pytest_codspeed-5.0.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb", size = 366339, upload-time = "2026-05-22T16:20:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/fb27aeb40a81320e7349553b877a21333c897b27c8dfe215630452908f36/pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a", size = 933055, upload-time = "2026-05-22T16:20:44.861Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d9/6f2d69e96deaf0475a695fc9195af59e7a3b5fab50782855e65c63a7bc28/pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad", size = 934481, upload-time = "2026-05-22T16:20:58.264Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b2/1d2a993c532146dce9eca5b5942d51898021c3579ce18b2454f932a915f8/pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378", size = 74033, upload-time = "2026-05-22T16:20:26.814Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-memray" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "memray" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/61/d0ab0418ea7a2494a3a020a4bc388011b59b820e8335797cdc6489cfd9fe/pytest_memray-1.10.0.tar.gz", hash = "sha256:38f1068aa8562887d452ae14f77509f138be1acc74542dd5fc41e74f651cf932", size = 246123, upload-time = "2026-08-07T22:07:28.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/d7/9e6d28d95cfe6a1f3643c46b06266f2eb2ea711b0bb6ef1b0f1697a19c2f/pytest_memray-1.10.0-py3-none-any.whl", hash = "sha256:eecdbad5c4df3c385892b1cfdde48266f21ae601a44c4fe6557b781d7d409b95", size = 20122, upload-time = "2026-08-07T22:07:27.269Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, + { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, + { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, + { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, + { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, + { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[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 = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.57" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/f4/fa40086c46a2e59e3d9239031f76623622e60e0d79f3df1282df2797a5c4/zensical-0.0.57.tar.gz", hash = "sha256:25fcbdf89a57153cc3ad1108a89d17c7226da5d3c551a8839c69cbd9c472a9d8", size = 4000458, upload-time = "2026-08-21T20:43:49.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/b9/49c37dc65105d1ca4a8b600a02c84ece00218d2293b2630611c620185ca3/zensical-0.0.57-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98867d1a6ea2c57f1ebcf4902f61601f427350f2df0c04e30cfac8ba6163cd29", size = 12888507, upload-time = "2026-08-21T20:43:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/05/f7/54539984418de11387bbace39a744195555d32c98c95bf4d112b432548f5/zensical-0.0.57-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d7935d77d73a279545052e05d89d31960f30c1f33f53933f4c101fa271aee74", size = 12778169, upload-time = "2026-08-21T20:43:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/74aa60aa4cfecd5bd31ce60cb6a092cb56f1bc1aaadcc173463861ea4eb5/zensical-0.0.57-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7046d433511d97aa603915f0f6792d15b7f839793abc2b66ab7b7ff753ecff5", size = 13230823, upload-time = "2026-08-21T20:43:25.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d1/742d2487dd65dd18277daebcd37db56d5bd4a2408df02bde703ef8fb7b64/zensical-0.0.57-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab85c5066b95e3a877cf8971e4ce30abb1ca1459fbfcc631f0a5a2bab56351a4", size = 13170523, upload-time = "2026-08-21T20:43:27.456Z" }, + { url = "https://files.pythonhosted.org/packages/56/6f/12b570775d344f1a3d77e26d4ae0160bcac9e41ca38f7135352ccdf9b2c8/zensical-0.0.57-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f13d1b57ad3c8b8634933a93ea870ebac11245fe0c968d27fd2a059ee1c6311", size = 13549941, upload-time = "2026-08-21T20:43:29.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4e/436e6fc76674244c084ef7f6f17dc5ff85c76b15aef77c48b703fd0a2dda/zensical-0.0.57-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:021dd8fb70d1816cd012684fcf45d32b8f88a0cd28b7cbe71e5f8564f6d5764d", size = 13210086, upload-time = "2026-08-21T20:43:32.098Z" }, + { url = "https://files.pythonhosted.org/packages/ef/52/20f3aeda9af1090f24241670a5cc20fff7494545fea9f5fa094c82f3dbdf/zensical-0.0.57-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7e10f3c27fdc3eac3a9ae6ddcd87f3f00edc9f332050923313c95537961bfadd", size = 13408253, upload-time = "2026-08-21T20:43:34.258Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f2/2b18ba2f19674dbfcf745f3b66e005cc8efa66a1bcaba5e1b4f79467868a/zensical-0.0.57-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:78c85fee55c5aac3bdf8157e980c56397dca835167a5577c5429b5eb24ed990c", size = 13446689, upload-time = "2026-08-21T20:43:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/05/ba/68cdba447a9097e5f97742eef046020c6fa42d82972849b3a46a0718e890/zensical-0.0.57-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:478d252e1924f3876e72cf7806967cb62e50d86eddb3da04bf43e882b532fa1b", size = 13598580, upload-time = "2026-08-21T20:43:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/89/6358a4df272328bed5bea90b04d43e73758bc45ff058c5cb2665e1147314/zensical-0.0.57-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66a9ca6b5f625b2a2b215eec2f3c72843a92d5d512042045ac6351d5dee9b339", size = 13557609, upload-time = "2026-08-21T20:43:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/77/e1/8831301a24f736743e3788f09ea048918b0bdcea4aaa90f7770a433d6eec/zensical-0.0.57-cp310-abi3-win32.whl", hash = "sha256:f0fe3dc27ca7dc4e168eddd0fe5b0f4d44e311fd4e0019241e289819e445203c", size = 12446805, upload-time = "2026-08-21T20:43:43.097Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3f/5d0ecd77d9ce962fdfde22dec036f4257a43ef6dbd55fb5c05fd294985ad/zensical-0.0.57-cp310-abi3-win_amd64.whl", hash = "sha256:a756834025c1c54e806e943be6d8df1048d0f8bcf6086e958568407a070a2572", size = 12716781, upload-time = "2026-08-21T20:43:45.273Z" }, +] + +[[package]] +name = "zizmor" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/f8/f4e3fc0b316d5241b6d6968e8fb702e28446bc7d3c1e2b229f4caa6eacf2/zizmor-1.29.0.tar.gz", hash = "sha256:60e34e83c67064e0036989c7c525d13413e897aa4c4f683f1efb2048cdb28a47", size = 571865, upload-time = "2026-08-01T21:09:19.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/97/667ef4db0ca9225ee402c1b947b5b6f17fd234d72c15a71db150b7695c62/zizmor-1.29.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ea72f84d610643d57f96430c655a3780d0b874e477d32e14eae8e910f6cce1fd", size = 9037504, upload-time = "2026-08-01T21:08:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d4/9fc7deaf75778e7516fa1d6c836377c3cb5d203dedc28899946b6f11ecdb/zizmor-1.29.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5aafe617d7b1e0c0c15d58fdf20495f360f74a791dfa136f76630b4cc06c2a34", size = 8654426, upload-time = "2026-08-01T21:08:59.212Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/6db714fb0aa08aeec62eb9d6ad6a443a1f4ed50d4c0b789944ae55fb83e4/zizmor-1.29.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:67644ae8d6d0394204b9a488f7d86f0dd66fe562f4ba85fc53e6105a6bfc7b6a", size = 8918927, upload-time = "2026-08-01T21:09:01.46Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/a12edc0c0c0a0101c54dbb9099ff08f8de2fcb35c36cb706db3deb2c2728/zizmor-1.29.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:81e4093fed5c8a41d6ae7bb773085a9d2e6c0b0a0b560d46a9c76d69be0a07ed", size = 8500655, upload-time = "2026-08-01T21:09:03.677Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/dfa67018b76bc4f2f50e265e8cbd1293833d1b1de5f3f02fbbb7487ae9c6/zizmor-1.29.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:587b99c2e1b34575c6c8565c2bfde415ca8bc0310f5589f19bc948c8dea10a20", size = 9351035, upload-time = "2026-08-01T21:09:06.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/1b/93cdd5a06984b394d90001f9778008a21689052904109080a09952626c99/zizmor-1.29.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:061600f23c46f2e400bcdef666c236de7e5c0b07dd6ca046daa001eb1514b909", size = 8941717, upload-time = "2026-08-01T21:09:08.861Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f4/8d9e54405b477bc8e4b56c1a60123fca26c109ea6a762eea104fab32555e/zizmor-1.29.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:332546480be38aca95c149f835e0dcb7679ab5d74618a90c6ccb3fa6b8c7b99d", size = 8468289, upload-time = "2026-08-01T21:09:11.096Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/06d57ca830cc4653369c5aca22cccbf04c8c36ee84a67f351214e556bad8/zizmor-1.29.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a7462b9ab45d72a20ad5ab8193b430df8184c59e2bf46954ddd09496f2f00b45", size = 9446505, upload-time = "2026-08-01T21:09:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/253d9a3538e0ea6f96a3bcd69839c0c5a816a00299620b58a10b5bd1df59/zizmor-1.29.0-py3-none-win32.whl", hash = "sha256:8c759e68cd866375030ca39e19e2de47a056b7be7288c1620e2d5b4c274f631f", size = 7655723, upload-time = "2026-08-01T21:09:15.758Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2d/7919bc23475273ed8038a031fc24bc3f2005c78e608ce8df96746ef0fb98/zizmor-1.29.0-py3-none-win_amd64.whl", hash = "sha256:0fb85948ba5ffc7a8116eee36fe9cfc10167225c97bd2810e3378e66a9fd27c4", size = 8785519, upload-time = "2026-08-01T21:09:17.513Z" }, +] diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..c048e39 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,113 @@ +[project] +site_url = "https://danfimov.github.io/h11/" +site_name = "h11-mypyc" +docs_dir = "docs/src" +site_description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +site_author = "Nathaniel J. Smith" + +copyright = "Copyright © 2016 Nathaniel J. Smith" + +repo_url = "https://github.com/danfimov/h11" +repo_name = "danfimov/h11" +edit_uri = "edit/master/docs/src/" + +nav = [ + { "Introduction" = "index.md" }, + { "Getting started" = "basic-usage.md" }, + { "API documentation" = "api.md" }, + { "Examples" = "examples.md" }, + { "Details of our HTTP support" = "supported-http.md" }, + { "History of changes" = "changes.md" }, +] + +extra_css = ["stylesheets/extra.css"] + +[project.theme] +language = "en" +features = [ + "announce.dismiss", + "content.action.edit", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.footnote.tooltips", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.top", + "navigation.tracking", + "search.highlight", + "toc.follow", +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to system preference" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/danfimov/h11" + +[[project.extra.social]] +icon = "fontawesome/brands/python" +link = "https://pypi.org/project/h11-mypyc/" + +[project.plugins.mkdocstrings.handlers.python] +inventories = ["https://docs.python.org/3/objects.inv"] +paths = ["."] +options.docstring_style = "google" +options.show_source = false +options.show_root_heading = true +options.show_root_full_path = false +options.heading_level = 3 +options.members_order = "source" +options.separate_signature = true +options.show_signature_annotations = true +options.signature_crossrefs = true + +[project.markdown_extensions] +abbr = {} +admonition = {} +attr_list = {} +def_list = {} +footnotes = {} +md_in_html = {} +toc.permalink = true +pymdownx.betterem = {} +pymdownx.caret = {} +pymdownx.details = {} +pymdownx.emoji.emoji_generator = "zensical.extensions.emoji.to_svg" +pymdownx.emoji.emoji_index = "zensical.extensions.emoji.twemoji" +pymdownx.highlight.anchor_linenums = true +pymdownx.highlight.line_spans = "__span" +pymdownx.highlight.pygments_lang_class = true +pymdownx.inlinehilite = {} +pymdownx.keys = {} +pymdownx.magiclink = {} +pymdownx.mark = {} +pymdownx.smartsymbols = {} +pymdownx.snippets.base_path = ["docs/includes", "examples"] +pymdownx.superfences.custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, +] +pymdownx.tabbed.alternate_style = true +pymdownx.tabbed.combine_header_slug = true +pymdownx.tasklist.custom_checkbox = true +pymdownx.tilde = {}